ts-codemod-lib 3.1.1 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/dist/entry-point.mjs +1 -0
- package/dist/entry-point.mjs.map +1 -1
- package/dist/functions/ast-transformers/append-as-const.d.mts +16 -0
- package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
- package/dist/functions/ast-transformers/append-as-const.mjs +101 -0
- package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts +65 -0
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts.map +1 -0
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs +155 -0
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs.map +1 -0
- package/dist/functions/ast-transformers/index.d.mts +1 -0
- package/dist/functions/ast-transformers/index.d.mts.map +1 -1
- package/dist/functions/ast-transformers/index.mjs +1 -0
- package/dist/functions/ast-transformers/index.mjs.map +1 -1
- package/dist/functions/index.mjs +1 -0
- package/dist/functions/index.mjs.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/functions/ast-transformers/append-as-const.mts +251 -2
- package/src/functions/ast-transformers/append-as-const.test.mts +173 -1
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.mts +438 -0
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.test.mts +565 -0
- package/src/functions/ast-transformers/index.mts +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enable-no-unchecked-indexed-access.mjs","sources":["../../../src/functions/ast-transformers/enable-no-unchecked-indexed-access.mts"],"sourcesContent":["import { Arr } from 'ts-data-forge';\nimport * as tsm from 'ts-morph';\nimport { type DeepReadonly } from 'ts-type-forge';\nimport { hasDisableNextLineComment } from '../functions/index.mjs';\nimport { type TsMorphTransformer } from './types.mjs';\n\nconst TRANSFORMER_NAME = 'enable-no-unchecked-indexed-access';\n\n/**\n * Appends `!` to the index accesses that turning on the\n * [`noUncheckedIndexedAccess`](https://www.typescriptlang.org/tsconfig/#noUncheckedIndexedAccess)\n * compiler option would turn into type errors, as a stopgap while the option\n * is being enabled.\n *\n * The rewrite is driven by the type checker rather than by syntax: the file is\n * checked twice, once with `noUncheckedIndexedAccess` off and once with it on,\n * and `!` is appended only where the option is what added `undefined` to the\n * expression's type. Consequently an index whose presence the type already\n * guarantees keeps its access untouched — `[T, T][1]`, `[T, T, ...T[]][0]`,\n * and `({ a: T })['a']` are all left as they are, as is an element type that\n * contained `undefined` to begin with.\n *\n * Both spellings of an index read are covered, because the option widens both:\n * `rec['a']` and the dotted `rec.a` of a type carrying an index signature.\n *\n * `strictNullChecks` is turned on for both checks, because\n * `noUncheckedIndexedAccess` does nothing without it and the project the\n * transformer is handed may not have it on.\n *\n * The result is a stopgap, not a fix, and wants reviewing. A file transformed\n * on its own — which is how {@link transformSourceCode} drives every\n * transformer here — resolves neither a type from another module nor a\n * narrowing that depends on an imported type guard, and the latter produces\n * assertions a whole-program check does not need. Most of those are reported\n * by `@typescript-eslint/no-unnecessary-type-assertion`, whose fixer removes\n * them; what no rule can see is an assertion that makes a later check dead,\n * as the `if` of `const v = xs[0]!; if (v === undefined) …` now is.\n *\n * @example\n * ```ts\n * declare const xs: readonly number[];\n * declare const pair: readonly [number, number];\n * declare const rec: Record<string, number>;\n * declare const known: { a: number };\n *\n * xs[0].toFixed(); // -> xs[0]!.toFixed();\n * rec['a'].toFixed(); // -> rec['a']!.toFixed();\n * rec.a.toFixed(); // -> rec.a!.toFixed();\n * pair[1].toFixed(); // -> pair[1].toFixed(); (guaranteed to exist)\n * known.a.toFixed(); // -> known.a.toFixed(); (guaranteed to exist)\n * ```\n */\nexport const enableNoUncheckedIndexedAccessTransformer = (\n options?: EnableNoUncheckedIndexedAccessTransformerOptions,\n): TsMorphTransformer => {\n const optionsInternal: EnableNoUncheckedIndexedAccessTransformerOptionsInternal =\n {\n applyLevel: options?.applyLevel ?? 'all',\n debugPrint: options?.debug === true ? console.debug : () => {},\n } as const;\n\n return {\n name: TRANSFORMER_NAME,\n transform: (sourceAst) => {\n const positions = collectAssertionPositions(sourceAst, optionsInternal);\n\n if (Arr.isEmpty(positions)) {\n return;\n }\n\n // One text edit for the whole file: `insertText` forgets every\n // descendant node, so inserting one `!` at a time would reparse the\n // file once per assertion.\n sourceAst.replaceText(\n [0, sourceAst.getEnd()],\n insertNonNullAssertions(sourceAst.getFullText(), positions),\n );\n },\n };\n};\n\nexport type EnableNoUncheckedIndexedAccessTransformerOptions = DeepReadonly<{\n /**\n * How eagerly `!` is appended.\n *\n * - `\"all\"`: append it to every read whose type `noUncheckedIndexedAccess`\n * widens with `undefined`, so that as few type errors as possible remain.\n * - `\"avoidWhereUndefinedIsAllowed\"`: additionally leave the access alone\n * where its contextual type already accepts `undefined` (e.g.\n * `const x: number | undefined = xs[0];` or an argument of a\n * `(x: number | undefined) => void` parameter), which produces fewer but\n * only load-bearing assertions.\n *\n * @default \"all\"\n */\n applyLevel?: 'all' | 'avoidWhereUndefinedIsAllowed';\n\n debug?: boolean;\n}>;\n\ntype EnableNoUncheckedIndexedAccessTransformerOptionsInternal = DeepReadonly<{\n applyLevel: 'all' | 'avoidWhereUndefinedIsAllowed';\n debugPrint: (...args: readonly unknown[]) => void;\n}>;\n\n/**\n * An index read that `noUncheckedIndexedAccess` can widen: an element access,\n * and the dotted spelling of the same thing on a type with an index signature\n * (`rec.a`, which the option widens exactly as it widens `rec['a']`).\n */\ntype IndexReadExpression =\n tsm.ElementAccessExpression | tsm.PropertyAccessExpression;\n\n/**\n * Returns the positions at which a `!` has to be inserted, i.e. the end\n * offsets of the reads whose type `noUncheckedIndexedAccess` widens with\n * `undefined`.\n */\nconst collectAssertionPositions = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n sourceAst: tsm.SourceFile,\n options: EnableNoUncheckedIndexedAccessTransformerOptionsInternal,\n): readonly number[] => {\n const candidates: readonly IndexReadExpression[] = [\n ...sourceAst.getDescendantsOfKind(tsm.SyntaxKind.ElementAccessExpression),\n ...sourceAst.getDescendantsOfKind(tsm.SyntaxKind.PropertyAccessExpression),\n ].filter(\n (node) => isAssertableReadPosition(node) && !isIgnoredByComment(node),\n );\n\n if (Arr.isEmpty(candidates)) {\n return [];\n }\n\n const project = sourceAst.getProject();\n\n const originalOptions = project.getCompilerOptions();\n\n try {\n // With the option on first, because that narrows the candidates down to\n // the few reads worth checking a second time. `strictNullChecks` comes\n // with it: `noUncheckedIndexedAccess` does nothing without it, and the\n // project handed to the transformer may not have it on.\n project.compilerOptions.set({\n strictNullChecks: true,\n noUncheckedIndexedAccess: true,\n });\n\n const widened = candidates.filter(\n (node) =>\n typeIncludesUndefined(node.getType()) &&\n !(\n options.applyLevel === 'avoidWhereUndefinedIsAllowed' &&\n contextAcceptsUndefined(node)\n ),\n );\n\n if (Arr.isEmpty(widened)) {\n return [];\n }\n\n project.compilerOptions.set({ noUncheckedIndexedAccess: false });\n\n // What is possibly `undefined` without the option too is not the option's\n // doing — the element type says so, or the expression is erroneous (an\n // unresolved import resolves to `any`, which is never widened).\n return widened\n .filter((node) => !typeIncludesUndefined(node.getType()))\n .map((node) => {\n options.debugPrint(`${node.getText()} -> ${node.getText()}!`);\n\n return node.getEnd();\n });\n } finally {\n // `set` merges, so the two options overridden above have to be named\n // explicitly: they are absent from `originalOptions` when the project did\n // not carry them, and would otherwise stay overridden for whichever\n // transformer runs next.\n project.compilerOptions.set({\n ...originalOptions,\n strictNullChecks: originalOptions.strictNullChecks,\n noUncheckedIndexedAccess: originalOptions.noUncheckedIndexedAccess,\n });\n }\n};\n\nconst insertNonNullAssertions = (\n fullText: string,\n positions: readonly number[],\n): string => {\n const mut_parts: string[] = [];\n\n let mut_restStart: number = 0;\n\n for (const position of positions.toSorted((a, b) => a - b)) {\n mut_parts.push(fullText.slice(mut_restStart, position), '!');\n\n mut_restStart = position;\n }\n\n mut_parts.push(fullText.slice(mut_restStart));\n\n return mut_parts.join('');\n};\n\nconst typeIncludesUndefined = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n type: tsm.Type,\n): boolean =>\n type.isUndefined() ||\n (type.isUnion() && type.getUnionTypes().some((t) => t.isUndefined()));\n\n/**\n * Whether the position the access sits in already accepts `undefined`, in\n * which case `noUncheckedIndexedAccess` reports nothing there and the\n * assertion would only be noise.\n */\nconst contextAcceptsUndefined = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n node: IndexReadExpression,\n): boolean => {\n const contextualType = node.getContextualType();\n\n return (\n contextualType !== undefined &&\n (contextualType.isAny() ||\n contextualType.isUnknown() ||\n typeIncludesUndefined(contextualType))\n );\n};\n\nconst isIgnoredByComment = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n node: IndexReadExpression,\n): boolean => {\n if (hasDisableNextLineComment(node, TRANSFORMER_NAME)) {\n return true;\n }\n\n // An index access is rarely the first thing on its own line, so the comment\n // that covers it is the one above the statement it appears in.\n const statement = node.getFirstAncestor((ancestor) =>\n tsm.Node.isStatement(ancestor),\n );\n\n return (\n statement !== undefined &&\n hasDisableNextLineComment(statement, TRANSFORMER_NAME)\n );\n};\n\n/**\n * Whether a `!` appended to this access would be both syntactically valid and\n * meaningful.\n *\n * Note that most write positions need no rule of their own: TypeScript gives\n * the target of an assignment the element type without `undefined`, so the\n * type comparison in {@link collectAssertionPositions} already passes them\n * over. The exceptions handled here are the ones it does widen.\n */\nconst isAssertableReadPosition = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n node: IndexReadExpression,\n): boolean => {\n const parent = node.getParent();\n\n if (parent === undefined) {\n return false;\n }\n\n return (\n !isAlreadyAssertedOrGuarded(node, parent) &&\n !isWriteTarget(node, parent) &&\n !isTestedForAbsence(node, parent)\n );\n};\n\n/** `xs[0]!`, `xs[0] as T`, `<T>xs[0]`, `xs[0]?.foo`, `xs[0]?.()`, `xs[0]?.[1]`. */\nconst isAlreadyAssertedOrGuarded = (\n node: tsm.Node,\n parent: tsm.Node,\n): boolean => {\n if (\n parent.isKind(tsm.SyntaxKind.NonNullExpression) ||\n parent.isKind(tsm.SyntaxKind.AsExpression) ||\n parent.isKind(tsm.SyntaxKind.TypeAssertionExpression)\n ) {\n return true;\n }\n\n if (isInOptionalChain(node)) {\n return true;\n }\n\n // Only the object of the optional chain is guarded; an index argument such\n // as the `xs[0]` of `m?.[xs[0]]` is not.\n return (\n (parent.isKind(tsm.SyntaxKind.PropertyAccessExpression) ||\n parent.isKind(tsm.SyntaxKind.ElementAccessExpression) ||\n parent.isKind(tsm.SyntaxKind.CallExpression)) &&\n parent.hasQuestionDotToken() &&\n parent.getExpression() === node\n );\n};\n\n/**\n * Whether the read sits in an optional chain that can short-circuit before it,\n * as the `xs[0]?.toFixed` of `xs[0]?.toFixed()` does. `undefined` in such a\n * type comes from the chain rather than from the index, and the chain already\n * accounts for it; asserting there would assert away the author's own `?.`.\n *\n * Parentheses end a chain (`(a?.b).c` is not one), which is why the walk stops\n * at anything that is not a chain link.\n */\nconst isInOptionalChain = (node: tsm.Node): boolean => {\n if (\n node.isKind(tsm.SyntaxKind.PropertyAccessExpression) ||\n node.isKind(tsm.SyntaxKind.ElementAccessExpression) ||\n node.isKind(tsm.SyntaxKind.CallExpression)\n ) {\n return (\n node.hasQuestionDotToken() || isInOptionalChain(node.getExpression())\n );\n }\n\n return (\n node.isKind(tsm.SyntaxKind.NonNullExpression) &&\n isInOptionalChain(node.getExpression())\n );\n};\n\n/**\n * Positions that write the value rather than read it, where `!` cannot be\n * appended at all: an assignment target is not an expression (`xs[0]! = 1`\n * does not parse as an assignment), and `delete xs[0]!` and `xs[0]!++` are\n * equally invalid.\n *\n * `xs[0] += 1` is the one case this leaves a type error behind: its left-hand\n * side is read as well as written, so `noUncheckedIndexedAccess` reports it,\n * but no assertion can be spelled there.\n */\nconst isWriteTarget = (node: tsm.Node, parent: tsm.Node): boolean => {\n if (\n parent.isKind(tsm.SyntaxKind.DeleteExpression) ||\n parent.isKind(tsm.SyntaxKind.PostfixUnaryExpression)\n ) {\n return true;\n }\n\n if (parent.isKind(tsm.SyntaxKind.PrefixUnaryExpression)) {\n const operator = parent.getOperatorToken();\n\n return (\n operator === tsm.SyntaxKind.PlusPlusToken ||\n operator === tsm.SyntaxKind.MinusMinusToken\n );\n }\n\n if (\n parent.isKind(tsm.SyntaxKind.ForOfStatement) ||\n parent.isKind(tsm.SyntaxKind.ForInStatement)\n ) {\n return parent.getInitializer() === node;\n }\n\n return (\n parent.isKind(tsm.SyntaxKind.BinaryExpression) &&\n parent.getLeft() === node &&\n isAssignmentOperatorKind(parent.getOperatorToken().getKind())\n );\n};\n\n/**\n * Positions that read the access precisely to find out whether the value is\n * there — a truthiness check, an equality comparison, `??`, `typeof`. None of\n * them is an error under `noUncheckedIndexedAccess`, and asserting inside one\n * would defeat the check it performs (`case undefined:` would stop compiling\n * against an asserted `switch` subject).\n */\nconst isTestedForAbsence = (node: tsm.Node, parent: tsm.Node): boolean => {\n if (parent.isKind(tsm.SyntaxKind.TypeOfExpression)) {\n return true;\n }\n\n if (parent.isKind(tsm.SyntaxKind.PrefixUnaryExpression)) {\n return parent.getOperatorToken() === tsm.SyntaxKind.ExclamationToken;\n }\n\n if (parent.isKind(tsm.SyntaxKind.BinaryExpression)) {\n const operator = parent.getOperatorToken().getKind();\n\n return (\n equalityOperatorKinds.includes(operator) ||\n (parent.getLeft() === node &&\n shortCircuitOperatorKinds.includes(operator))\n );\n }\n\n if (\n parent.isKind(tsm.SyntaxKind.IfStatement) ||\n parent.isKind(tsm.SyntaxKind.WhileStatement) ||\n parent.isKind(tsm.SyntaxKind.DoStatement) ||\n parent.isKind(tsm.SyntaxKind.SwitchStatement)\n ) {\n return parent.getExpression() === node;\n }\n\n if (parent.isKind(tsm.SyntaxKind.ForStatement)) {\n return parent.getCondition() === node;\n }\n\n return (\n parent.isKind(tsm.SyntaxKind.ConditionalExpression) &&\n parent.getCondition() === node\n );\n};\n\n/**\n * `=` through `^=`, i.e. every assignment operator. TypeScript keeps them in\n * one contiguous block of {@link tsm.SyntaxKind} and marks its ends, which is\n * how the compiler itself recognizes them.\n */\nconst isAssignmentOperatorKind = (kind: tsm.SyntaxKind): boolean =>\n tsm.SyntaxKind.FirstAssignment <= kind &&\n kind <= tsm.SyntaxKind.LastAssignment;\n\nconst equalityOperatorKinds: readonly tsm.SyntaxKind[] = [\n tsm.SyntaxKind.EqualsEqualsToken,\n tsm.SyntaxKind.EqualsEqualsEqualsToken,\n tsm.SyntaxKind.ExclamationEqualsToken,\n tsm.SyntaxKind.ExclamationEqualsEqualsToken,\n] as const;\n\nconst shortCircuitOperatorKinds: readonly tsm.SyntaxKind[] = [\n tsm.SyntaxKind.QuestionQuestionToken,\n tsm.SyntaxKind.AmpersandAmpersandToken,\n tsm.SyntaxKind.BarBarToken,\n] as const;\n"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './append-as-const.mjs';
|
|
2
2
|
export * from './convert-interface-to-type.mjs';
|
|
3
3
|
export * from './convert-to-readonly.mjs';
|
|
4
|
+
export * from './enable-no-unchecked-indexed-access.mjs';
|
|
4
5
|
export * from './readonly-transformer-helpers/index.mjs';
|
|
5
6
|
export * from './replace-any-with-unknown.mjs';
|
|
6
7
|
export * from './replace-record-with-unknown-record.mjs';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../../../src/functions/ast-transformers/index.mts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,iCAAiC,CAAC;AAChD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0CAA0C,CAAC;AACzD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0CAA0C,CAAC;AACzD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../../../src/functions/ast-transformers/index.mts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,iCAAiC,CAAC;AAChD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0CAA0C,CAAC;AACzD,cAAc,0CAA0C,CAAC;AACzD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0CAA0C,CAAC;AACzD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,aAAa,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { appendAsConstTransformer } from './append-as-const.mjs';
|
|
2
2
|
export { convertInterfaceToTypeTransformer } from './convert-interface-to-type.mjs';
|
|
3
3
|
export { convertToReadonlyTransformer } from './convert-to-readonly.mjs';
|
|
4
|
+
export { enableNoUncheckedIndexedAccessTransformer } from './enable-no-unchecked-indexed-access.mjs';
|
|
4
5
|
export { compareUnionIntersectionTypes } from './readonly-transformer-helpers/compare-union-types.mjs';
|
|
5
6
|
export { invalidDeepReadonlyTypeName } from './readonly-transformer-helpers/constants.mjs';
|
|
6
7
|
export { groupUnionIntersectionTypes } from './readonly-transformer-helpers/group-union-types.mjs';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
|
package/dist/functions/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { appendAsConstTransformer } from './ast-transformers/append-as-const.mjs';
|
|
2
2
|
export { convertInterfaceToTypeTransformer } from './ast-transformers/convert-interface-to-type.mjs';
|
|
3
3
|
export { convertToReadonlyTransformer } from './ast-transformers/convert-to-readonly.mjs';
|
|
4
|
+
export { enableNoUncheckedIndexedAccessTransformer } from './ast-transformers/enable-no-unchecked-indexed-access.mjs';
|
|
4
5
|
export { compareUnionIntersectionTypes } from './ast-transformers/readonly-transformer-helpers/compare-union-types.mjs';
|
|
5
6
|
export { invalidDeepReadonlyTypeName } from './ast-transformers/readonly-transformer-helpers/constants.mjs';
|
|
6
7
|
export { groupUnionIntersectionTypes } from './ast-transformers/readonly-transformer-helpers/group-union-types.mjs';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { appendAsConstTransformer } from './functions/ast-transformers/append-as-const.mjs';
|
|
2
2
|
export { convertInterfaceToTypeTransformer } from './functions/ast-transformers/convert-interface-to-type.mjs';
|
|
3
3
|
export { convertToReadonlyTransformer } from './functions/ast-transformers/convert-to-readonly.mjs';
|
|
4
|
+
export { enableNoUncheckedIndexedAccessTransformer } from './functions/ast-transformers/enable-no-unchecked-indexed-access.mjs';
|
|
4
5
|
export { compareUnionIntersectionTypes } from './functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs';
|
|
5
6
|
export { invalidDeepReadonlyTypeName } from './functions/ast-transformers/readonly-transformer-helpers/constants.mjs';
|
|
6
7
|
export { groupUnionIntersectionTypes } from './functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs';
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-codemod-lib",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"ts-type-forge": "^9.2.2"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@types/node": "26.
|
|
46
|
+
"@types/node": "26.4.0",
|
|
47
47
|
"@vitest/browser-playwright": "4.1.11",
|
|
48
48
|
"@vitest/coverage-v8": "4.1.11",
|
|
49
49
|
"@vitest/ui": "4.1.11",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"dedent": "1.7.2",
|
|
52
52
|
"esbuild": "0.28.2",
|
|
53
53
|
"eslint": "10.8.1",
|
|
54
|
-
"eslint-config-typed": "5.
|
|
54
|
+
"eslint-config-typed": "5.10.0",
|
|
55
55
|
"eslint-plugin-ts-data-forge": "0.5.0",
|
|
56
56
|
"eslint-plugin-ts-fortress": "1.0.4",
|
|
57
57
|
"eslint-plugin-ts-type-forge": "0.7.0",
|
|
@@ -20,6 +20,8 @@ export const appendAsConstTransformer = (
|
|
|
20
20
|
const optionsInternal: AppendAsConstTransformerOptionsInternal = {
|
|
21
21
|
applyLevel: options?.applyLevel ?? 'avoidInFunctionArgs',
|
|
22
22
|
ignoredPrefixes: ignorePrefixes,
|
|
23
|
+
removeAsConstForConstTypeParameters:
|
|
24
|
+
options?.removeAsConstForConstTypeParameters ?? true,
|
|
23
25
|
|
|
24
26
|
debugPrint: options?.debug === true ? console.debug : () => {},
|
|
25
27
|
replaceNode:
|
|
@@ -43,6 +45,10 @@ export const appendAsConstTransformer = (
|
|
|
43
45
|
optionsInternal,
|
|
44
46
|
);
|
|
45
47
|
}
|
|
48
|
+
|
|
49
|
+
if (optionsInternal.removeAsConstForConstTypeParameters) {
|
|
50
|
+
removeAsConstInConstTypeParameterArgs(sourceAst, optionsInternal);
|
|
51
|
+
}
|
|
46
52
|
},
|
|
47
53
|
};
|
|
48
54
|
};
|
|
@@ -62,8 +68,22 @@ export type AppendAsConstTransformerOptions = DeepReadonly<{
|
|
|
62
68
|
*/
|
|
63
69
|
ignorePrefixes?: string[];
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Whether to remove redundant `as const` assertions from call arguments
|
|
73
|
+
* whose corresponding parameter type is exactly a `const`-modified type
|
|
74
|
+
* parameter.
|
|
75
|
+
*
|
|
76
|
+
* (e.g. `f([1, 2] as const)` becomes `f([1, 2])` for
|
|
77
|
+
* `function f<const T>(x: T): T`, because the `const` type parameter
|
|
78
|
+
* already makes TypeScript infer the argument as if it were annotated
|
|
79
|
+
* with `as const`.)
|
|
80
|
+
*
|
|
81
|
+
* Only applies when the callee resolves to a single call signature within
|
|
82
|
+
* the transformed file itself; imported callees are left as they are.
|
|
83
|
+
*
|
|
84
|
+
* @default true
|
|
85
|
+
*/
|
|
86
|
+
removeAsConstForConstTypeParameters?: boolean;
|
|
67
87
|
|
|
68
88
|
debug?: boolean;
|
|
69
89
|
}>;
|
|
@@ -71,6 +91,7 @@ export type AppendAsConstTransformerOptions = DeepReadonly<{
|
|
|
71
91
|
type AppendAsConstTransformerOptionsInternal = DeepReadonly<{
|
|
72
92
|
applyLevel: 'all' | 'avoidInFunctionArgs';
|
|
73
93
|
ignoredPrefixes: ISet<string>;
|
|
94
|
+
removeAsConstForConstTypeParameters: boolean;
|
|
74
95
|
|
|
75
96
|
debugPrint: (...args: readonly unknown[]) => void;
|
|
76
97
|
replaceNode: (node: tsm.Node, newNodeText: string) => void;
|
|
@@ -364,3 +385,231 @@ const transformNode = (
|
|
|
364
385
|
// return;
|
|
365
386
|
}
|
|
366
387
|
};
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Removes redundant `as const` assertions from call arguments whose
|
|
391
|
+
* corresponding parameter type is exactly a `const`-modified type parameter
|
|
392
|
+
* (e.g. `f([1, 2] as const)` for `function f<const T>(x: T): T`).
|
|
393
|
+
*
|
|
394
|
+
* A `const` type parameter makes TypeScript treat every inference candidate
|
|
395
|
+
* as if it were annotated with `as const`, so when the parameter type is the
|
|
396
|
+
* bare type parameter itself, removing the assertion cannot change the
|
|
397
|
+
* inferred type. Conservative bail-outs (the `as const` is kept) when:
|
|
398
|
+
*
|
|
399
|
+
* - the callee does not resolve to exactly one call signature (unresolved
|
|
400
|
+
* imports resolve to none, since each file is transformed in an isolated
|
|
401
|
+
* single-file project; overloaded functions resolve to several),
|
|
402
|
+
* - the call has explicit type arguments (inference is not involved),
|
|
403
|
+
* - the argument is a spread argument or follows one (its parameter position
|
|
404
|
+
* is not statically known),
|
|
405
|
+
* - the parameter type is not the bare type parameter (e.g. `readonly T[]`).
|
|
406
|
+
*/
|
|
407
|
+
const removeAsConstInConstTypeParameterArgs = (
|
|
408
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
409
|
+
sourceAst: tsm.SourceFile,
|
|
410
|
+
options: AppendAsConstTransformerOptionsInternal,
|
|
411
|
+
): void => {
|
|
412
|
+
// Reversed so that calls nested inside another call's arguments are
|
|
413
|
+
// processed before the outer call replaces (and thereby forgets) them.
|
|
414
|
+
for (const call of sourceAst
|
|
415
|
+
.getDescendantsOfKind(tsm.SyntaxKind.CallExpression)
|
|
416
|
+
.toReversed()) {
|
|
417
|
+
if (call.wasForgotten()) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const args = call.getArguments();
|
|
422
|
+
|
|
423
|
+
// Cheap syntactic pre-check before touching the type checker
|
|
424
|
+
if (!args.some(isAsConstNode)) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (hasDisableNextLineComment(call, TRANSFORMER_NAME)) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const variableStatement = call.getFirstAncestorByKind(
|
|
433
|
+
tsm.SyntaxKind.VariableStatement,
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
if (
|
|
437
|
+
variableStatement !== undefined &&
|
|
438
|
+
hasDisableNextLineComment(variableStatement, TRANSFORMER_NAME)
|
|
439
|
+
) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// With explicit type arguments no inference happens, so the `as const`
|
|
444
|
+
// may be load-bearing for assignability.
|
|
445
|
+
if (!Arr.isEmpty(call.getTypeArguments())) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const callTarget = resolveSingleSignatureCallTarget(call);
|
|
450
|
+
|
|
451
|
+
if (callTarget === undefined) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
removeAsConstArgsOfCall(args, callTarget, options);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const removeAsConstArgsOfCall = (
|
|
460
|
+
args: readonly tsm.Node[],
|
|
461
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
462
|
+
callTarget: ResolvedCallTarget,
|
|
463
|
+
options: AppendAsConstTransformerOptionsInternal,
|
|
464
|
+
): void => {
|
|
465
|
+
// Argument positions at or after a spread argument cannot be mapped to a
|
|
466
|
+
// parameter statically.
|
|
467
|
+
const firstSpreadIndex = args.findIndex((a) =>
|
|
468
|
+
a.isKind(tsm.SyntaxKind.SpreadElement),
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const mappableArgCount =
|
|
472
|
+
firstSpreadIndex === -1 ? args.length : firstSpreadIndex;
|
|
473
|
+
|
|
474
|
+
for (const [argumentIndex, argument] of args.entries()) {
|
|
475
|
+
if (argumentIndex >= mappableArgCount) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (argument.wasForgotten() || !isAsConstNode(argument)) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (hasDisableNextLineComment(argument, TRANSFORMER_NAME)) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const parameter = getMatchedParameter(callTarget.parameters, argumentIndex);
|
|
488
|
+
|
|
489
|
+
if (
|
|
490
|
+
parameter === undefined ||
|
|
491
|
+
!isBareConstTypeParameterReference(
|
|
492
|
+
parameter,
|
|
493
|
+
callTarget.constTypeParameterNames,
|
|
494
|
+
)
|
|
495
|
+
) {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
options.debugPrint(
|
|
500
|
+
'removing redundant as const in const type parameter argument',
|
|
501
|
+
argument.getText(),
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
options.replaceNode(argument, argument.getExpression().getText());
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
type ResolvedCallTarget = Readonly<{
|
|
509
|
+
parameters: readonly tsm.ParameterDeclaration[];
|
|
510
|
+
constTypeParameterNames: ISet<string>;
|
|
511
|
+
}>;
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Resolves the callee of a call expression to the declaration of its single
|
|
515
|
+
* call signature, and collects the names of that declaration's
|
|
516
|
+
* `const`-modified type parameters.
|
|
517
|
+
*
|
|
518
|
+
* Returns `undefined` when the callee does not resolve to exactly one call
|
|
519
|
+
* signature, the resolved declaration has no parameters or type parameters,
|
|
520
|
+
* or none of the type parameters carry the `const` modifier.
|
|
521
|
+
*/
|
|
522
|
+
const resolveSingleSignatureCallTarget = (
|
|
523
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
524
|
+
call: tsm.CallExpression,
|
|
525
|
+
): ResolvedCallTarget | undefined => {
|
|
526
|
+
// 0 signatures: the callee is unresolved (e.g. imported, since each file
|
|
527
|
+
// is transformed in an isolated single-file project).
|
|
528
|
+
// 2+ signatures: overloads, where removing `as const` could change which
|
|
529
|
+
// overload is selected.
|
|
530
|
+
if (
|
|
531
|
+
!Arr.isFixedLengthArray(
|
|
532
|
+
1,
|
|
533
|
+
call.getExpression().getType().getCallSignatures(),
|
|
534
|
+
)
|
|
535
|
+
) {
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const declaration = call
|
|
540
|
+
.getProject()
|
|
541
|
+
.getTypeChecker()
|
|
542
|
+
.getResolvedSignature(call)
|
|
543
|
+
?.getDeclaration();
|
|
544
|
+
|
|
545
|
+
if (
|
|
546
|
+
declaration === undefined ||
|
|
547
|
+
!tsm.Node.isParametered(declaration) ||
|
|
548
|
+
!tsm.Node.isTypeParametered(declaration)
|
|
549
|
+
) {
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Only type parameters declared on the call's own signature are inferred
|
|
554
|
+
// at the call site; a `const` type parameter of an enclosing declaration
|
|
555
|
+
// (e.g. a generic class containing this method) is already fixed there.
|
|
556
|
+
const constTypeParameterNames = ISet.create(
|
|
557
|
+
declaration
|
|
558
|
+
.getTypeParameters()
|
|
559
|
+
.filter((tp) => tp.hasModifier(tsm.SyntaxKind.ConstKeyword))
|
|
560
|
+
.map((tp) => tp.getName()),
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
if (constTypeParameterNames.size === 0) {
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
parameters: declaration.getParameters(),
|
|
569
|
+
constTypeParameterNames,
|
|
570
|
+
} as const;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const getMatchedParameter = (
|
|
574
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
575
|
+
parameters: readonly tsm.ParameterDeclaration[],
|
|
576
|
+
argumentIndex: number,
|
|
577
|
+
): tsm.ParameterDeclaration | undefined => {
|
|
578
|
+
if (argumentIndex < parameters.length) {
|
|
579
|
+
return parameters[argumentIndex];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const lastParameter = parameters.at(-1);
|
|
583
|
+
|
|
584
|
+
return lastParameter?.isRestParameter() === true ? lastParameter : undefined;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Whether the parameter's declared type is exactly a bare reference to one of
|
|
589
|
+
* the given `const`-modified type parameters (i.e. `x: T`, not `x: readonly
|
|
590
|
+
* T[]` or `x: Wrapper<T>` — there the argument expression's own type is still
|
|
591
|
+
* observable, so the `as const` is kept).
|
|
592
|
+
*/
|
|
593
|
+
const isBareConstTypeParameterReference = (
|
|
594
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
595
|
+
parameter: tsm.ParameterDeclaration,
|
|
596
|
+
constTypeParameterNames: ISet<string>,
|
|
597
|
+
): boolean => {
|
|
598
|
+
const typeNode = parameter.getTypeNode();
|
|
599
|
+
|
|
600
|
+
if (typeNode === undefined) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (!typeNode.isKind(tsm.SyntaxKind.TypeReference)) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const typeName = typeNode.getTypeName();
|
|
609
|
+
|
|
610
|
+
return (
|
|
611
|
+
typeName.isKind(tsm.SyntaxKind.Identifier) &&
|
|
612
|
+
Arr.isEmpty(typeNode.getTypeArguments()) &&
|
|
613
|
+
constTypeParameterNames.has(typeName.getText())
|
|
614
|
+
);
|
|
615
|
+
};
|
|
@@ -11,10 +11,12 @@ import { transformSourceCode } from './transform-source-code.mjs';
|
|
|
11
11
|
const testFn = async ({
|
|
12
12
|
source,
|
|
13
13
|
expected,
|
|
14
|
+
options,
|
|
14
15
|
debug,
|
|
15
16
|
}: Readonly<{
|
|
16
17
|
source: string;
|
|
17
18
|
expected: string;
|
|
19
|
+
options?: Parameters<typeof appendAsConstTransformer>[0];
|
|
18
20
|
debug?: boolean;
|
|
19
21
|
}>): Promise<void> => {
|
|
20
22
|
if (debug !== true) {
|
|
@@ -26,7 +28,9 @@ const testFn = async ({
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
const transformed = await formatter(
|
|
29
|
-
transformSourceCode(source, false, [
|
|
31
|
+
transformSourceCode(source, false, [
|
|
32
|
+
appendAsConstTransformer({ ...options, debug }),
|
|
33
|
+
]),
|
|
30
34
|
);
|
|
31
35
|
|
|
32
36
|
const expectedFormatted = await formatter(expected);
|
|
@@ -698,6 +702,174 @@ describe(appendAsConstTransformer, () => {
|
|
|
698
702
|
])('$name', testFn);
|
|
699
703
|
});
|
|
700
704
|
|
|
705
|
+
describe('Redundant as const in const type parameter arguments', () => {
|
|
706
|
+
test.each([
|
|
707
|
+
{
|
|
708
|
+
name: 'const type parameter (function declaration) should remove as const',
|
|
709
|
+
source: dedent`
|
|
710
|
+
function f<const T>(x: T): T { return x; }
|
|
711
|
+
const a = f([1, 2] as const);
|
|
712
|
+
`,
|
|
713
|
+
expected: dedent`
|
|
714
|
+
function f<const T>(x: T): T { return x; }
|
|
715
|
+
const a = f([1, 2]);
|
|
716
|
+
`,
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
name: 'const type parameter (expression statement call) should remove as const',
|
|
720
|
+
source: dedent`
|
|
721
|
+
function f<const T>(x: T): T { return x; }
|
|
722
|
+
f({ x: 1 } as const);
|
|
723
|
+
`,
|
|
724
|
+
expected: dedent`
|
|
725
|
+
function f<const T>(x: T): T { return x; }
|
|
726
|
+
f({ x: 1 });
|
|
727
|
+
`,
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
name: 'const type parameter (arrow function) should remove as const',
|
|
731
|
+
source: dedent`
|
|
732
|
+
const f = <const S,>(x: S): S => x;
|
|
733
|
+
const a = f([1, 2] as const);
|
|
734
|
+
`,
|
|
735
|
+
expected: dedent`
|
|
736
|
+
const f = <const S,>(x: S): S => x;
|
|
737
|
+
const a = f([1, 2]);
|
|
738
|
+
`,
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
name: 'const type parameter (object literal method) should remove as const',
|
|
742
|
+
source: dedent`
|
|
743
|
+
const Obj = { m: <const U,>(x: U): U => x } as const;
|
|
744
|
+
const a = Obj.m([1] as const);
|
|
745
|
+
`,
|
|
746
|
+
expected: dedent`
|
|
747
|
+
const Obj = { m: <const U,>(x: U): U => x } as const;
|
|
748
|
+
const a = Obj.m([1]);
|
|
749
|
+
`,
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
name: 'const rest type parameter should remove as const',
|
|
753
|
+
source: dedent`
|
|
754
|
+
function rest<const T extends readonly unknown[]>(...xs: T): T { return xs; }
|
|
755
|
+
const r = rest(1 as const, [2, 3] as const);
|
|
756
|
+
`,
|
|
757
|
+
expected: dedent`
|
|
758
|
+
function rest<const T extends readonly unknown[]>(...xs: T): T { return xs; }
|
|
759
|
+
const r = rest(1, [2, 3]);
|
|
760
|
+
`,
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
name: 'mixed const and non-const type parameters should only remove for const ones',
|
|
764
|
+
source: dedent`
|
|
765
|
+
function h<const A, B>(a: A, b: B): A { return a; }
|
|
766
|
+
const m = h([1] as const, [2] as const);
|
|
767
|
+
`,
|
|
768
|
+
expected: dedent`
|
|
769
|
+
function h<const A, B>(a: A, b: B): A { return a; }
|
|
770
|
+
const m = h([1], [2] as const);
|
|
771
|
+
`,
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
name: 'non-const type parameter should keep as const',
|
|
775
|
+
source: dedent`
|
|
776
|
+
function g<T>(x: T): T { return x; }
|
|
777
|
+
const b = g([1, 2] as const);
|
|
778
|
+
`,
|
|
779
|
+
expected: dedent`
|
|
780
|
+
function g<T>(x: T): T { return x; }
|
|
781
|
+
const b = g([1, 2] as const);
|
|
782
|
+
`,
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
name: 'imported callee should keep as const',
|
|
786
|
+
source: dedent`
|
|
787
|
+
import { f } from './other.mjs';
|
|
788
|
+
const a = f([1, 2] as const);
|
|
789
|
+
`,
|
|
790
|
+
expected: dedent`
|
|
791
|
+
import { f } from './other.mjs';
|
|
792
|
+
const a = f([1, 2] as const);
|
|
793
|
+
`,
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
name: 'overloaded function should keep as const',
|
|
797
|
+
source: dedent`
|
|
798
|
+
function ov<const T>(x: T): T;
|
|
799
|
+
function ov(x: number): number;
|
|
800
|
+
function ov(x: unknown): unknown { return x; }
|
|
801
|
+
const e = ov([1] as const);
|
|
802
|
+
`,
|
|
803
|
+
expected: dedent`
|
|
804
|
+
function ov<const T>(x: T): T;
|
|
805
|
+
function ov(x: number): number;
|
|
806
|
+
function ov(x: unknown): unknown { return x; }
|
|
807
|
+
const e = ov([1] as const);
|
|
808
|
+
`,
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
name: 'explicit type arguments should keep as const',
|
|
812
|
+
source: dedent`
|
|
813
|
+
function f<const T>(x: T): T { return x; }
|
|
814
|
+
const j = f<readonly [1, 2]>([1, 2] as const);
|
|
815
|
+
`,
|
|
816
|
+
expected: dedent`
|
|
817
|
+
function f<const T>(x: T): T { return x; }
|
|
818
|
+
const j = f<readonly [1, 2]>([1, 2] as const);
|
|
819
|
+
`,
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
name: 'non-bare type parameter reference (readonly T[]) should keep as const',
|
|
823
|
+
source: dedent`
|
|
824
|
+
function ro<const T>(xs: readonly T[]): T { return xs[0] as T; }
|
|
825
|
+
const o = ro([1, 2] as const);
|
|
826
|
+
`,
|
|
827
|
+
expected: dedent`
|
|
828
|
+
function ro<const T>(xs: readonly T[]): T { return xs[0] as T; }
|
|
829
|
+
const o = ro([1, 2] as const);
|
|
830
|
+
`,
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
name: 'argument after spread argument should keep as const',
|
|
834
|
+
source: dedent`
|
|
835
|
+
function f<const T>(a: T, b: T): T { return a; }
|
|
836
|
+
const xs = [1, 2] as const;
|
|
837
|
+
const s = f(...xs, [3] as const);
|
|
838
|
+
`,
|
|
839
|
+
expected: dedent`
|
|
840
|
+
function f<const T>(a: T, b: T): T { return a; }
|
|
841
|
+
const xs = [1, 2] as const;
|
|
842
|
+
const s = f(...xs, [3] as const);
|
|
843
|
+
`,
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
name: 'transformer-ignore-next-line should keep as const',
|
|
847
|
+
source: dedent`
|
|
848
|
+
function f<const T>(x: T): T { return x; }
|
|
849
|
+
// transformer-ignore-next-line
|
|
850
|
+
const a = f([1, 2] as const);
|
|
851
|
+
`,
|
|
852
|
+
expected: dedent`
|
|
853
|
+
function f<const T>(x: T): T { return x; }
|
|
854
|
+
// transformer-ignore-next-line
|
|
855
|
+
const a = f([1, 2] as const);
|
|
856
|
+
`,
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
name: 'removeAsConstForConstTypeParameters: false should keep as const',
|
|
860
|
+
source: dedent`
|
|
861
|
+
function f<const T>(x: T): T { return x; }
|
|
862
|
+
const a = f([1, 2] as const);
|
|
863
|
+
`,
|
|
864
|
+
expected: dedent`
|
|
865
|
+
function f<const T>(x: T): T { return x; }
|
|
866
|
+
const a = f([1, 2] as const);
|
|
867
|
+
`,
|
|
868
|
+
options: { removeAsConstForConstTypeParameters: false },
|
|
869
|
+
},
|
|
870
|
+
])('$name', testFn);
|
|
871
|
+
});
|
|
872
|
+
|
|
701
873
|
describe('ignorePrefixes option (mut_ prefix)', () => {
|
|
702
874
|
test.each([
|
|
703
875
|
{
|