ts-codemod-lib 3.4.4 → 3.5.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/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
- package/dist/functions/ast-transformers/append-as-const.mjs +3 -3
- package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-interface-to-type.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-interface-to-type.mjs +2 -1
- package/dist/functions/ast-transformers/convert-interface-to-type.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +2 -2
- package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
- package/dist/functions/ast-transformers/replace-any-with-unknown.d.mts.map +1 -1
- package/dist/functions/ast-transformers/replace-any-with-unknown.mjs +3 -2
- package/dist/functions/ast-transformers/replace-any-with-unknown.mjs.map +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts.map +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +6 -5
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -1
- package/dist/functions/functions/has-disable-next-line-comment.d.mts.map +1 -1
- package/dist/functions/functions/has-disable-next-line-comment.mjs +56 -4
- package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -1
- package/dist/functions/utils/index.d.mts +1 -0
- package/dist/functions/utils/index.d.mts.map +1 -1
- package/dist/functions/utils/index.mjs +1 -0
- package/dist/functions/utils/index.mjs.map +1 -1
- package/dist/functions/utils/replace-node-with-text.d.mts +21 -0
- package/dist/functions/utils/replace-node-with-text.d.mts.map +1 -0
- package/dist/functions/utils/replace-node-with-text.mjs +22 -0
- package/dist/functions/utils/replace-node-with-text.mjs.map +1 -0
- package/dist/functions/utils/replace-with-debug.d.mts.map +1 -1
- package/dist/functions/utils/replace-with-debug.mjs +2 -1
- package/dist/functions/utils/replace-with-debug.mjs.map +1 -1
- package/package.json +13 -13
- package/src/functions/ast-transformers/append-as-const.mts +6 -3
- package/src/functions/ast-transformers/convert-interface-to-type.mts +2 -1
- package/src/functions/ast-transformers/convert-to-readonly.mts +5 -2
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +9 -20
- package/src/functions/ast-transformers/replace-any-with-unknown.mts +3 -2
- package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +7 -5
- package/src/functions/functions/has-disable-next-line-comment.mts +79 -6
- package/src/functions/functions/has-disable-next-line-comment.test.mts +96 -0
- package/src/functions/utils/index.mts +1 -0
- package/src/functions/utils/replace-node-with-text.mts +37 -0
- package/src/functions/utils/replace-node-with-text.test.mts +89 -0
- package/src/functions/utils/replace-with-debug.mts +2 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type * as tsm from 'ts-morph';
|
|
2
|
+
/**
|
|
3
|
+
* `node.replaceWithText(newNodeText)`, sparing a one-line text the indentation
|
|
4
|
+
* lookup.
|
|
5
|
+
*
|
|
6
|
+
* `replaceWithText` writes the text through a writer whose indentation it
|
|
7
|
+
* looks up first, for the lines after the first one, and the lookup reparses
|
|
8
|
+
* the whole file in the language service: on top of the reparse every
|
|
9
|
+
* replacement makes anyway, once per replacement, so it is quadratic in the
|
|
10
|
+
* size of the file. A text with no `\n` has no line after the first, the
|
|
11
|
+
* indentation is never applied, and a plain writer from the same project
|
|
12
|
+
* writes the same text — so the lookup is skipped. A multi-line text takes
|
|
13
|
+
* the original path.
|
|
14
|
+
*
|
|
15
|
+
* The writer is passed as `replaceWithText`'s second argument, which its
|
|
16
|
+
* declaration leaves out and ts-morph passes itself when it sets an
|
|
17
|
+
* expression. Were it ever dropped, the method would look the indentation up
|
|
18
|
+
* as before: slower, never different.
|
|
19
|
+
*/
|
|
20
|
+
export declare const replaceNodeWithText: (node: tsm.Node, newNodeText: string) => tsm.Node;
|
|
21
|
+
//# sourceMappingURL=replace-node-with-text.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replace-node-with-text.d.mts","sourceRoot":"","sources":["../../../src/functions/utils/replace-node-with-text.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,GAAG,MAAM,UAAU,CAAC;AAErC;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,mBAAmB,SACxB,GAAG,CAAC,IAAI,eACD,MAAM,KAClB,GAAG,CAAC,IAOA,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
export const replaceNodeWithText = (node, newNodeText) => newNodeText.includes('\n')
|
|
20
|
+
? node.replaceWithText(newNodeText)
|
|
21
|
+
: node.replaceWithText.call(node, newNodeText, node.getProject().createWriter());
|
|
22
|
+
//# sourceMappingURL=replace-node-with-text.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replace-node-with-text.mjs","sourceRoot":"","sources":["../../../src/functions/utils/replace-node-with-text.mts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,IAAc,EACd,WAAmB,EACT,EAAE,CACZ,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC;IACxB,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC;IACnC,CAAC,CAAE,IAAI,CAAC,eAA6C,CAAC,IAAI,CACtD,IAAI,EACJ,WAAW,EACX,IAAI,CAAC,UAAU,EAAE,CAAC,YAAY,EAAE,CACjC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replace-with-debug.d.mts","sourceRoot":"","sources":["../../../src/functions/utils/replace-with-debug.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,GAAG,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"replace-with-debug.d.mts","sourceRoot":"","sources":["../../../src/functions/utils/replace-with-debug.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,GAAG,MAAM,UAAU,CAAC;AAGrC,eAAO,MAAM,yBAAyB,SAC9B,GAAG,CAAC,IAAI,eACD,MAAM,KAClB,IAIF,CAAC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { replaceNodeWithText } from './replace-node-with-text.mjs';
|
|
1
2
|
export const replaceNodeWithDebugPrint = (node, newNodeText) => {
|
|
2
3
|
console.debug(`${node.getText()} -> ${newNodeText}`);
|
|
3
|
-
node
|
|
4
|
+
replaceNodeWithText(node, newNodeText);
|
|
4
5
|
};
|
|
5
6
|
//# sourceMappingURL=replace-with-debug.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replace-with-debug.mjs","sourceRoot":"","sources":["../../../src/functions/utils/replace-with-debug.mts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"replace-with-debug.mjs","sourceRoot":"","sources":["../../../src/functions/utils/replace-with-debug.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAEnE,MAAM,CAAC,MAAM,yBAAyB,GAAG,CACvC,IAAc,EACd,WAAmB,EACb,EAAE;IACR,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,WAAW,EAAE,CAAC,CAAC;IAErD,mBAAmB,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;AACzC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-codemod-lib",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"LICENSE"
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"ts-data-forge": "^14.
|
|
41
|
+
"ts-data-forge": "^14.8.0",
|
|
42
42
|
"ts-morph": "^28.0.0",
|
|
43
43
|
"ts-type-forge": "^9.2.2"
|
|
44
44
|
},
|
|
@@ -51,10 +51,10 @@
|
|
|
51
51
|
"dedent": "1.7.2",
|
|
52
52
|
"esbuild": "0.28.2",
|
|
53
53
|
"eslint": "10.12.0",
|
|
54
|
-
"eslint-config-typed": "5.
|
|
55
|
-
"eslint-plugin-ts-data-forge": "0.
|
|
56
|
-
"eslint-plugin-ts-fortress": "1.3.
|
|
57
|
-
"eslint-plugin-ts-type-forge": "0.8.
|
|
54
|
+
"eslint-config-typed": "5.19.0",
|
|
55
|
+
"eslint-plugin-ts-data-forge": "0.9.0",
|
|
56
|
+
"eslint-plugin-ts-fortress": "1.3.4",
|
|
57
|
+
"eslint-plugin-ts-type-forge": "0.8.9",
|
|
58
58
|
"jiti": "2.7.0",
|
|
59
59
|
"npm-run-all2": "9.0.3",
|
|
60
60
|
"playwright": "1.64.0",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"pnpm": ">=9.0.0"
|
|
76
76
|
},
|
|
77
77
|
"volta": {
|
|
78
|
-
"node": "26.
|
|
78
|
+
"node": "26.12.0"
|
|
79
79
|
},
|
|
80
80
|
"publishConfig": {
|
|
81
81
|
"access": "public",
|
|
@@ -86,11 +86,11 @@
|
|
|
86
86
|
"check:cspell": "cspell \"**\" --gitignore --gitignore-root ../../ --no-progress",
|
|
87
87
|
"check:dist": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/check-dist.mts",
|
|
88
88
|
"check:ext": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/check-ext.mts",
|
|
89
|
-
"check:lint": "oxlint . && eslint .",
|
|
90
|
-
"check:test": "pnpm run z:vitest:node run",
|
|
91
|
-
"check:test:browser": "pnpm run z:vitest --project='Browser' run",
|
|
92
|
-
"check:test:cov": "pnpm run z:vitest:node run --coverage",
|
|
93
|
-
"check:types": "node ../../node_modules/typescript-native/bin/tsc --noEmit",
|
|
89
|
+
"check:lint": "node ../../tools/scripts/cmd/assert-deps-built.mts && oxlint . && eslint .",
|
|
90
|
+
"check:test": "node ../../tools/scripts/cmd/assert-deps-built.mts && pnpm run z:vitest:node run",
|
|
91
|
+
"check:test:browser": "node ../../tools/scripts/cmd/assert-deps-built.mts && pnpm run z:vitest --project='Browser' run",
|
|
92
|
+
"check:test:cov": "node ../../tools/scripts/cmd/assert-deps-built.mts && pnpm run z:vitest:node run --coverage",
|
|
93
|
+
"check:types": "node ../../tools/scripts/cmd/assert-deps-built.mts && node ../../node_modules/typescript-native/bin/tsc --noEmit",
|
|
94
94
|
"check-all": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/check-all.mts",
|
|
95
95
|
"clean": "pnpm run /clean:.*/",
|
|
96
96
|
"clean:logs": "npx rimraf ./*.log",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"fix:fmt": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/format-uncommitted.mts --cwd .",
|
|
103
103
|
"fix:fmt:diff": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/format-diff-from.mts origin/main --cwd .",
|
|
104
104
|
"fix:fmt:full": "prettier --write .",
|
|
105
|
-
"fix:lint": "oxlint . --fix && eslint . --fix",
|
|
105
|
+
"fix:lint": "node ../../tools/scripts/cmd/assert-deps-built.mts && oxlint . --fix && eslint . --fix",
|
|
106
106
|
"fmt": "pnpm run fix:fmt",
|
|
107
107
|
"gen": "run-s gen:index gen:readme gen:jsdoc",
|
|
108
108
|
"gen:index": "run-s gen:index:src fix:fmt",
|
|
@@ -5,7 +5,10 @@ import {
|
|
|
5
5
|
hasDisableNextLineComment,
|
|
6
6
|
isAsConstNode,
|
|
7
7
|
} from '../functions/index.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
replaceNodeWithDebugPrint,
|
|
10
|
+
replaceNodeWithText,
|
|
11
|
+
} from '../utils/index.mjs';
|
|
9
12
|
import type { TsMorphTransformer } from './types.mjs';
|
|
10
13
|
|
|
11
14
|
const TRANSFORMER_NAME = 'append-as-const';
|
|
@@ -27,7 +30,7 @@ export const appendAsConstTransformer = (
|
|
|
27
30
|
replaceNode:
|
|
28
31
|
options?.debug === true
|
|
29
32
|
? replaceNodeWithDebugPrint
|
|
30
|
-
: (node, newNodeText) => node
|
|
33
|
+
: (node, newNodeText) => replaceNodeWithText(node, newNodeText),
|
|
31
34
|
} as const;
|
|
32
35
|
|
|
33
36
|
return {
|
|
@@ -319,7 +322,7 @@ const transformNode = (
|
|
|
319
322
|
|
|
320
323
|
options.debugPrint(`${node.getText()} -> ${asConstText}`);
|
|
321
324
|
|
|
322
|
-
transformNode(node
|
|
325
|
+
transformNode(replaceNodeWithText(node, asConstText), context, options);
|
|
323
326
|
|
|
324
327
|
return;
|
|
325
328
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Arr } from 'ts-data-forge';
|
|
4
4
|
import type * as tsm from 'ts-morph';
|
|
5
5
|
import { hasDisableNextLineComment } from '../functions/index.mjs';
|
|
6
|
+
import { replaceNodeWithText } from '../utils/index.mjs';
|
|
6
7
|
import type { TsMorphTransformer } from './types.mjs';
|
|
7
8
|
|
|
8
9
|
const TRANSFORMER_NAME = 'convert-interface-to-type';
|
|
@@ -91,7 +92,7 @@ export const convertInterfaceToTypeTransformer = (): TsMorphTransformer =>
|
|
|
91
92
|
const typeAliasText =
|
|
92
93
|
`${jsDocText}${exportKeyword}type ${interfaceName}${typeParamsStr} = ${mut_typeBody};` as const;
|
|
93
94
|
|
|
94
|
-
interfaceDecl
|
|
95
|
+
replaceNodeWithText(interfaceDecl, typeAliasText);
|
|
95
96
|
};
|
|
96
97
|
|
|
97
98
|
const buildTypeLiteral = (
|
|
@@ -29,7 +29,10 @@ import {
|
|
|
29
29
|
wrapWithParentheses,
|
|
30
30
|
type ReadonlyTypeReferenceNode,
|
|
31
31
|
} from '../functions/index.mjs';
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
replaceNodeWithDebugPrint,
|
|
34
|
+
replaceNodeWithText,
|
|
35
|
+
} from '../utils/index.mjs';
|
|
33
36
|
import {
|
|
34
37
|
groupUnionIntersectionTypes,
|
|
35
38
|
invalidDeepReadonlyTypeName,
|
|
@@ -72,7 +75,7 @@ export const convertToReadonlyTransformer = (
|
|
|
72
75
|
replaceNode:
|
|
73
76
|
options?.debug === true
|
|
74
77
|
? replaceNodeWithDebugPrint
|
|
75
|
-
: (node, newNodeText) => node
|
|
78
|
+
: (node, newNodeText) => replaceNodeWithText(node, newNodeText),
|
|
76
79
|
} as const;
|
|
77
80
|
|
|
78
81
|
return {
|
|
@@ -7,9 +7,7 @@ import * as prettierPluginEstree from 'prettier/plugins/estree';
|
|
|
7
7
|
import * as prettierPluginTypeScript from 'prettier/plugins/typescript';
|
|
8
8
|
import * as prettier from 'prettier/standalone';
|
|
9
9
|
import { expectType } from 'ts-data-forge';
|
|
10
|
-
|
|
11
|
-
// rewrite `describe('DeepReadonly', ...)` into a reference to this type.
|
|
12
|
-
import type { DeepReadonly as TsTypeForgeDeepReadonly } from 'ts-type-forge';
|
|
10
|
+
import type { DeepReadonly } from 'ts-type-forge';
|
|
13
11
|
import {
|
|
14
12
|
convertToReadonlyTransformer,
|
|
15
13
|
type ReadonlyTransformerOptions,
|
|
@@ -3090,34 +3088,25 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
3090
3088
|
type B = Readonly<{ b: string[] }>;
|
|
3091
3089
|
|
|
3092
3090
|
// `|` commutes with DeepReadonly, so merging a union is safe.
|
|
3093
|
-
expectType<
|
|
3094
|
-
TsTypeForgeDeepReadonly<A | B>,
|
|
3095
|
-
TsTypeForgeDeepReadonly<A> | TsTypeForgeDeepReadonly<B>
|
|
3096
|
-
>('=');
|
|
3091
|
+
expectType<DeepReadonly<A | B>, DeepReadonly<A> | DeepReadonly<B>>('=');
|
|
3097
3092
|
|
|
3098
|
-
expectType<
|
|
3099
|
-
TsTypeForgeDeepReadonly<F | X>,
|
|
3100
|
-
TsTypeForgeDeepReadonly<F> | TsTypeForgeDeepReadonly<X>
|
|
3101
|
-
>('=');
|
|
3093
|
+
expectType<DeepReadonly<F | X>, DeepReadonly<F> | DeepReadonly<X>>('=');
|
|
3102
3094
|
|
|
3103
3095
|
expectType<
|
|
3104
|
-
|
|
3105
|
-
|
|
3096
|
+
DeepReadonly<readonly string[] | X>,
|
|
3097
|
+
DeepReadonly<readonly string[]> | DeepReadonly<X>
|
|
3106
3098
|
>('=');
|
|
3107
3099
|
|
|
3108
3100
|
// `&` does not: an intersection is tested as a whole and matches every
|
|
3109
3101
|
// branch any one member matches. `F & X` hits the function branch and
|
|
3110
3102
|
// comes back unchanged, leaving `x` mutable.
|
|
3111
|
-
expectType<
|
|
3103
|
+
expectType<DeepReadonly<F & X>, F & X>('=');
|
|
3112
3104
|
|
|
3113
|
-
expectType<
|
|
3114
|
-
TsTypeForgeDeepReadonly<F & X>,
|
|
3115
|
-
TsTypeForgeDeepReadonly<F> & TsTypeForgeDeepReadonly<X>
|
|
3116
|
-
>('!=');
|
|
3105
|
+
expectType<DeepReadonly<F & X>, DeepReadonly<F> & DeepReadonly<X>>('!=');
|
|
3117
3106
|
|
|
3118
3107
|
expectType<
|
|
3119
|
-
|
|
3120
|
-
|
|
3108
|
+
DeepReadonly<readonly string[] & X>,
|
|
3109
|
+
DeepReadonly<readonly string[]> & DeepReadonly<X>
|
|
3121
3110
|
>('!=');
|
|
3122
3111
|
|
|
3123
3112
|
// `Readonly` over type literals does commute with `&`, which is what
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
isSpreadNamedTupleMemberNode,
|
|
5
5
|
isSpreadParameterNode,
|
|
6
6
|
} from '../functions/index.mjs';
|
|
7
|
+
import { replaceNodeWithText } from '../utils/index.mjs';
|
|
7
8
|
import type { TsMorphTransformer } from './types.mjs';
|
|
8
9
|
|
|
9
10
|
const TRANSFORMER_NAME = 'replace-any-with-unknown';
|
|
@@ -37,12 +38,12 @@ const transformNode = (node: tsm.Node): void => {
|
|
|
37
38
|
// `[name: E0, ...args: any)]` -> `[name: E0, ...args: unknown[]]`
|
|
38
39
|
isSpreadNamedTupleMemberNode(parent))
|
|
39
40
|
) {
|
|
40
|
-
anyKeywordNode
|
|
41
|
+
replaceNodeWithText(anyKeywordNode, 'readonly unknown[]');
|
|
41
42
|
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
anyKeywordNode
|
|
46
|
+
replaceNodeWithText(anyKeywordNode, 'unknown');
|
|
46
47
|
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Arr } from 'ts-data-forge';
|
|
3
3
|
import * as tsm from 'ts-morph';
|
|
4
4
|
import { hasDisableNextLineComment } from '../functions/index.mjs';
|
|
5
|
+
import { replaceNodeWithText } from '../utils/index.mjs';
|
|
5
6
|
import type { TsMorphTransformer } from './types.mjs';
|
|
6
7
|
|
|
7
8
|
const TRANSFORMER_NAME = 'replace-record-with-unknown-record';
|
|
@@ -88,7 +89,8 @@ const processInterfaceDeclaration = (
|
|
|
88
89
|
|
|
89
90
|
const interfaceName = interfaceDecl.getName();
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
replaceNodeWithText(
|
|
93
|
+
interfaceDecl,
|
|
92
94
|
`${exportPrefix}type ${interfaceName} = UnknownRecord;`,
|
|
93
95
|
);
|
|
94
96
|
|
|
@@ -128,7 +130,7 @@ const visitTypeNode = (node: tsm.TypeNode): boolean => {
|
|
|
128
130
|
Arr.isFixedLengthArray(1, indexSigs) &&
|
|
129
131
|
isStringUnknownIndexSignature(indexSigs[0])
|
|
130
132
|
) {
|
|
131
|
-
node
|
|
133
|
+
replaceNodeWithText(node, 'UnknownRecord');
|
|
132
134
|
|
|
133
135
|
return true;
|
|
134
136
|
}
|
|
@@ -156,7 +158,7 @@ const visitTypeNode = (node: tsm.TypeNode): boolean => {
|
|
|
156
158
|
Arr.isFixedLengthArray(1, indexSigs) &&
|
|
157
159
|
isStringUnknownIndexSignature(indexSigs[0])
|
|
158
160
|
) {
|
|
159
|
-
node
|
|
161
|
+
replaceNodeWithText(node, 'UnknownRecord');
|
|
160
162
|
|
|
161
163
|
return true;
|
|
162
164
|
}
|
|
@@ -229,7 +231,7 @@ const replaceIfRecordUnknown = (node: tsm.TypeReferenceNode): boolean => {
|
|
|
229
231
|
typeArgs[0].getText() === 'string' &&
|
|
230
232
|
typeArgs[1].getText() === 'unknown'
|
|
231
233
|
) {
|
|
232
|
-
node
|
|
234
|
+
replaceNodeWithText(node, 'UnknownRecord');
|
|
233
235
|
|
|
234
236
|
return true;
|
|
235
237
|
}
|
|
@@ -255,7 +257,7 @@ const replaceIfRecordUnknown = (node: tsm.TypeReferenceNode): boolean => {
|
|
|
255
257
|
innerTypeArgs[0].getText() === 'string' &&
|
|
256
258
|
innerTypeArgs[1].getText() === 'unknown'
|
|
257
259
|
) {
|
|
258
|
-
node
|
|
260
|
+
replaceNodeWithText(node, 'UnknownRecord');
|
|
259
261
|
|
|
260
262
|
return true;
|
|
261
263
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Arr } from 'ts-data-forge';
|
|
1
2
|
import * as tsm from 'ts-morph';
|
|
2
3
|
import { IGNORE_LINE_COMMENT_PREFIXES } from '../constants/index.mjs';
|
|
3
4
|
|
|
@@ -19,16 +20,22 @@ export const hasDisableNextLineComment = (
|
|
|
19
20
|
node: tsm.Node,
|
|
20
21
|
transformerName?: string,
|
|
21
22
|
): boolean => {
|
|
22
|
-
|
|
23
|
+
// Most nodes have no leading comment, and then nothing below can match.
|
|
24
|
+
// Asking first spares them the line numbers, the costly part.
|
|
25
|
+
const leadingCommentRanges = node.getLeadingCommentRanges();
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
if (nodeStartLine <= 1) {
|
|
27
|
+
if (Arr.isEmpty(leadingCommentRanges)) {
|
|
26
28
|
return false;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
const
|
|
31
|
+
const lineNumberAt = lineNumberLookup(node.getSourceFile().compilerNode);
|
|
30
32
|
|
|
31
|
-
const
|
|
33
|
+
const nodeStartLine = lineNumberAt(node.getStart());
|
|
34
|
+
|
|
35
|
+
// Cannot be ignored if it's on the first line
|
|
36
|
+
if (nodeStartLine <= 1) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
32
39
|
|
|
33
40
|
// Iterate backwards through comments as the closest one is most relevant
|
|
34
41
|
for (let mut_i = leadingCommentRanges.length - 1; mut_i >= 0; mut_i--) {
|
|
@@ -38,7 +45,7 @@ export const hasDisableNextLineComment = (
|
|
|
38
45
|
// Get the end position and convert it to line number
|
|
39
46
|
const commentEndPos = commentRange.getEnd();
|
|
40
47
|
|
|
41
|
-
const commentEndLine =
|
|
48
|
+
const commentEndLine = lineNumberAt(commentEndPos);
|
|
42
49
|
|
|
43
50
|
// Check if the comment is on the immediately preceding line
|
|
44
51
|
if (nodeStartLine === commentEndLine + 1) {
|
|
@@ -91,3 +98,69 @@ export const hasDisableNextLineComment = (
|
|
|
91
98
|
|
|
92
99
|
return false;
|
|
93
100
|
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The 1-based line number of a position in `sourceFile`, counted the way
|
|
104
|
+
* ts-morph's `getStartLineNumber` and `getLineAndColumnAtPos` count it: one
|
|
105
|
+
* plus the number of `\n` before the position (a lone `\r` starts no line,
|
|
106
|
+
* unlike in the compiler's own line map).
|
|
107
|
+
*
|
|
108
|
+
* ts-morph scans the text from its start on every call, which made this
|
|
109
|
+
* check, asked of every node a transformer visits, quadratic in the size of
|
|
110
|
+
* the file. The newline positions are collected once per compiler source file
|
|
111
|
+
* instead. ts-morph replaces that object whenever the file is edited, so an
|
|
112
|
+
* entry never outlives the text it was collected from.
|
|
113
|
+
*/
|
|
114
|
+
const lineNumberLookup = (
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
116
|
+
sourceFile: tsm.ts.SourceFile,
|
|
117
|
+
): ((pos: number) => number) => {
|
|
118
|
+
const cached = newlinePositionsCache.get(sourceFile);
|
|
119
|
+
|
|
120
|
+
const newlinePositions = cached ?? newlinePositionsOf(sourceFile.text);
|
|
121
|
+
|
|
122
|
+
if (cached === undefined) {
|
|
123
|
+
newlinePositionsCache.set(sourceFile, newlinePositions);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (pos) => countLessThan(newlinePositions, pos) + 1;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const newlinePositionsCache = new WeakMap<
|
|
130
|
+
tsm.ts.SourceFile,
|
|
131
|
+
readonly number[]
|
|
132
|
+
>();
|
|
133
|
+
|
|
134
|
+
const newlinePositionsOf = (text: string): readonly number[] => {
|
|
135
|
+
const mut_positions: number[] = [];
|
|
136
|
+
|
|
137
|
+
for (
|
|
138
|
+
let mut_index = text.indexOf('\n');
|
|
139
|
+
mut_index !== -1;
|
|
140
|
+
mut_index = text.indexOf('\n', mut_index + 1)
|
|
141
|
+
) {
|
|
142
|
+
mut_positions.push(mut_index);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return mut_positions;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** The number of elements of the ascending `sorted` that are below `value`. */
|
|
149
|
+
const countLessThan = (sorted: readonly number[], value: number): number => {
|
|
150
|
+
let mut_low = 0;
|
|
151
|
+
|
|
152
|
+
let mut_high = sorted.length;
|
|
153
|
+
|
|
154
|
+
while (mut_low < mut_high) {
|
|
155
|
+
const mid = Math.floor((mut_low + mut_high) / 2);
|
|
156
|
+
|
|
157
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
158
|
+
if (sorted[mid]! < value) {
|
|
159
|
+
mut_low = mid + 1;
|
|
160
|
+
} else {
|
|
161
|
+
mut_high = mid;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return mut_low;
|
|
166
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as tsm from 'ts-morph';
|
|
2
|
+
import { hasDisableNextLineComment } from './has-disable-next-line-comment.mjs';
|
|
3
|
+
|
|
4
|
+
/** The first type alias in `source`, the node every case asks about. */
|
|
5
|
+
const typeAliasOf = (source: string): tsm.TypeAliasDeclaration => {
|
|
6
|
+
const project = new tsm.Project({ useInMemoryFileSystem: true });
|
|
7
|
+
|
|
8
|
+
return project
|
|
9
|
+
.createSourceFile('source.ts', source)
|
|
10
|
+
.getFirstDescendantByKindOrThrow(tsm.SyntaxKind.TypeAliasDeclaration);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe(hasDisableNextLineComment, () => {
|
|
14
|
+
test.each([
|
|
15
|
+
{
|
|
16
|
+
name: 'a comment on the line above (LF)',
|
|
17
|
+
source: '// transformer-ignore-next-line\ntype A = number[];\n',
|
|
18
|
+
expected: true,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'a comment on the line above (CRLF)',
|
|
22
|
+
source: '// transformer-ignore-next-line\r\ntype A = number[];\r\n',
|
|
23
|
+
expected: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'a comment two lines above',
|
|
27
|
+
source: '// transformer-ignore-next-line\n\ntype A = number[];\n',
|
|
28
|
+
expected: false,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'a comment two lines above (CRLF)',
|
|
32
|
+
source: '// transformer-ignore-next-line\r\n\r\ntype A = number[];\r\n',
|
|
33
|
+
expected: false,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'a later comment on the line above shadows an earlier ignore comment',
|
|
37
|
+
source:
|
|
38
|
+
'// transformer-ignore-next-line\n// something else\ntype A = number[];\n',
|
|
39
|
+
expected: false,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'the node on the first line',
|
|
43
|
+
source: 'type A = number[];\n',
|
|
44
|
+
expected: false,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'the node on the first line after a block comment',
|
|
48
|
+
source: '/* transformer-ignore-next-line */ type A = number[];\n',
|
|
49
|
+
expected: false,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'a lone CR, which does not count as a line break',
|
|
53
|
+
source: '// transformer-ignore-next-line\rtype A = number[];\n',
|
|
54
|
+
expected: false,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'a lone CR before the comment, deep in the file',
|
|
58
|
+
source:
|
|
59
|
+
'const x = 1;\r\nconst y = 2;\r// transformer-ignore-next-line\ntype A = number[];\n',
|
|
60
|
+
expected: true,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'a node far below many lines',
|
|
64
|
+
source: `${'const x = 1;\n'.repeat(500)}// transformer-ignore-next-line\ntype A = number[];\n`,
|
|
65
|
+
expected: true,
|
|
66
|
+
},
|
|
67
|
+
])('$name', ({ source, expected }) => {
|
|
68
|
+
expect(hasDisableNextLineComment(typeAliasOf(source))).toBe(expected);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('matches the transformer name list', () => {
|
|
72
|
+
const node = typeAliasOf(
|
|
73
|
+
'// transformer-ignore-next-line append-as-const, convert-to-readonly\ntype A = number[];\n',
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
assert.isTrue(hasDisableNextLineComment(node, 'convert-to-readonly'));
|
|
77
|
+
|
|
78
|
+
assert.isFalse(hasDisableNextLineComment(node, 'replace-any-with-unknown'));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('reads the current text after the file has been edited', () => {
|
|
82
|
+
const node = typeAliasOf('type A = number[];\n');
|
|
83
|
+
|
|
84
|
+
assert.isFalse(hasDisableNextLineComment(node));
|
|
85
|
+
|
|
86
|
+
const sourceFile = node.getSourceFile();
|
|
87
|
+
|
|
88
|
+
sourceFile.insertText(0, '// transformer-ignore-next-line\n');
|
|
89
|
+
|
|
90
|
+
const edited = sourceFile.getFirstDescendantByKindOrThrow(
|
|
91
|
+
tsm.SyntaxKind.TypeAliasDeclaration,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
assert.isTrue(hasDisableNextLineComment(edited));
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type * as tsm from 'ts-morph';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `node.replaceWithText(newNodeText)`, sparing a one-line text the indentation
|
|
5
|
+
* lookup.
|
|
6
|
+
*
|
|
7
|
+
* `replaceWithText` writes the text through a writer whose indentation it
|
|
8
|
+
* looks up first, for the lines after the first one, and the lookup reparses
|
|
9
|
+
* the whole file in the language service: on top of the reparse every
|
|
10
|
+
* replacement makes anyway, once per replacement, so it is quadratic in the
|
|
11
|
+
* size of the file. A text with no `\n` has no line after the first, the
|
|
12
|
+
* indentation is never applied, and a plain writer from the same project
|
|
13
|
+
* writes the same text — so the lookup is skipped. A multi-line text takes
|
|
14
|
+
* the original path.
|
|
15
|
+
*
|
|
16
|
+
* The writer is passed as `replaceWithText`'s second argument, which its
|
|
17
|
+
* declaration leaves out and ts-morph passes itself when it sets an
|
|
18
|
+
* expression. Were it ever dropped, the method would look the indentation up
|
|
19
|
+
* as before: slower, never different.
|
|
20
|
+
*/
|
|
21
|
+
export const replaceNodeWithText = (
|
|
22
|
+
node: tsm.Node,
|
|
23
|
+
newNodeText: string,
|
|
24
|
+
): tsm.Node =>
|
|
25
|
+
newNodeText.includes('\n')
|
|
26
|
+
? node.replaceWithText(newNodeText)
|
|
27
|
+
: (node.replaceWithText as ReplaceWithTextWithWriter).call(
|
|
28
|
+
node,
|
|
29
|
+
newNodeText,
|
|
30
|
+
node.getProject().createWriter(),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
type ReplaceWithTextWithWriter = (
|
|
34
|
+
this: tsm.Node,
|
|
35
|
+
textOrWriterFunction: string,
|
|
36
|
+
writer: tsm.CodeBlockWriter,
|
|
37
|
+
) => tsm.Node;
|