ts-codemod-lib 4.0.0 → 4.1.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 +2 -1
- package/dist/functions/ast-transformers/append-as-const.d.mts +9 -10
- package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
- package/dist/functions/ast-transformers/append-as-const.mjs +123 -114
- package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts +28 -7
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +452 -253
- package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts +8 -8
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs +103 -104
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs.map +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts +2 -2
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +51 -51
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -1
- package/dist/functions/ast-transformers/transform-source-code.d.mts.map +1 -1
- package/dist/functions/ast-transformers/transform-source-code.mjs +69 -69
- package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -1
- package/dist/functions/constants/mutable-prefixes.d.mts +8 -8
- package/dist/functions/functions/has-disable-next-line-comment.mjs +23 -23
- package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -1
- package/dist/functions/functions/is-readonly-node.d.mts +2 -2
- package/dist/functions/functions/is-readonly-node.d.mts.map +1 -1
- package/dist/functions/functions/is-readonly-node.mjs +13 -13
- package/dist/functions/functions/is-readonly-node.mjs.map +1 -1
- package/dist/functions/functions/wrap-with-parentheses.d.mts +2 -2
- package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -1
- package/dist/functions/functions/wrap-with-parentheses.mjs +87 -32
- package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -1
- package/dist/functions/utils/replace-node-with-text.d.mts +10 -11
- package/dist/functions/utils/replace-node-with-text.d.mts.map +1 -1
- package/dist/functions/utils/replace-node-with-text.mjs +0 -1
- package/dist/functions/utils/replace-node-with-text.mjs.map +1 -1
- package/package.json +9 -9
- package/src/functions/ast-transformers/append-as-const.mts +242 -231
- package/src/functions/ast-transformers/append-as-const.test.mts +39 -6
- package/src/functions/ast-transformers/convert-to-readonly-record-import.test.mts +458 -0
- package/src/functions/ast-transformers/convert-to-readonly.mts +1003 -694
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +80 -33
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.mts +178 -179
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.test.mts +29 -29
- package/src/functions/ast-transformers/replace-any-with-unknown.test.mts +6 -6
- package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +75 -75
- package/src/functions/ast-transformers/replace-record-with-unknown-record.test.mts +6 -6
- package/src/functions/ast-transformers/transform-source-code.mts +110 -110
- package/src/functions/ast-transformers/transformer-specific-ignore.test.mts +6 -6
- package/src/functions/constants/mutable-prefixes.mts +8 -8
- package/src/functions/functions/has-disable-next-line-comment.mts +35 -35
- package/src/functions/functions/is-readonly-node.mts +25 -25
- package/src/functions/functions/wrap-with-parentheses.mts +138 -52
- package/src/functions/functions/wrap-with-parentheses.test.mts +115 -0
- package/src/functions/utils/replace-node-with-text.mts +10 -11
|
@@ -34,6 +34,29 @@ const preamble = dedent`
|
|
|
34
34
|
declare const k: string;
|
|
35
35
|
`;
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Runs the transformer over `code` in {@link project}, and returns what it made
|
|
39
|
+
* of it.
|
|
40
|
+
*/
|
|
41
|
+
const transformInProject = (
|
|
42
|
+
code: string,
|
|
43
|
+
options?: Parameters<typeof enableNoUncheckedIndexedAccessTransformer>[0],
|
|
44
|
+
): string => {
|
|
45
|
+
const sourceAst = project.createSourceFile('source.ts', code, {
|
|
46
|
+
overwrite: true,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
enableNoUncheckedIndexedAccessTransformer(options).transform(sourceAst);
|
|
50
|
+
|
|
51
|
+
return sourceAst.getFullText();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const formatter = (code: string): Promise<string> =>
|
|
55
|
+
prettier.format(code, {
|
|
56
|
+
parser: 'typescript',
|
|
57
|
+
plugins: [prettierPluginTypeScript, prettierPluginEstree],
|
|
58
|
+
});
|
|
59
|
+
|
|
37
60
|
const testFn = async ({
|
|
38
61
|
source,
|
|
39
62
|
expected,
|
|
@@ -59,23 +82,6 @@ const testFn = async ({
|
|
|
59
82
|
expect(transformed).toBe(expectedFormatted);
|
|
60
83
|
};
|
|
61
84
|
|
|
62
|
-
/**
|
|
63
|
-
* Runs the transformer over `code` in {@link project}, and returns what it
|
|
64
|
-
* made of it.
|
|
65
|
-
*/
|
|
66
|
-
const transformInProject = (
|
|
67
|
-
code: string,
|
|
68
|
-
options?: Parameters<typeof enableNoUncheckedIndexedAccessTransformer>[0],
|
|
69
|
-
): string => {
|
|
70
|
-
const sourceAst = project.createSourceFile('source.ts', code, {
|
|
71
|
-
overwrite: true,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
enableNoUncheckedIndexedAccessTransformer(options).transform(sourceAst);
|
|
75
|
-
|
|
76
|
-
return sourceAst.getFullText();
|
|
77
|
-
};
|
|
78
|
-
|
|
79
85
|
/**
|
|
80
86
|
* The one project every case is checked in, rather than one project per case.
|
|
81
87
|
*
|
|
@@ -87,14 +93,14 @@ const transformInProject = (
|
|
|
87
93
|
* fraction of a millisecond. Sixty of those took `check:test:cov` past Vitest's
|
|
88
94
|
* timeout on CI.
|
|
89
95
|
*
|
|
90
|
-
* Reusing the project is half of the answer and the smaller half — measured,
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
96
|
+
* Reusing the project is half of the answer and the smaller half — measured, it
|
|
97
|
+
* takes a case from ~300 ms to ~260 ms, because what an edit invalidates is the
|
|
98
|
+
* program, not the parsed library behind it. Naming the libraries the cases
|
|
99
|
+
* actually need is the rest: together they bring a case to ~25 ms.
|
|
94
100
|
*
|
|
95
101
|
* What that leaves out — `transformSourceCode` itself, and the transformer
|
|
96
|
-
* against the full library — is what the tests at the bottom of this file
|
|
97
|
-
*
|
|
102
|
+
* against the full library — is what the tests at the bottom of this file still
|
|
103
|
+
* cover.
|
|
98
104
|
*/
|
|
99
105
|
const project = new tsm.Project({
|
|
100
106
|
useInMemoryFileSystem: true,
|
|
@@ -107,12 +113,6 @@ const project = new tsm.Project({
|
|
|
107
113
|
},
|
|
108
114
|
});
|
|
109
115
|
|
|
110
|
-
const formatter = (code: string): Promise<string> =>
|
|
111
|
-
prettier.format(code, {
|
|
112
|
-
parser: 'typescript',
|
|
113
|
-
plugins: [prettierPluginTypeScript, prettierPluginEstree],
|
|
114
|
-
});
|
|
115
|
-
|
|
116
116
|
describe(enableNoUncheckedIndexedAccessTransformer, () => {
|
|
117
117
|
describe('accesses the option widens with undefined', () => {
|
|
118
118
|
test.each([
|
|
@@ -7,6 +7,12 @@ import * as prettier from 'prettier/standalone';
|
|
|
7
7
|
import { replaceAnyWithUnknownTransformer } from './replace-any-with-unknown.mjs';
|
|
8
8
|
import { transformSourceCode } from './transform-source-code.mjs';
|
|
9
9
|
|
|
10
|
+
const formatter = (code: string): Promise<string> =>
|
|
11
|
+
prettier.format(code, {
|
|
12
|
+
parser: 'typescript',
|
|
13
|
+
plugins: [prettierPluginTypeScript, prettierPluginEstree],
|
|
14
|
+
});
|
|
15
|
+
|
|
10
16
|
const testFn = async ({
|
|
11
17
|
source,
|
|
12
18
|
expected,
|
|
@@ -33,12 +39,6 @@ const testFn = async ({
|
|
|
33
39
|
expect(transformed).toBe(expectedFormatted);
|
|
34
40
|
};
|
|
35
41
|
|
|
36
|
-
const formatter = (code: string): Promise<string> =>
|
|
37
|
-
prettier.format(code, {
|
|
38
|
-
parser: 'typescript',
|
|
39
|
-
plugins: [prettierPluginTypeScript, prettierPluginEstree],
|
|
40
|
-
});
|
|
41
|
-
|
|
42
42
|
describe(replaceAnyWithUnknownTransformer, () => {
|
|
43
43
|
test.each([
|
|
44
44
|
{
|
|
@@ -12,8 +12,8 @@ const UNKNOWN_RECORD_MODULE = 'ts-type-forge';
|
|
|
12
12
|
const UNKNOWN_RECORD_NAME = 'UnknownRecord';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Replaces `Readonly<Record<string, unknown>>`, `Record<string, unknown>`,
|
|
16
|
-
*
|
|
15
|
+
* Replaces `Readonly<Record<string, unknown>>`, `Record<string, unknown>`, and
|
|
16
|
+
* index signatures `[k: string]: unknown` with `UnknownRecord`.
|
|
17
17
|
*
|
|
18
18
|
* When a replacement occurs, also inserts
|
|
19
19
|
* `import { type UnknownRecord } from 'ts-type-forge';` at the top of the
|
|
@@ -35,77 +35,14 @@ export const replaceRecordWithUnknownRecordTransformer =
|
|
|
35
35
|
},
|
|
36
36
|
}) as const;
|
|
37
37
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
): boolean => {
|
|
41
|
-
const typeAliasModifications = container.getTypeAliases().map((typeAlias) => {
|
|
42
|
-
if (hasDisableNextLineComment(typeAlias, TRANSFORMER_NAME)) {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const typeNode = typeAlias.getTypeNode();
|
|
47
|
-
|
|
48
|
-
return typeNode !== undefined && visitTypeNode(typeNode);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const interfaceModifications = container
|
|
52
|
-
.getInterfaces()
|
|
53
|
-
.map(processInterfaceDeclaration);
|
|
54
|
-
|
|
55
|
-
return [...typeAliasModifications, ...interfaceModifications].includes(true);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const processInterfaceDeclaration = (
|
|
59
|
-
interfaceDecl: tsm.InterfaceDeclaration,
|
|
38
|
+
const isStringUnknownIndexSignature = (
|
|
39
|
+
sig: tsm.IndexSignatureDeclaration,
|
|
60
40
|
): boolean => {
|
|
61
|
-
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Check if interface has index signature [k: string]: unknown
|
|
66
|
-
const indexSignatures = interfaceDecl.getIndexSignatures();
|
|
67
|
-
|
|
68
|
-
const hasStringUnknownSignature = indexSignatures.some((sig) => {
|
|
69
|
-
const keyType = sig.getKeyType();
|
|
70
|
-
|
|
71
|
-
const returnType = sig.getReturnType();
|
|
72
|
-
|
|
73
|
-
return keyType.getText() === 'string' && returnType.getText() === 'unknown';
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// If it has the signature and no other members, replace entire interface with type alias.
|
|
77
|
-
// Skip when the interface carries semantics that a plain type alias would lose
|
|
78
|
-
// (type parameters, extends clauses, declare modifier, default export).
|
|
79
|
-
if (
|
|
80
|
-
hasStringUnknownSignature &&
|
|
81
|
-
Arr.isEmpty(interfaceDecl.getProperties()) &&
|
|
82
|
-
Arr.isFixedLengthArray(1, indexSignatures) &&
|
|
83
|
-
Arr.isEmpty(interfaceDecl.getTypeParameters()) &&
|
|
84
|
-
Arr.isEmpty(interfaceDecl.getExtends()) &&
|
|
85
|
-
!interfaceDecl.hasDeclareKeyword() &&
|
|
86
|
-
!interfaceDecl.hasDefaultKeyword()
|
|
87
|
-
) {
|
|
88
|
-
const exportPrefix = interfaceDecl.hasExportKeyword() ? 'export ' : '';
|
|
89
|
-
|
|
90
|
-
const interfaceName = interfaceDecl.getName();
|
|
91
|
-
|
|
92
|
-
replaceNodeWithText(
|
|
93
|
-
interfaceDecl,
|
|
94
|
-
`${exportPrefix}type ${interfaceName} = UnknownRecord;`,
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
41
|
+
const keyType = sig.getKeyTypeNode();
|
|
99
42
|
|
|
100
|
-
|
|
101
|
-
return interfaceDecl
|
|
102
|
-
.getProperties()
|
|
103
|
-
.map((property) => {
|
|
104
|
-
const typeNode = property.getTypeNode();
|
|
43
|
+
const returnType = sig.getReturnTypeNode();
|
|
105
44
|
|
|
106
|
-
|
|
107
|
-
})
|
|
108
|
-
.includes(true);
|
|
45
|
+
return keyType.getText() === 'string' && returnType?.getText() === 'unknown';
|
|
109
46
|
};
|
|
110
47
|
|
|
111
48
|
const visitTypeNode = (node: tsm.TypeNode): boolean => {
|
|
@@ -208,14 +145,77 @@ const visitTypeNode = (node: tsm.TypeNode): boolean => {
|
|
|
208
145
|
);
|
|
209
146
|
};
|
|
210
147
|
|
|
211
|
-
const
|
|
212
|
-
|
|
148
|
+
const processInterfaceDeclaration = (
|
|
149
|
+
interfaceDecl: tsm.InterfaceDeclaration,
|
|
213
150
|
): boolean => {
|
|
214
|
-
|
|
151
|
+
if (hasDisableNextLineComment(interfaceDecl, TRANSFORMER_NAME)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
215
154
|
|
|
216
|
-
|
|
155
|
+
// Check if interface has index signature [k: string]: unknown
|
|
156
|
+
const indexSignatures = interfaceDecl.getIndexSignatures();
|
|
217
157
|
|
|
218
|
-
|
|
158
|
+
const hasStringUnknownSignature = indexSignatures.some((sig) => {
|
|
159
|
+
const keyType = sig.getKeyType();
|
|
160
|
+
|
|
161
|
+
const returnType = sig.getReturnType();
|
|
162
|
+
|
|
163
|
+
return keyType.getText() === 'string' && returnType.getText() === 'unknown';
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// If it has the signature and no other members, replace entire interface with type alias.
|
|
167
|
+
// Skip when the interface carries semantics that a plain type alias would lose
|
|
168
|
+
// (type parameters, extends clauses, declare modifier, default export).
|
|
169
|
+
if (
|
|
170
|
+
hasStringUnknownSignature &&
|
|
171
|
+
Arr.isEmpty(interfaceDecl.getProperties()) &&
|
|
172
|
+
Arr.isFixedLengthArray(1, indexSignatures) &&
|
|
173
|
+
Arr.isEmpty(interfaceDecl.getTypeParameters()) &&
|
|
174
|
+
Arr.isEmpty(interfaceDecl.getExtends()) &&
|
|
175
|
+
!interfaceDecl.hasDeclareKeyword() &&
|
|
176
|
+
!interfaceDecl.hasDefaultKeyword()
|
|
177
|
+
) {
|
|
178
|
+
const exportPrefix = interfaceDecl.hasExportKeyword() ? 'export ' : '';
|
|
179
|
+
|
|
180
|
+
const interfaceName = interfaceDecl.getName();
|
|
181
|
+
|
|
182
|
+
replaceNodeWithText(
|
|
183
|
+
interfaceDecl,
|
|
184
|
+
`${exportPrefix}type ${interfaceName} = UnknownRecord;`,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Otherwise, check properties for Record types
|
|
191
|
+
return interfaceDecl
|
|
192
|
+
.getProperties()
|
|
193
|
+
.map((property) => {
|
|
194
|
+
const typeNode = property.getTypeNode();
|
|
195
|
+
|
|
196
|
+
return typeNode !== undefined && visitTypeNode(typeNode);
|
|
197
|
+
})
|
|
198
|
+
.includes(true);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const processDeclarations = (
|
|
202
|
+
container: tsm.SourceFile | tsm.ModuleDeclaration,
|
|
203
|
+
): boolean => {
|
|
204
|
+
const typeAliasModifications = container.getTypeAliases().map((typeAlias) => {
|
|
205
|
+
if (hasDisableNextLineComment(typeAlias, TRANSFORMER_NAME)) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const typeNode = typeAlias.getTypeNode();
|
|
210
|
+
|
|
211
|
+
return typeNode !== undefined && visitTypeNode(typeNode);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const interfaceModifications = container
|
|
215
|
+
.getInterfaces()
|
|
216
|
+
.map(processInterfaceDeclaration);
|
|
217
|
+
|
|
218
|
+
return [...typeAliasModifications, ...interfaceModifications].includes(true);
|
|
219
219
|
};
|
|
220
220
|
|
|
221
221
|
const replaceIfRecordUnknown = (node: tsm.TypeReferenceNode): boolean => {
|
|
@@ -7,6 +7,12 @@ import * as prettier from 'prettier/standalone';
|
|
|
7
7
|
import { replaceRecordWithUnknownRecordTransformer } from './replace-record-with-unknown-record.mjs';
|
|
8
8
|
import { transformSourceCode } from './transform-source-code.mjs';
|
|
9
9
|
|
|
10
|
+
const formatter = (code: string): Promise<string> =>
|
|
11
|
+
prettier.format(code, {
|
|
12
|
+
parser: 'typescript',
|
|
13
|
+
plugins: [prettierPluginTypeScript, prettierPluginEstree],
|
|
14
|
+
});
|
|
15
|
+
|
|
10
16
|
const testFn = async ({
|
|
11
17
|
source,
|
|
12
18
|
expected,
|
|
@@ -25,12 +31,6 @@ const testFn = async ({
|
|
|
25
31
|
expect(transformed).toBe(expectedFormatted);
|
|
26
32
|
};
|
|
27
33
|
|
|
28
|
-
const formatter = (code: string): Promise<string> =>
|
|
29
|
-
prettier.format(code, {
|
|
30
|
-
parser: 'typescript',
|
|
31
|
-
plugins: [prettierPluginTypeScript, prettierPluginEstree],
|
|
32
|
-
});
|
|
33
|
-
|
|
34
34
|
describe(replaceRecordWithUnknownRecordTransformer, () => {
|
|
35
35
|
test.each([
|
|
36
36
|
{
|
|
@@ -9,94 +9,47 @@ const fileIgnorePrefixes = [
|
|
|
9
9
|
'transform-ignore',
|
|
10
10
|
] as const;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
* Finds the file-level ignore comment `/* <prefix> name, name *\/` and returns
|
|
14
|
-
* the transformer names it lists (empty for all transformers), or `undefined`
|
|
15
|
-
* when the file has none. Whitespace, line breaks included, may surround the
|
|
16
|
-
* prefix and the names, but the names themselves must stay on one line. The
|
|
17
|
-
* prefixes are tried in order, each against the whole file.
|
|
18
|
-
*
|
|
19
|
-
* The scan runs in time linear in the length of `code`, whatever it contains.
|
|
20
|
-
*/
|
|
21
|
-
const findFileIgnoreTransformers = (
|
|
12
|
+
export const transformSourceCode = (
|
|
22
13
|
code: string,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
14
|
+
isTsx: boolean,
|
|
15
|
+
transformers: readonly TsMorphTransformer[],
|
|
16
|
+
debug: boolean = false,
|
|
17
|
+
): string => {
|
|
18
|
+
const project = new tsm.Project({
|
|
19
|
+
useInMemoryFileSystem: true,
|
|
20
|
+
compilerOptions: {
|
|
21
|
+
jsx: isTsx ? tsm.ts.JsxEmit.React : undefined,
|
|
22
|
+
target: tsm.ts.ScriptTarget.ESNext,
|
|
23
|
+
module: tsm.ts.ModuleKind.ESNext,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
34
26
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
): readonly string[] | undefined => {
|
|
39
|
-
// Queries into these searches only move forward, so each part of `code` is
|
|
40
|
-
// scanned a bounded number of times.
|
|
41
|
-
const findCommentEnd = createForwardSearch((from) =>
|
|
42
|
-
code.indexOf('*/', from),
|
|
27
|
+
const sourceAst = project.createSourceFile(
|
|
28
|
+
`source.${isTsx ? 'tsx' : 'ts'}`,
|
|
29
|
+
code,
|
|
43
30
|
);
|
|
44
31
|
|
|
45
|
-
const
|
|
46
|
-
indexOfLineTerminator(code, from),
|
|
47
|
-
);
|
|
32
|
+
const ignoredTransformers = findFileIgnoreTransformers(code);
|
|
48
33
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
);
|
|
34
|
+
for (const transformer of transformers) {
|
|
35
|
+
const transformerName = transformer.name;
|
|
52
36
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
37
|
+
if (shouldSkipFile(ignoredTransformers, transformerName)) {
|
|
38
|
+
if (debug) {
|
|
39
|
+
console.debug(
|
|
40
|
+
`skipped by ignore-file comment for transformer: ${transformerName}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
59
43
|
|
|
60
|
-
if (!code.startsWith(prefix, prefixStart)) {
|
|
61
44
|
continue;
|
|
62
45
|
}
|
|
63
46
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const commentEnd = findCommentEnd(namesStart);
|
|
67
|
-
|
|
68
|
-
// Nothing after this point is closed, so no later comment can match.
|
|
69
|
-
if (commentEnd === -1) {
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const namesEnd = Math.max(namesStart, trimEndBefore(commentEnd));
|
|
74
|
-
|
|
75
|
-
const lineTerminator = findLineTerminator(namesStart);
|
|
76
|
-
|
|
77
|
-
if (lineTerminator === -1 || lineTerminator >= namesEnd) {
|
|
78
|
-
const names = code.slice(namesStart, namesEnd);
|
|
79
|
-
|
|
80
|
-
// Empty means ignore all transformers; otherwise parse comma-separated
|
|
81
|
-
// transformer names
|
|
82
|
-
return names === '' ? [] : names.split(',').map((name) => name.trim());
|
|
83
|
-
}
|
|
47
|
+
transformer.transform(sourceAst);
|
|
84
48
|
}
|
|
85
49
|
|
|
86
|
-
return
|
|
50
|
+
return sourceAst.getFullText();
|
|
87
51
|
};
|
|
88
52
|
|
|
89
|
-
const shouldSkipFile = (
|
|
90
|
-
ignoredTransformers: readonly string[] | undefined,
|
|
91
|
-
transformerName: string,
|
|
92
|
-
): boolean =>
|
|
93
|
-
ignoredTransformers !== undefined &&
|
|
94
|
-
// Empty array means ignore all transformers (file-level ignore without
|
|
95
|
-
// specific transformers); otherwise check if the transformer is in the
|
|
96
|
-
// ignore list
|
|
97
|
-
(Arr.isEmpty(ignoredTransformers) ||
|
|
98
|
-
ignoredTransformers.includes(transformerName));
|
|
99
|
-
|
|
100
53
|
/**
|
|
101
54
|
* Wraps a search returning the first index at or after `from` (or -1), reusing
|
|
102
55
|
* the previous answer while it still holds for a later `from`.
|
|
@@ -137,6 +90,10 @@ const createLastValueCache = (
|
|
|
137
90
|
};
|
|
138
91
|
};
|
|
139
92
|
|
|
93
|
+
// Same set as `\s` and `String.prototype.trim`
|
|
94
|
+
const isWhitespace = (char: string | undefined): boolean =>
|
|
95
|
+
char !== undefined && /^\s$/u.test(char);
|
|
96
|
+
|
|
140
97
|
const skipWhitespace = (code: string, from: number): number => {
|
|
141
98
|
let mut_index = from;
|
|
142
99
|
|
|
@@ -157,6 +114,10 @@ const skipWhitespaceBackward = (code: string, end: number): number => {
|
|
|
157
114
|
return mut_index;
|
|
158
115
|
};
|
|
159
116
|
|
|
117
|
+
// The characters `.` does not match
|
|
118
|
+
const isLineTerminator = (char: string | undefined): boolean =>
|
|
119
|
+
char !== undefined && /^[\n\r\u{2028}\u{2029}]$/u.test(char);
|
|
120
|
+
|
|
160
121
|
const indexOfLineTerminator = (code: string, from: number): number => {
|
|
161
122
|
let mut_index = from;
|
|
162
123
|
|
|
@@ -167,51 +128,90 @@ const indexOfLineTerminator = (code: string, from: number): number => {
|
|
|
167
128
|
return mut_index < code.length ? mut_index : -1;
|
|
168
129
|
};
|
|
169
130
|
|
|
170
|
-
|
|
171
|
-
const isWhitespace = (char: string | undefined): boolean =>
|
|
172
|
-
char !== undefined && /^\s$/u.test(char);
|
|
173
|
-
|
|
174
|
-
// The characters `.` does not match
|
|
175
|
-
const isLineTerminator = (char: string | undefined): boolean =>
|
|
176
|
-
char !== undefined && /^[\n\r\u{2028}\u{2029}]$/u.test(char);
|
|
177
|
-
|
|
178
|
-
export const transformSourceCode = (
|
|
131
|
+
const findFileIgnoreComment = (
|
|
179
132
|
code: string,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
compilerOptions: {
|
|
187
|
-
jsx: isTsx ? tsm.ts.JsxEmit.React : undefined,
|
|
188
|
-
target: tsm.ts.ScriptTarget.ESNext,
|
|
189
|
-
module: tsm.ts.ModuleKind.ESNext,
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
const sourceAst = project.createSourceFile(
|
|
194
|
-
`source.${isTsx ? 'tsx' : 'ts'}`,
|
|
195
|
-
code,
|
|
133
|
+
prefix: string,
|
|
134
|
+
): readonly string[] | undefined => {
|
|
135
|
+
// Queries into these searches only move forward, so each part of `code` is
|
|
136
|
+
// scanned a bounded number of times.
|
|
137
|
+
const findCommentEnd = createForwardSearch((from) =>
|
|
138
|
+
code.indexOf('*/', from),
|
|
196
139
|
);
|
|
197
140
|
|
|
198
|
-
const
|
|
141
|
+
const findLineTerminator = createForwardSearch((from) =>
|
|
142
|
+
indexOfLineTerminator(code, from),
|
|
143
|
+
);
|
|
199
144
|
|
|
200
|
-
|
|
201
|
-
|
|
145
|
+
const trimEndBefore = createLastValueCache((commentEnd: number) =>
|
|
146
|
+
skipWhitespaceBackward(code, commentEnd),
|
|
147
|
+
);
|
|
202
148
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
149
|
+
for (
|
|
150
|
+
let mut_open = code.indexOf('/*');
|
|
151
|
+
mut_open !== -1;
|
|
152
|
+
mut_open = code.indexOf('/*', mut_open + 2)
|
|
153
|
+
) {
|
|
154
|
+
const prefixStart = skipWhitespace(code, mut_open + 2);
|
|
209
155
|
|
|
156
|
+
if (!code.startsWith(prefix, prefixStart)) {
|
|
210
157
|
continue;
|
|
211
158
|
}
|
|
212
159
|
|
|
213
|
-
|
|
160
|
+
const namesStart = skipWhitespace(code, prefixStart + prefix.length);
|
|
161
|
+
|
|
162
|
+
const commentEnd = findCommentEnd(namesStart);
|
|
163
|
+
|
|
164
|
+
// Nothing after this point is closed, so no later comment can match.
|
|
165
|
+
if (commentEnd === -1) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const namesEnd = Math.max(namesStart, trimEndBefore(commentEnd));
|
|
170
|
+
|
|
171
|
+
const lineTerminator = findLineTerminator(namesStart);
|
|
172
|
+
|
|
173
|
+
if (lineTerminator === -1 || lineTerminator >= namesEnd) {
|
|
174
|
+
const names = code.slice(namesStart, namesEnd);
|
|
175
|
+
|
|
176
|
+
// Empty means ignore all transformers; otherwise parse comma-separated
|
|
177
|
+
// transformer names
|
|
178
|
+
return names === '' ? [] : names.split(',').map((name) => name.trim());
|
|
179
|
+
}
|
|
214
180
|
}
|
|
215
181
|
|
|
216
|
-
return
|
|
182
|
+
return undefined;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Finds the file-level ignore comment `/* <prefix> name, name *\/` and returns
|
|
187
|
+
* the transformer names it lists (empty for all transformers), or `undefined`
|
|
188
|
+
* when the file has none. Whitespace, line breaks included, may surround the
|
|
189
|
+
* prefix and the names, but the names themselves must stay on one line. The
|
|
190
|
+
* prefixes are tried in order, each against the whole file.
|
|
191
|
+
*
|
|
192
|
+
* The scan runs in time linear in the length of `code`, whatever it contains.
|
|
193
|
+
*/
|
|
194
|
+
const findFileIgnoreTransformers = (
|
|
195
|
+
code: string,
|
|
196
|
+
): readonly string[] | undefined => {
|
|
197
|
+
for (const prefix of fileIgnorePrefixes) {
|
|
198
|
+
const names = findFileIgnoreComment(code, prefix);
|
|
199
|
+
|
|
200
|
+
if (names !== undefined) {
|
|
201
|
+
return names;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return undefined;
|
|
217
206
|
};
|
|
207
|
+
|
|
208
|
+
const shouldSkipFile = (
|
|
209
|
+
ignoredTransformers: readonly string[] | undefined,
|
|
210
|
+
transformerName: string,
|
|
211
|
+
): boolean =>
|
|
212
|
+
ignoredTransformers !== undefined &&
|
|
213
|
+
// Empty array means ignore all transformers (file-level ignore without
|
|
214
|
+
// specific transformers); otherwise check if the transformer is in the ignore
|
|
215
|
+
// list
|
|
216
|
+
(Arr.isEmpty(ignoredTransformers) ||
|
|
217
|
+
ignoredTransformers.includes(transformerName));
|
|
@@ -9,6 +9,12 @@ import { convertToReadonlyTransformer } from './convert-to-readonly.mjs';
|
|
|
9
9
|
import { replaceAnyWithUnknownTransformer } from './replace-any-with-unknown.mjs';
|
|
10
10
|
import { transformSourceCode } from './transform-source-code.mjs';
|
|
11
11
|
|
|
12
|
+
const formatter = (code: string): Promise<string> =>
|
|
13
|
+
prettier.format(code, {
|
|
14
|
+
parser: 'typescript',
|
|
15
|
+
plugins: [prettierPluginTypeScript, prettierPluginEstree],
|
|
16
|
+
});
|
|
17
|
+
|
|
12
18
|
const testFn = async ({
|
|
13
19
|
source,
|
|
14
20
|
expected,
|
|
@@ -39,12 +45,6 @@ const testFn = async ({
|
|
|
39
45
|
expect(transformed).toBe(expectedFormatted);
|
|
40
46
|
};
|
|
41
47
|
|
|
42
|
-
const formatter = (code: string): Promise<string> =>
|
|
43
|
-
prettier.format(code, {
|
|
44
|
-
parser: 'typescript',
|
|
45
|
-
plugins: [prettierPluginTypeScript, prettierPluginEstree],
|
|
46
|
-
});
|
|
47
|
-
|
|
48
48
|
describe('Transformer-specific ignore comments (Integration)', () => {
|
|
49
49
|
test.each([
|
|
50
50
|
{
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The default `mutableVariablePrefixes` and `mutablePropertyPrefixes` of the
|
|
3
|
-
* transformers: a name that starts with one of these is left mutable. A
|
|
4
|
-
*
|
|
3
|
+
* transformers: a name that starts with one of these is left mutable. A private
|
|
4
|
+
* name (`#mut_x`) is matched with or after its `#`.
|
|
5
5
|
*
|
|
6
|
-
* The ESLint counterpart is `
|
|
7
|
-
* `libs/eslint-config-typed/src/
|
|
8
|
-
*
|
|
6
|
+
* The ESLint counterpart is `mutablePrefix` in
|
|
7
|
+
* `libs/eslint-config-typed/src/constants/mutable-prefix.mts`, from which
|
|
8
|
+
* `eslint-functional-rules.mts` there builds its name and property patterns;
|
|
9
9
|
* `libs/eslint-config-typed/src/rules/eslint-functional-rules.test.mts` fails
|
|
10
10
|
* when the two differ; it reads this package's `dist/`, so build first.
|
|
11
11
|
*/
|
|
@@ -15,9 +15,9 @@ export const DEFAULT_MUTABLE_PREFIXES: readonly string[] = Object.freeze([
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* The default `mutableTypePrefixes` of the transformers: a type alias, an
|
|
18
|
-
* interface or a class whose name starts with one of these is left mutable,
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* interface or a class whose name starts with one of these is left mutable, and
|
|
19
|
+
* `append-as-const` adds no `as const` where the value has to be assignable to
|
|
20
|
+
* it (an annotated variable, `satisfies`, an arrow's return).
|
|
21
21
|
* `Mutable` suits the PascalCase of type names; `mut_` is accepted as for
|
|
22
22
|
* variables. No ESLint rule reads type names, so there is no counterpart to
|
|
23
23
|
* keep in step.
|