Skip to content

Commit

Permalink
fix: streamline type collection by removing 'flags' (#2025)
Browse files Browse the repository at this point in the history
## PR Checklist

- [x] Addresses an existing open issue: fixes #2024
- [x] That issue was marked as [`status: accepting
prs`](https://github.com/JoshuaKGoldberg/TypeStat/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22)
- [x] Steps in
[CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/TypeStat/blob/main/.github/CONTRIBUTING.md)
were taken

## Overview

Previously, types collection included both _flags_ (`ts.TypeFlags`
numbers) and the actual _types_ (`ts.Type` objects). I don't remember
why I separated the two. Everything is capturable by _types_, and
`checker.isTypeAssignableTo` only applies on _types_.

This removes the _flags_ and goes with just _types_. Doing so fixes the
bug around enums not being captured (because of bypassing assignability
checking).
  • Loading branch information
JoshuaKGoldberg authored Nov 30, 2024
1 parent dde3e63 commit 29822a8
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 157 deletions.
40 changes: 0 additions & 40 deletions src/mutations/aliasing/aliases.ts

This file was deleted.

9 changes: 1 addition & 8 deletions src/mutations/aliasing/joinIntoType.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import ts from "typescript";

import { isNotUndefined, uniquify } from "../../shared/arrays.js";
import { uniquify } from "../../shared/arrays.js";
import { FileMutationsRequest } from "../../shared/fileMutator.js";
import { getApplicableTypeAliases } from "./aliases.js";

export const joinIntoType = (
flags: ReadonlySet<ts.TypeFlags>,
types: ReadonlySet<ts.Type>,
request: FileMutationsRequest,
) => {
const alias = getApplicableTypeAliases(request);

return uniquify(
...Array.from(types)
.map((type) => request.services.printers.type(type))
.map((type) => (type.includes("=>") ? `(${type})` : type)),
...Array.from(flags)
.map((flag) => alias.get(flag))
.filter(isNotUndefined),
).join(" | ");
};
88 changes: 32 additions & 56 deletions src/mutations/collecting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,21 @@ import ts from "typescript";
import { FileMutationsRequest } from "../shared/fileMutator.js";
import { isKnownGlobalBaseType } from "../shared/nodeTypes.js";
import { setSubtract } from "../shared/sets.js";
import { getApplicableTypeAliases } from "./aliasing/aliases.js";
import {
findMissingFlags,
isTypeFlagSetRecursively,
} from "./collecting/flags.js";
import { isTypeFlagSetRecursively } from "./collecting/flags.js";

/**
* Collects assigned and missing flags and types, recursively accounting for type unions.
* Collects assigned and missing types, recursively accounting for type unions.
* @param request Metadata and settings to collect mutations in a file.
* @param declaredType Original type declared on a node.
* @param allAssignedTypes All types immediately or later assigned to the node.
*/
export const collectUsageFlagsAndSymbols = (
export const collectUsageSymbols = (
request: FileMutationsRequest,
declaredType: ts.Type,
allAssignedTypes: readonly ts.Type[],
) => {
// Collect which flags are later assigned to the type
const [assignedFlags, assignedTypes] = collectFlagsAndTypesFromTypes(
request,
allAssignedTypes,
);
// Collect which types are later assigned to the type
const assignedTypes = collectRawTypesFromTypes(request, allAssignedTypes);

// If the declared type is the general 'any', then we assume all are missing
// Similarly, if it's a plain Function or Object, we'll want to replace its contents
Expand All @@ -34,81 +27,52 @@ export const collectUsageFlagsAndSymbols = (
isKnownGlobalBaseType(declaredType)
) {
return {
assignedFlags,
assignedTypes,
missingFlags: assignedFlags,
missingTypes: assignedTypes,
};
}

// Otherwise, collect which flags and types are declared (as a type annotation)...
const [declaredFlags, declaredTypes] = collectFlagsAndTypesFromTypes(
request,
[declaredType],
);
// Otherwise, collect which types are declared (as a type annotation)...
const declaredTypes = collectRawTypesFromTypes(request, [declaredType]);

// Subtract the above to find any flags or types assigned but not declared
// Subtract the above to find any types assigned but not declared
return {
assignedFlags,
assignedTypes,
missingFlags: findMissingFlags(declaredType, assignedFlags, declaredFlags),
missingTypes: findMissingTypes(request, assignedTypes, declaredTypes),
};
};

/**
* Separates raw type node(s) into their contained flags and types.
* Separates raw type node(s) into their contained types.
* @param request Metadata and settings to collect mutations in a file.
* @param allTypes Any number of raw type nodes.
* @param allowStrictNullCheckAliases Whether to allow `null` and `undefined` aliases regardless of compiler strictness.
* @returns Flags and types found within the raw type nodes.
* @returns Types found within the raw type nodes.
*/
export const collectFlagsAndTypesFromTypes = (
export const collectRawTypesFromTypes = (
request: FileMutationsRequest,
allTypes: readonly ts.Type[],
allowStrictNullCheckAliases?: boolean,
): [Set<ts.TypeFlags>, Set<ts.Type>] => {
const foundFlags = new Set<ts.TypeFlags>();
): Set<ts.Type> => {
const foundTypes = new Set<ts.Type>();
const applicableTypeAliases = getApplicableTypeAliases(
request,
allowStrictNullCheckAliases,
);

// Scan each type for undeclared type additions
for (const type of allTypes) {
// For any simple type flag we later will care about for aliasing, add it if it's in the type
for (const [typeFlag] of applicableTypeAliases) {
if (isTypeFlagSetRecursively(type, typeFlag)) {
foundFlags.add(typeFlag);
}
}

// If the type is a rich type (has a symbol), add it in directly
if (type.getSymbol() !== undefined) {
foundTypes.add(type);
continue;
}

// If the type is a union, add any flags or types found within it
// If the type is a union, add any types found within it
if (tsutils.isUnionType(type)) {
const subTypes = recursivelyCollectSubTypes(type);
const [subFlags, deepSubTypes] = collectFlagsAndTypesFromTypes(
request,
subTypes,
);

for (const subFlag of subFlags) {
foundFlags.add(subFlag);
}
const deepSubTypes = collectRawTypesFromTypes(request, subTypes);

for (const deepSubType of deepSubTypes) {
foundTypes.add(deepSubType);
}

continue;
}

// Otherwise, it's likely either an intrinsic, primitive, or a shape
foundTypes.add(type);
}

return [foundFlags, foundTypes];
return foundTypes;
};

export const recursivelyCollectSubTypes = (type: ts.UnionType): ts.Type[] => {
Expand Down Expand Up @@ -149,7 +113,14 @@ const findMissingTypes = (
return false;
}

// For each potential missing type:
for (const potentialParentType of declaredTypes) {
// If the potential parent type is unknown, then ignore it
if (potentialParentType.flags === ts.TypeFlags.Unknown) {
continue;
}

// If the assigned type is assignable to it, then it's a no
if (
request.services.program
.getTypeChecker()
Expand All @@ -163,6 +134,11 @@ const findMissingTypes = (
};

for (const assignedType of assignedTypes) {
// The 'void' type shouldn't be assigned to anything, so we ignore it
if (assignedType.flags === ts.TypeFlags.Void) {
remainingMissingTypes.delete(assignedType);
}

if (!isAssignedTypeMissingFromDeclared(assignedType)) {
remainingMissingTypes.delete(assignedType);
}
Expand Down
42 changes: 0 additions & 42 deletions src/mutations/collecting/flags.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,6 @@
import * as tsutils from "ts-api-utils";
import ts from "typescript";

import { setSubtract } from "../../shared/sets.js";

const knownTypeFlagEquivalents = new Map([
[ts.TypeFlags.BigInt, ts.TypeFlags.BigIntLiteral],
[ts.TypeFlags.BigIntLiteral, ts.TypeFlags.BigInt],
[ts.TypeFlags.Boolean, ts.TypeFlags.BooleanLiteral],
[ts.TypeFlags.BooleanLiteral, ts.TypeFlags.Boolean],
[ts.TypeFlags.Number, ts.TypeFlags.NumberLiteral],
[ts.TypeFlags.NumberLiteral, ts.TypeFlags.Number],
[ts.TypeFlags.String, ts.TypeFlags.StringLiteral],
[ts.TypeFlags.StringLiteral, ts.TypeFlags.String],
[ts.TypeFlags.Undefined, ts.TypeFlags.Void],
[ts.TypeFlags.Void, ts.TypeFlags.Undefined],
]);

export const findMissingFlags = (
declaredType: ts.Type,
assignedFlags: ReadonlySet<ts.TypeFlags>,
declaredFlags: ReadonlySet<ts.TypeFlags>,
): Set<ts.TypeFlags> => {
// If the type is declared to allow `any`, it can't be missing anything
if (isTypeFlagSetRecursively(declaredType, ts.TypeFlags.Any)) {
return new Set();
}

// Otherwise, it's all the flags assigned to it that weren't already declared
const missingFlags = setSubtract(assignedFlags, declaredFlags);

// Remove any flags that are just equivalents of the existing ones
// For example, initial presence of `void` makes `undefined` unnecessary, and vice versa
for (const [original, equivalent] of knownTypeFlagEquivalents) {
if (
missingFlags.has(equivalent) &&
isTypeFlagSetRecursively(declaredType, original)
) {
missingFlags.delete(equivalent);
}
}

return missingFlags;
};

/**
* Checks if a type contains a type flag, accounting for deep nested type unions.
* @param parentType Parent type to check for the type flag.
Expand Down
23 changes: 13 additions & 10 deletions src/mutations/creators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
isKnownGlobalBaseType,
} from "../shared/nodeTypes.js";
import { joinIntoType } from "./aliasing/joinIntoType.js";
import { collectUsageFlagsAndSymbols } from "./collecting.js";
import { collectUsageSymbols } from "./collecting.js";

/**
* Creates a mutation to add types to an existing type, if any are new.
Expand All @@ -30,20 +30,20 @@ export const createTypeAdditionMutation = (
return undefined;
}

// Find any missing flags and symbols (a.k.a. types)
const { missingFlags, missingTypes } = collectUsageFlagsAndSymbols(
// Find any missing symbols (a.k.a. types)
const { missingTypes } = collectUsageSymbols(
request,
declaredType,
allAssignedTypes,
);

// If nothing is missing, rejoice! The type was already fine.
if (missingFlags.size === 0 && missingTypes.size === 0) {
if (missingTypes.size === 0) {
return undefined;
}

// Join the missing types into a type string to declare
const newTypeAlias = joinIntoType(missingFlags, missingTypes, request);
const newTypeAlias = joinIntoType(missingTypes, request);

// If the original type was a bottom type or just something like Function or Object, replace it entirely
if (
Expand Down Expand Up @@ -87,17 +87,20 @@ export const createTypeCreationMutation = (
declaredType: ts.Type,
allAssignedTypes: readonly ts.Type[],
): TextInsertMutation | undefined => {
// Find the already assigned flags and symbols, as well as any missing ones
const { assignedFlags, assignedTypes, missingFlags, missingTypes } =
collectUsageFlagsAndSymbols(request, declaredType, allAssignedTypes);
// Find the already assigned symbols, as well as any missing ones
const { assignedTypes, missingTypes } = collectUsageSymbols(
request,
declaredType,
allAssignedTypes,
);

// If nothing is missing, rejoice! The type was already fine.
if (missingFlags.size === 0 && missingTypes.size === 0) {
if (missingTypes.size === 0) {
return undefined;
}

// Join the missing types into a type string to declare
const newTypeAlias = joinIntoType(assignedFlags, assignedTypes, request);
const newTypeAlias = joinIntoType(assignedTypes, request);

// Create a mutation insertion that adds the assigned types in
return {
Expand Down
Loading

0 comments on commit 29822a8

Please sign in to comment.