diff --git a/src/mutations/aliasing/aliases.ts b/src/mutations/aliasing/aliases.ts deleted file mode 100644 index ab0ded5cb..000000000 --- a/src/mutations/aliasing/aliases.ts +++ /dev/null @@ -1,40 +0,0 @@ -import ts from "typescript"; - -import { FileMutationsRequest } from "../../shared/fileMutator.js"; - -/** - * Type flags and aliases to check when --strictNullChecks is not enabled. - */ -const nonStrictTypeFlagAliases = new Map([ - [ts.TypeFlags.BigInt, "bigint"], - [ts.TypeFlags.BigIntLiteral, "bigint"], - [ts.TypeFlags.Boolean, "boolean"], - [ts.TypeFlags.BooleanLiteral, "boolean"], - [ts.TypeFlags.Number, "number"], - [ts.TypeFlags.NumberLiteral, "number"], - [ts.TypeFlags.String, "string"], - [ts.TypeFlags.StringLiteral, "string"], -]); - -/** - * Type flags and aliases to check when --strictNullChecks is enabled. - */ -const strictTypeFlagsWithAliases = new Map([ - ...nonStrictTypeFlagAliases, - [ts.TypeFlags.Null, "null"], - [ts.TypeFlags.Undefined, "undefined"], -]); - -/** - * @returns Built-in type flags and aliases per overall request strictNullChecks setting. - */ -export const getApplicableTypeAliases = ( - request: FileMutationsRequest, - alwaysAllowStrictNullCheckAliases = false, -) => - alwaysAllowStrictNullCheckAliases || - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - request.options.types.strictNullChecks || - request.services.program.getCompilerOptions().strictNullChecks - ? strictTypeFlagsWithAliases - : nonStrictTypeFlagAliases; diff --git a/src/mutations/aliasing/joinIntoType.ts b/src/mutations/aliasing/joinIntoType.ts index d9ac0f361..1e1b87039 100644 --- a/src/mutations/aliasing/joinIntoType.ts +++ b/src/mutations/aliasing/joinIntoType.ts @@ -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, types: ReadonlySet, 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(" | "); }; diff --git a/src/mutations/collecting.ts b/src/mutations/collecting.ts index 4d74b3f96..1a56bbec2 100644 --- a/src/mutations/collecting.ts +++ b/src/mutations/collecting.ts @@ -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 @@ -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, Set] => { - const foundFlags = new Set(); +): Set => { const foundTypes = new Set(); - 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[] => { @@ -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() @@ -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); } diff --git a/src/mutations/collecting/flags.ts b/src/mutations/collecting/flags.ts index c82a8c53f..37da2b571 100644 --- a/src/mutations/collecting/flags.ts +++ b/src/mutations/collecting/flags.ts @@ -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, - declaredFlags: ReadonlySet, -): Set => { - // 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. diff --git a/src/mutations/creators.ts b/src/mutations/creators.ts index dc45c9183..f0920262e 100644 --- a/src/mutations/creators.ts +++ b/src/mutations/creators.ts @@ -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. @@ -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 ( @@ -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 { diff --git a/test/__snapshots__/fixEnumAsArgument.test.ts.snap b/test/__snapshots__/fixEnumAsArgument.test.ts.snap new file mode 100644 index 000000000..f7cd52460 --- /dev/null +++ b/test/__snapshots__/fixEnumAsArgument.test.ts.snap @@ -0,0 +1,78 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`enum as argument > options 1`] = ` +"{ + "cleanups": { + "suppressTypeErrors": false + }, + "compilerOptions": { + "files": [ + "actual.ts" + ], + "noImplicitAny": false, + "noImplicitThis": false, + "strictNullChecks": false + }, + "files": { + "above": "", + "below": "", + "renameExtensions": false + }, + "filters": [], + "fixes": { + "importExtensions": false, + "incompleteTypes": true, + "missingProperties": false, + "noImplicitAny": false, + "noImplicitThis": false, + "noInferableTypes": false, + "strictNonNullAssertions": false + }, + "hints": { + "react": { + "propTypes": "whenRequired", + "propTypesOptionality": "asWritten" + } + }, + "mutators": [ + [ + "fixImportExtensions", + null + ], + [ + "fixIncompleteTypes", + null + ], + [ + "fixMissingProperties", + null + ], + [ + "fixNoImplicitAny", + null + ], + [ + "fixNoImplicitThis", + null + ], + [ + "fixNoInferableTypes", + null + ], + [ + "fixStrictNonNullAssertions", + null + ] + ], + "output": {}, + "package": { + "directory": "", + "file": "/package.json" + }, + "postProcess": { + "shell": [] + }, + "projectPath": "/tsconfig.json", + "types": {} +}" +`; diff --git a/test/cases/fixes/incompleteTypes/enumAsArgument/expected.ts b/test/cases/fixes/incompleteTypes/enumAsArgument/expected.ts new file mode 100644 index 000000000..7443c3b8c --- /dev/null +++ b/test/cases/fixes/incompleteTypes/enumAsArgument/expected.ts @@ -0,0 +1,11 @@ +(function () { + enum Value { + A, + B, + } + + function withValue(value: string | Value) {} + + withValue(Value.A); + withValue(Value.B); +})(); diff --git a/test/cases/fixes/incompleteTypes/enumAsArgument/original.ts b/test/cases/fixes/incompleteTypes/enumAsArgument/original.ts new file mode 100644 index 000000000..755e6eb40 --- /dev/null +++ b/test/cases/fixes/incompleteTypes/enumAsArgument/original.ts @@ -0,0 +1,11 @@ +(function () { + enum Value { + A, + B, + } + + function withValue(value: string) {} + + withValue(Value.A); + withValue(Value.B); +})(); diff --git a/test/cases/fixes/incompleteTypes/enumAsArgument/tsconfig.json b/test/cases/fixes/incompleteTypes/enumAsArgument/tsconfig.json new file mode 100644 index 000000000..7de2b9de3 --- /dev/null +++ b/test/cases/fixes/incompleteTypes/enumAsArgument/tsconfig.json @@ -0,0 +1,3 @@ +{ + "files": ["actual.ts"] +} diff --git a/test/cases/fixes/incompleteTypes/enumAsArgument/typestat.json b/test/cases/fixes/incompleteTypes/enumAsArgument/typestat.json new file mode 100644 index 000000000..1406ae0fa --- /dev/null +++ b/test/cases/fixes/incompleteTypes/enumAsArgument/typestat.json @@ -0,0 +1,5 @@ +{ + "fixes": { + "incompleteTypes": true + } +} diff --git a/test/cases/fixes/incompleteTypes/implicitGenerics/incompleteImplicitClassGenerics/expected.ts b/test/cases/fixes/incompleteTypes/implicitGenerics/incompleteImplicitClassGenerics/expected.ts index bff2b260b..fb4bc09a1 100644 --- a/test/cases/fixes/incompleteTypes/implicitGenerics/incompleteImplicitClassGenerics/expected.ts +++ b/test/cases/fixes/incompleteTypes/implicitGenerics/incompleteImplicitClassGenerics/expected.ts @@ -5,7 +5,7 @@ import { ComponentLike } from "./react-like"; class BaseWithoutGenerics {} class BaseWithOneGeneric { - constructor(t: T | OneInterface | OneType | string) {} + constructor(t: T | string | OneInterface | OneType) {} } class BaseWithTwoGenerics { constructor(t: T | number, u: U | boolean) {} diff --git a/test/fixEnumAsArgument.test.ts b/test/fixEnumAsArgument.test.ts new file mode 100644 index 000000000..3f59a3480 --- /dev/null +++ b/test/fixEnumAsArgument.test.ts @@ -0,0 +1,15 @@ +import path from "node:path"; +import { expect, test } from "vitest"; + +import { runMutationTest } from "../src/tests/testSetup.js"; + +test("enum as argument", async () => { + const caseDir = path.join( + import.meta.dirname, + "cases/fixes/incompleteTypes/enumAsArgument", + ); + const { actualContent, expectedFilePath, options } = + await runMutationTest(caseDir); + await expect(actualContent).toMatchFileSnapshot(expectedFilePath); + expect(options).toMatchSnapshot("options"); +}, 10000);