wrec 0.29.2 → 0.29.4
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/{wrec-DHp2V7DA.js → wrec-DEac2MyH.js} +74 -67
- package/dist/wrec-ssr.d.ts +1 -0
- package/dist/wrec-ssr.es.js +1 -1
- package/dist/wrec.d.ts +1 -0
- package/dist/wrec.es.js +1 -1
- package/package.json +2 -2
- package/scripts/lint.js +8 -16
- package/scripts/used-by.js +625 -431
package/scripts/used-by.js
CHANGED
|
@@ -1,143 +1,388 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// This script inspects a given Wrec component source file and
|
|
3
|
+
// determines the proper values for property config `usedBy` properties.
|
|
4
|
+
// Each value is a list of methods that use the property
|
|
5
|
+
// or a single method name.
|
|
6
|
+
//
|
|
7
|
+
// This uses the TypeScript compiler to parse the source file,
|
|
8
|
+
// discover which expressions call which methods,
|
|
9
|
+
// trace property usage through those call chains, and
|
|
10
|
+
// output or update the `usedBy` properties`.
|
|
11
|
+
//
|
|
12
|
+
// To run this, enter `npx wrec-usedby [--dry] [file-path]`
|
|
13
|
+
// If no file-path is specified, the script runs on
|
|
14
|
+
// all .js and .ts files in and below the current directory.
|
|
15
|
+
//
|
|
16
|
+
// Include the --dry flag for a dry run where `usedBy` values are output,
|
|
17
|
+
// but no files are modified.
|
|
2
18
|
|
|
3
19
|
import fs from 'node:fs';
|
|
4
20
|
import path from 'node:path';
|
|
5
21
|
import ts from 'typescript';
|
|
6
22
|
|
|
7
|
-
const
|
|
8
|
-
const dry = args.includes('--dry');
|
|
9
|
-
const verbose = args.includes('--verbose');
|
|
10
|
-
const inputPaths = args.filter(arg => !arg.startsWith('--'));
|
|
23
|
+
const cwd = process.cwd();
|
|
11
24
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
25
|
+
// Records property names introduced by
|
|
26
|
+
// an identifier or object binding pattern.
|
|
27
|
+
// An object binding pattern is destructuring syntax
|
|
28
|
+
// used in a binding position, like a variable declaration,
|
|
29
|
+
// parameter list, or assignment target.
|
|
30
|
+
function addBindingName(props, name) {
|
|
31
|
+
if (ts.isIdentifier(name)) {
|
|
32
|
+
props.add(name.text);
|
|
33
|
+
} else if (ts.isObjectBindingPattern(name)) {
|
|
34
|
+
addObjectBindingProps(props, name);
|
|
35
|
+
}
|
|
15
36
|
}
|
|
16
37
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
38
|
+
// Adds property names referenced by an object binding pattern.
|
|
39
|
+
function addObjectBindingProps(props, bindingPattern) {
|
|
40
|
+
for (const element of bindingPattern.elements) {
|
|
41
|
+
if (element.dotDotDotToken) continue;
|
|
21
42
|
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
43
|
+
if (element.propertyName) {
|
|
44
|
+
const name = getNameText(element.propertyName);
|
|
45
|
+
if (name) props.add(name);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
addBindingName(props, element.name);
|
|
50
|
+
}
|
|
27
51
|
}
|
|
28
52
|
|
|
29
|
-
|
|
30
|
-
|
|
53
|
+
// Analyzes a parsed source file and returns any proposed `usedBy` edits.
|
|
54
|
+
// This is the heart of the functionality in this script.
|
|
55
|
+
function analyzeSourceFile(sourceFile) {
|
|
56
|
+
const edits = [];
|
|
57
|
+
const {names: wrecNames, quote} = getWrecImportInfo(sourceFile);
|
|
58
|
+
const suggestions = [];
|
|
59
|
+
let foundWrecSubclass = false;
|
|
31
60
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
61
|
+
// Find the statement that defines a class that extends Wrec.
|
|
62
|
+
for (const node of sourceFile.statements) {
|
|
63
|
+
if (!ts.isClassDeclaration(node) || !extendsWrec(node, wrecNames)) continue;
|
|
64
|
+
foundWrecSubclass = true;
|
|
37
65
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
66
|
+
let propertiesObject = null;
|
|
67
|
+
// Find the static property named "properties".
|
|
68
|
+
for (const member of node.members) {
|
|
69
|
+
if (
|
|
70
|
+
ts.isPropertyDeclaration(member) &&
|
|
71
|
+
hasStaticModifier(member) &&
|
|
72
|
+
getNameText(member.name) === 'properties' &&
|
|
73
|
+
member.initializer &&
|
|
74
|
+
ts.isObjectLiteralExpression(member.initializer)
|
|
75
|
+
) {
|
|
76
|
+
propertiesObject = member.initializer;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
43
80
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
81
|
+
// Bail out if no static property named "properties" was found.
|
|
82
|
+
if (!propertiesObject) continue;
|
|
83
|
+
|
|
84
|
+
// Get a Set of the defined property names.
|
|
85
|
+
const propertyNames = new Set(
|
|
86
|
+
propertiesObject.properties
|
|
87
|
+
.filter(ts.isPropertyAssignment)
|
|
88
|
+
.map(property => getNameText(property.name))
|
|
89
|
+
.filter(name => name !== null)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Get a map where the keys are property names and
|
|
93
|
+
// the values are Sets of public methods that use it transitively.
|
|
94
|
+
const propToMethods = getMethodUsages(node, propertyNames);
|
|
95
|
+
|
|
96
|
+
// For each member that represents a component property ...
|
|
97
|
+
for (const member of propertiesObject.properties) {
|
|
98
|
+
// Skip the member if not a property assignment.
|
|
99
|
+
if (!ts.isPropertyAssignment(member)) continue;
|
|
100
|
+
|
|
101
|
+
// Skip the member if we can't gets its name
|
|
102
|
+
// or if its value isn't an object literal.
|
|
103
|
+
const propName = getNameText(member.name);
|
|
104
|
+
if (!propName || !ts.isObjectLiteralExpression(member.initializer))
|
|
105
|
+
continue;
|
|
106
|
+
|
|
107
|
+
// Convert the Set of methods that use the property into a sorted array.
|
|
108
|
+
const methodNames = [
|
|
109
|
+
...(propToMethods.get(propName) ?? new Set())
|
|
110
|
+
].sort();
|
|
49
111
|
|
|
50
|
-
|
|
51
|
-
|
|
112
|
+
// Get an array of all the NodeObjects that represent
|
|
113
|
+
// properties in the configuration object, except the one for "usedBy".
|
|
114
|
+
const configObject = member.initializer;
|
|
115
|
+
const existingMembers = configObject.properties.filter(
|
|
116
|
+
property =>
|
|
117
|
+
!(
|
|
118
|
+
ts.isPropertyAssignment(property) &&
|
|
119
|
+
getNameText(property.name) === 'usedBy'
|
|
120
|
+
)
|
|
121
|
+
);
|
|
52
122
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
123
|
+
// If the property is used by any methods ...
|
|
124
|
+
if (methodNames.length > 0) {
|
|
125
|
+
suggestions.push({
|
|
126
|
+
propName,
|
|
127
|
+
suggestion: createUsedByProperty(methodNames, quote)
|
|
128
|
+
});
|
|
129
|
+
} else {
|
|
130
|
+
// Determine if the configuration object already had a "usedBy" property.
|
|
131
|
+
const hadUsedBy =
|
|
132
|
+
existingMembers.length !== configObject.properties.length;
|
|
133
|
+
if (hadUsedBy) {
|
|
134
|
+
suggestions.push({propName, suggestion: 'remove usedBy'});
|
|
135
|
+
} else {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Build the new text for the property configuration object.
|
|
141
|
+
const nextText = buildConfigText(sourceFile, member, methodNames, quote);
|
|
142
|
+
// Get the existing text for the property configuration object.
|
|
143
|
+
const currentText = sourceFile.text.slice(
|
|
144
|
+
configObject.getStart(sourceFile),
|
|
145
|
+
configObject.end
|
|
146
|
+
);
|
|
147
|
+
// If they differ, add an object describing
|
|
148
|
+
// the desired edit to the edits array.
|
|
149
|
+
if (nextText !== currentText) {
|
|
150
|
+
edits.push({
|
|
151
|
+
start: configObject.getStart(sourceFile),
|
|
152
|
+
end: configObject.end,
|
|
153
|
+
propName,
|
|
154
|
+
oldText: currentText,
|
|
155
|
+
text: nextText
|
|
156
|
+
});
|
|
157
|
+
}
|
|
57
158
|
}
|
|
58
|
-
return files;
|
|
59
159
|
}
|
|
60
160
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
!entry.name.includes('.test.')
|
|
71
|
-
) {
|
|
72
|
-
files.push(fullPath);
|
|
73
|
-
}
|
|
161
|
+
// If we didn't find a Wrec subclass, then there are no changes to make.
|
|
162
|
+
if (!foundWrecSubclass) {
|
|
163
|
+
return {
|
|
164
|
+
changed: false,
|
|
165
|
+
edits: [],
|
|
166
|
+
foundWrecSubclass: false,
|
|
167
|
+
suggestions,
|
|
168
|
+
text: sourceFile.text
|
|
169
|
+
};
|
|
74
170
|
}
|
|
75
171
|
|
|
76
|
-
|
|
77
|
-
|
|
172
|
+
// If we found a Wrec subclass,
|
|
173
|
+
// but no edits for usedBy properties are needed ...
|
|
174
|
+
if (edits.length === 0) {
|
|
175
|
+
return {
|
|
176
|
+
changed: false,
|
|
177
|
+
edits: [],
|
|
178
|
+
foundWrecSubclass: true,
|
|
179
|
+
suggestions,
|
|
180
|
+
text: sourceFile.text
|
|
181
|
+
};
|
|
182
|
+
}
|
|
78
183
|
|
|
79
|
-
|
|
184
|
+
// Get the current text in the source file.
|
|
185
|
+
let nextSource = sourceFile.text;
|
|
80
186
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
187
|
+
// Sort the edits to be made from last to first.
|
|
188
|
+
edits.sort((a, b) => b.start - a.start);
|
|
189
|
+
|
|
190
|
+
// Make each of the edits to the source file.
|
|
191
|
+
for (const edit of edits) {
|
|
192
|
+
nextSource =
|
|
193
|
+
nextSource.slice(0, edit.start) + edit.text + nextSource.slice(edit.end);
|
|
88
194
|
}
|
|
89
|
-
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
changed: true,
|
|
198
|
+
edits,
|
|
199
|
+
foundWrecSubclass: true,
|
|
200
|
+
suggestions,
|
|
201
|
+
text: nextSource
|
|
202
|
+
};
|
|
90
203
|
}
|
|
91
204
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
205
|
+
// Builds a new property config object
|
|
206
|
+
// with an updated `usedBy` entry if needed.
|
|
207
|
+
// - member is an AST node for the initialization of a property
|
|
208
|
+
// inside the "static properties" object.
|
|
209
|
+
// - methodsNames is an array of the methods that use the property.
|
|
210
|
+
// - quote is either a single or double quote.
|
|
211
|
+
function buildConfigText(sourceFile, member, methodNames, quote) {
|
|
212
|
+
const configObject = member.initializer;
|
|
213
|
+
|
|
214
|
+
// Get an array of AST nodes for all the config object properties
|
|
215
|
+
// except `usedBy`.
|
|
216
|
+
const existingMembers = configObject.properties.filter(
|
|
217
|
+
property =>
|
|
218
|
+
!(
|
|
219
|
+
ts.isPropertyAssignment(property) &&
|
|
220
|
+
getNameText(property.name) === 'usedBy'
|
|
221
|
+
)
|
|
95
222
|
);
|
|
96
|
-
}
|
|
97
223
|
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
224
|
+
// Create property assignment strings from the AST nodes.
|
|
225
|
+
const {text} = sourceFile;
|
|
226
|
+
const propertyStrings = existingMembers.map(property =>
|
|
227
|
+
text.slice(property.getStart(sourceFile), property.end).trim()
|
|
228
|
+
);
|
|
101
229
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
) {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
230
|
+
// If this property is used by any methods,
|
|
231
|
+
// add a usedBy property that lists them.
|
|
232
|
+
if (methodNames.length > 0) {
|
|
233
|
+
propertyStrings.push(createUsedByProperty(methodNames, quote));
|
|
234
|
+
}
|
|
110
235
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
moduleName.endsWith('/wrec') ||
|
|
116
|
-
moduleName.endsWith('/wrec-ssr');
|
|
117
|
-
if (!isWrecModule) continue;
|
|
236
|
+
const existingPropertiesString = text.slice(
|
|
237
|
+
configObject.getStart(sourceFile),
|
|
238
|
+
configObject.end
|
|
239
|
+
);
|
|
118
240
|
|
|
119
|
-
|
|
120
|
-
|
|
241
|
+
const multiline = existingPropertiesString.includes('\n');
|
|
242
|
+
if (multiline) {
|
|
243
|
+
// Build and return a multi-line config object string
|
|
244
|
+
// that preserves the existing indentation.
|
|
245
|
+
const memberIndent = getIndent(text, member.getStart(sourceFile));
|
|
246
|
+
const [firstMember] = existingMembers;
|
|
247
|
+
const propertyIndent = firstMember
|
|
248
|
+
? getIndent(text, firstMember.getStart(sourceFile))
|
|
249
|
+
: memberIndent + ' ';
|
|
250
|
+
const content = propertyStrings
|
|
251
|
+
.map(part => `${propertyIndent}${part}`)
|
|
252
|
+
.join(',\n');
|
|
253
|
+
return `{\n${content}\n${memberIndent}}`;
|
|
254
|
+
}
|
|
121
255
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
256
|
+
// Build and return a single line config object string
|
|
257
|
+
// that preserves the existing spacing around curly braces.
|
|
258
|
+
const openMatch = existingPropertiesString.match(/^\{(\s*)/);
|
|
259
|
+
const closeMatch = existingPropertiesString.match(/(\s*)\}$/);
|
|
260
|
+
const openSpacing = openMatch ? openMatch[1] : ' ';
|
|
261
|
+
const closeSpacing = closeMatch ? closeMatch[1] : ' ';
|
|
262
|
+
return `{${openSpacing}${propertyStrings.join(', ')}${closeSpacing}}`;
|
|
263
|
+
}
|
|
126
264
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
265
|
+
// Walks a method body to collect component property reads
|
|
266
|
+
// and method calls made through `this`.
|
|
267
|
+
// This is only called by getMethodUsages.
|
|
268
|
+
function collectMethodBodyUsage(node, props, calledMethods) {
|
|
269
|
+
if (
|
|
270
|
+
ts.isPropertyAccessExpression(node) &&
|
|
271
|
+
node.expression.kind === ts.SyntaxKind.ThisKeyword
|
|
272
|
+
) {
|
|
273
|
+
// Handles direct property access like `this.foo`
|
|
274
|
+
// and records method calls like `this.foo()`.
|
|
275
|
+
recordThisAccess(props, calledMethods, node, node.name.text);
|
|
276
|
+
} else if (
|
|
277
|
+
ts.isElementAccessExpression(node) &&
|
|
278
|
+
node.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
279
|
+
node.argumentExpression &&
|
|
280
|
+
ts.isStringLiteralLike(node.argumentExpression)
|
|
281
|
+
) {
|
|
282
|
+
// Handles string-based element access like `this['foo']`
|
|
283
|
+
// and records method calls like `this['foo']()`.
|
|
284
|
+
recordThisAccess(props, calledMethods, node, node.argumentExpression.text);
|
|
285
|
+
} else if (
|
|
286
|
+
ts.isVariableDeclaration(node) &&
|
|
287
|
+
node.initializer &&
|
|
288
|
+
node.initializer.kind === ts.SyntaxKind.ThisKeyword
|
|
289
|
+
) {
|
|
290
|
+
// Handles variable declarations initialized from `this`,
|
|
291
|
+
// such as `const {foo} = this` or `const self = this`.
|
|
292
|
+
addBindingName(props, node.name);
|
|
293
|
+
} else if (
|
|
294
|
+
ts.isBinaryExpression(node) &&
|
|
295
|
+
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
296
|
+
node.right.kind === ts.SyntaxKind.ThisKeyword
|
|
297
|
+
) {
|
|
298
|
+
// Handles assignments from `this`, such as destructuring
|
|
299
|
+
// or object-literal patterns that pull named properties from it.
|
|
300
|
+
if (ts.isObjectLiteralExpression(node.left)) {
|
|
301
|
+
for (const property of node.left.properties) {
|
|
302
|
+
if (
|
|
303
|
+
ts.isShorthandPropertyAssignment(property) ||
|
|
304
|
+
ts.isPropertyAssignment(property)
|
|
305
|
+
) {
|
|
306
|
+
const name = getNameText(property.name);
|
|
307
|
+
if (name) props.add(name);
|
|
130
308
|
}
|
|
131
309
|
}
|
|
310
|
+
} else if (ts.isObjectBindingPattern(node.left)) {
|
|
311
|
+
addObjectBindingProps(props, node.left);
|
|
132
312
|
}
|
|
133
313
|
}
|
|
134
314
|
|
|
135
|
-
|
|
315
|
+
ts.forEachChild(node, child =>
|
|
316
|
+
collectMethodBodyUsage(child, props, calledMethods)
|
|
317
|
+
);
|
|
136
318
|
}
|
|
137
319
|
|
|
138
|
-
|
|
320
|
+
// Returns a string for a usedBy property.
|
|
321
|
+
function createUsedByProperty(methodNames, quote) {
|
|
322
|
+
const value =
|
|
323
|
+
methodNames.length === 1
|
|
324
|
+
? `${quote}${methodNames[0]}${quote}`
|
|
325
|
+
: `[${methodNames.map(name => `${quote}${name}${quote}`).join(', ')}]`;
|
|
326
|
+
return `usedBy: ${value}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Determines what changes, if any, should be made in
|
|
330
|
+
// the usedBy properties in property configuration objects
|
|
331
|
+
// found in the file at a given relative path.
|
|
332
|
+
export function evaluateSourceFile(filePath, options = {}) {
|
|
333
|
+
const {dry = false} = options;
|
|
334
|
+
const absFilePath = path.resolve(cwd, filePath);
|
|
335
|
+
validateFile(absFilePath);
|
|
336
|
+
|
|
337
|
+
// The file is read in this function instead of in evaluateSourceText
|
|
338
|
+
// so unit tests can pass hard-coded text to that function.
|
|
339
|
+
const text = fs.readFileSync(absFilePath, 'utf8');
|
|
340
|
+
let {
|
|
341
|
+
changed,
|
|
342
|
+
foundWrecSubclass,
|
|
343
|
+
suggestions,
|
|
344
|
+
text: nextText
|
|
345
|
+
} = evaluateSourceText(absFilePath, text);
|
|
346
|
+
|
|
347
|
+
// If we didn't find the definition of a class that extends Wrec ...
|
|
348
|
+
if (!foundWrecSubclass) {
|
|
349
|
+
throw new Error('No class extending Wrec was found.');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// If this isn't a dry run and changes were made,
|
|
353
|
+
// write the modified source code back to the file.
|
|
354
|
+
if (!dry && changed) {
|
|
355
|
+
fs.writeFileSync(absFilePath, nextText);
|
|
356
|
+
suggestions = []; // all the suggestions have been applied
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {changed, foundWrecSubclass, suggestions, text: nextText};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Determines what changes, if any, should be made in
|
|
363
|
+
// the usedBy properties in property configuration objects
|
|
364
|
+
// found in source file text.
|
|
365
|
+
// This function was factored out of evaluateSourceFile
|
|
366
|
+
// to support unit tests.
|
|
367
|
+
export function evaluateSourceText(filePath, text) {
|
|
368
|
+
const scriptKind = filePath.endsWith('.ts')
|
|
369
|
+
? ts.ScriptKind.TS
|
|
370
|
+
: ts.ScriptKind.JS;
|
|
371
|
+
const sourceFile = ts.createSourceFile(
|
|
372
|
+
filePath,
|
|
373
|
+
text,
|
|
374
|
+
ts.ScriptTarget.Latest,
|
|
375
|
+
true,
|
|
376
|
+
scriptKind
|
|
377
|
+
);
|
|
378
|
+
return analyzeSourceFile(sourceFile);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Determines whether a class declaration extends one of the known Wrec imports.
|
|
382
|
+
// This is only called by analyzeSourceFile.
|
|
383
|
+
function extendsWrec(classNode, wrecNames) {
|
|
139
384
|
return Boolean(
|
|
140
|
-
|
|
385
|
+
classNode.heritageClauses?.some(
|
|
141
386
|
clause =>
|
|
142
387
|
clause.token === ts.SyntaxKind.ExtendsKeyword &&
|
|
143
388
|
clause.types.some(
|
|
@@ -150,87 +395,103 @@ function extendsWrec(node, wrecNames) {
|
|
|
150
395
|
);
|
|
151
396
|
}
|
|
152
397
|
|
|
153
|
-
|
|
398
|
+
// Gets the names of all methods that are called from
|
|
399
|
+
// JavaScript expressions in the component's static CSS template.
|
|
400
|
+
// This is only called by getMethodUsages.
|
|
401
|
+
function getCssCalledMethods(classNode) {
|
|
154
402
|
const methodNames = new Set();
|
|
155
403
|
const CALL_RE = /this\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
|
|
156
|
-
|
|
157
|
-
function visit(node) {
|
|
158
|
-
if (
|
|
159
|
-
ts.isPropertyAccessExpression(node) &&
|
|
160
|
-
node.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
161
|
-
ts.isCallExpression(node.parent) &&
|
|
162
|
-
node.parent.expression === node
|
|
163
|
-
) {
|
|
164
|
-
methodNames.add(node.name.text);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
ts.forEachChild(node, visit);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function addTemplateTextMethods(template) {
|
|
171
|
-
const text = template.getText();
|
|
172
|
-
for (const match of text.matchAll(CALL_RE)) {
|
|
173
|
-
methodNames.add(match[1]);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
404
|
+
let template;
|
|
176
405
|
|
|
177
406
|
for (const member of classNode.members) {
|
|
407
|
+
// If this member isn't the AST node for "static css = ..."
|
|
408
|
+
// then skip it.
|
|
178
409
|
if (
|
|
179
410
|
ts.isPropertyDeclaration(member) &&
|
|
180
411
|
hasStaticModifier(member) &&
|
|
181
|
-
getNameText(member.name) === '
|
|
182
|
-
member.initializer
|
|
412
|
+
getNameText(member.name) === 'css' &&
|
|
413
|
+
member.initializer &&
|
|
414
|
+
ts.isTaggedTemplateExpression(member.initializer) &&
|
|
415
|
+
ts.isIdentifier(member.initializer.tag) &&
|
|
416
|
+
member.initializer.tag.text === 'css'
|
|
183
417
|
) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
) {
|
|
189
|
-
addTemplateTextMethods(member.initializer.template);
|
|
190
|
-
}
|
|
191
|
-
ts.forEachChild(member.initializer, visit);
|
|
418
|
+
// Keep the last matching declaration because
|
|
419
|
+
// JavaScript class fields use last-one-wins semantics
|
|
420
|
+
// when the same static field is declared twice.
|
|
421
|
+
template = member.initializer.template;
|
|
192
422
|
}
|
|
193
423
|
}
|
|
194
424
|
|
|
425
|
+
// If no matching declaration was found, return an empty Set.
|
|
426
|
+
if (!template) return methodNames;
|
|
427
|
+
|
|
428
|
+
// Finds all method names called in CSS property value expressions
|
|
429
|
+
// matching `this.method()`.
|
|
430
|
+
const text = template.getText();
|
|
431
|
+
for (const match of text.matchAll(CALL_RE)) {
|
|
432
|
+
methodNames.add(match[1]);
|
|
433
|
+
}
|
|
434
|
+
|
|
195
435
|
return methodNames;
|
|
196
436
|
}
|
|
197
437
|
|
|
438
|
+
// Gets the names of all methods that are called from
|
|
439
|
+
// the "computed" JavaScript expression of an component property.
|
|
440
|
+
// This is only called by getMethodUsages.
|
|
198
441
|
function getComputedCalledMethods(classNode) {
|
|
199
442
|
const methodNames = new Set();
|
|
200
443
|
const CALL_RE = /this\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
|
|
444
|
+
let propertiesNode;
|
|
201
445
|
|
|
202
446
|
for (const member of classNode.members) {
|
|
447
|
+
// If this member isn't the AST node for "static properties = ..."
|
|
448
|
+
// then skip it.
|
|
203
449
|
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
getNameText(member.name)
|
|
207
|
-
|
|
208
|
-
|
|
450
|
+
ts.isPropertyDeclaration(member) &&
|
|
451
|
+
hasStaticModifier(member) &&
|
|
452
|
+
getNameText(member.name) === 'properties' &&
|
|
453
|
+
member.initializer &&
|
|
454
|
+
ts.isObjectLiteralExpression(member.initializer)
|
|
455
|
+
) {
|
|
456
|
+
// Keep the last matching declaration because
|
|
457
|
+
// JavaScript class fields use last-one-wins semantics
|
|
458
|
+
// when the same static field is declared twice.
|
|
459
|
+
propertiesNode = member.initializer;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// If no matching declaration was found, return an empty Set.
|
|
464
|
+
if (!propertiesNode) return methodNames;
|
|
465
|
+
|
|
466
|
+
// For each property in the last "static properties" object ...
|
|
467
|
+
for (const property of propertiesNode.properties) {
|
|
468
|
+
// If it isn't a property assignment or
|
|
469
|
+
// the property value isn't an object literal then skip it.
|
|
470
|
+
if (
|
|
471
|
+
!ts.isPropertyAssignment(property) ||
|
|
472
|
+
!ts.isObjectLiteralExpression(property.initializer)
|
|
209
473
|
) {
|
|
210
474
|
continue;
|
|
211
475
|
}
|
|
212
476
|
|
|
213
|
-
|
|
477
|
+
// For each property in the configuration object ...
|
|
478
|
+
for (const configProperty of property.initializer.properties) {
|
|
479
|
+
// If it isn't a property assignment or
|
|
480
|
+
// the property name isn't "computed" then skip it.
|
|
214
481
|
if (
|
|
215
|
-
!ts.isPropertyAssignment(
|
|
216
|
-
|
|
482
|
+
!ts.isPropertyAssignment(configProperty) ||
|
|
483
|
+
getNameText(configProperty.name) !== 'computed'
|
|
217
484
|
) {
|
|
218
485
|
continue;
|
|
219
486
|
}
|
|
220
487
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
!ts.isPropertyAssignment(configProperty) ||
|
|
224
|
-
getNameText(configProperty.name) !== 'computed'
|
|
225
|
-
) {
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
488
|
+
// If the property value isn't a string then skip it.
|
|
489
|
+
if (!ts.isStringLiteralLike(configProperty.initializer)) continue;
|
|
228
490
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
491
|
+
// Find all the method calls in the string JavaScript expression.
|
|
492
|
+
const computed = configProperty.initializer.text;
|
|
493
|
+
for (const match of computed.matchAll(CALL_RE)) {
|
|
494
|
+
methodNames.add(match[1]);
|
|
234
495
|
}
|
|
235
496
|
}
|
|
236
497
|
}
|
|
@@ -238,149 +499,53 @@ function getComputedCalledMethods(classNode) {
|
|
|
238
499
|
return methodNames;
|
|
239
500
|
}
|
|
240
501
|
|
|
502
|
+
// Returns the leading indentation in the line
|
|
503
|
+
// that begins at a given position (`pos`) inside `text`.
|
|
504
|
+
function getIndent(text, pos) {
|
|
505
|
+
const lineStart = text.lastIndexOf('\n', pos - 1) + 1;
|
|
506
|
+
const match = /^[ \t]*/.exec(text.slice(lineStart));
|
|
507
|
+
return match ? match[0] : '';
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Returns a map where the keys are property names and
|
|
511
|
+
// the values are Sets of public methods that use it transitively.
|
|
241
512
|
function getMethodUsages(classNode, propertyNames) {
|
|
242
513
|
const methodInfo = new Map();
|
|
243
|
-
for (const member of classNode.members) {
|
|
244
|
-
if (hasStaticModifier(member)) continue;
|
|
245
|
-
|
|
246
|
-
if (
|
|
247
|
-
(ts.isMethodDeclaration(member) ||
|
|
248
|
-
ts.isGetAccessorDeclaration(member) ||
|
|
249
|
-
ts.isSetAccessorDeclaration(member)) &&
|
|
250
|
-
member.body
|
|
251
|
-
) {
|
|
252
|
-
const methodName = getNameText(member.name);
|
|
253
|
-
if (!methodName) continue;
|
|
254
|
-
|
|
255
|
-
const props = new Set();
|
|
256
|
-
const calledMethods = new Set();
|
|
257
|
-
const isPrivate = ts.isPrivateIdentifier(member.name);
|
|
258
|
-
|
|
259
|
-
function addBindingName(name) {
|
|
260
|
-
if (ts.isIdentifier(name)) {
|
|
261
|
-
props.add(name.text);
|
|
262
|
-
} else if (ts.isObjectBindingPattern(name)) {
|
|
263
|
-
addObjectBindingProps(name);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function addObjectBindingProps(bindingPattern) {
|
|
268
|
-
for (const element of bindingPattern.elements) {
|
|
269
|
-
if (element.dotDotDotToken) continue;
|
|
270
|
-
|
|
271
|
-
if (element.propertyName) {
|
|
272
|
-
const name = getNameText(element.propertyName);
|
|
273
|
-
if (name) props.add(name);
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (ts.isIdentifier(element.name)) {
|
|
278
|
-
props.add(element.name.text);
|
|
279
|
-
} else if (ts.isObjectBindingPattern(element.name)) {
|
|
280
|
-
addObjectBindingProps(element.name);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function visit(child) {
|
|
286
|
-
if (
|
|
287
|
-
ts.isPropertyAccessExpression(child) &&
|
|
288
|
-
child.expression.kind === ts.SyntaxKind.ThisKeyword
|
|
289
|
-
) {
|
|
290
|
-
const name = child.name.text;
|
|
291
|
-
props.add(name);
|
|
292
|
-
if (
|
|
293
|
-
ts.isCallExpression(child.parent) &&
|
|
294
|
-
child.parent.expression === child
|
|
295
|
-
) {
|
|
296
|
-
calledMethods.add(name);
|
|
297
|
-
}
|
|
298
|
-
} else if (
|
|
299
|
-
ts.isElementAccessExpression(child) &&
|
|
300
|
-
child.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
301
|
-
child.argumentExpression &&
|
|
302
|
-
ts.isStringLiteralLike(child.argumentExpression)
|
|
303
|
-
) {
|
|
304
|
-
const name = child.argumentExpression.text;
|
|
305
|
-
props.add(name);
|
|
306
|
-
if (
|
|
307
|
-
ts.isCallExpression(child.parent) &&
|
|
308
|
-
child.parent.expression === child
|
|
309
|
-
) {
|
|
310
|
-
calledMethods.add(name);
|
|
311
|
-
}
|
|
312
|
-
} else if (
|
|
313
|
-
ts.isVariableDeclaration(child) &&
|
|
314
|
-
child.initializer &&
|
|
315
|
-
child.initializer.kind === ts.SyntaxKind.ThisKeyword
|
|
316
|
-
) {
|
|
317
|
-
if (ts.isObjectBindingPattern(child.name)) {
|
|
318
|
-
addObjectBindingProps(child.name);
|
|
319
|
-
} else {
|
|
320
|
-
addBindingName(child.name);
|
|
321
|
-
}
|
|
322
|
-
} else if (
|
|
323
|
-
ts.isBinaryExpression(child) &&
|
|
324
|
-
child.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
325
|
-
child.right.kind === ts.SyntaxKind.ThisKeyword
|
|
326
|
-
) {
|
|
327
|
-
if (ts.isObjectLiteralExpression(child.left)) {
|
|
328
|
-
for (const property of child.left.properties) {
|
|
329
|
-
if (
|
|
330
|
-
ts.isShorthandPropertyAssignment(property) ||
|
|
331
|
-
ts.isPropertyAssignment(property)
|
|
332
|
-
) {
|
|
333
|
-
const name = getNameText(property.name);
|
|
334
|
-
if (name) props.add(name);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
} else if (ts.isObjectBindingPattern(child.left)) {
|
|
338
|
-
addObjectBindingProps(child.left);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
ts.forEachChild(child, visit);
|
|
343
|
-
}
|
|
344
514
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
515
|
+
for (const member of classNode.members) {
|
|
516
|
+
// If the member doesn't represent an instance method, skip it.
|
|
517
|
+
if (!isInstanceMethodMember(member)) continue;
|
|
518
|
+
|
|
519
|
+
// If the member doesn't have a string name, skip it.
|
|
520
|
+
const methodName = getNameText(member.name);
|
|
521
|
+
if (!methodName) continue;
|
|
522
|
+
|
|
523
|
+
const props = new Set();
|
|
524
|
+
const calledMethods = new Set();
|
|
525
|
+
collectMethodBodyUsage(member.body, props, calledMethods);
|
|
526
|
+
methodInfo.set(methodName, {
|
|
527
|
+
calledMethods,
|
|
528
|
+
isPrivate: ts.isPrivateIdentifier(member.name),
|
|
529
|
+
props
|
|
530
|
+
});
|
|
348
531
|
}
|
|
349
532
|
|
|
533
|
+
// Build a Set of method names that are called from
|
|
534
|
+
// the "static html" template, the "static css" template,
|
|
535
|
+
// or computed property expressions.
|
|
350
536
|
const entryMethods = new Set([
|
|
537
|
+
...getCssCalledMethods(classNode),
|
|
351
538
|
...getTemplateCalledMethods(classNode),
|
|
352
539
|
...getComputedCalledMethods(classNode)
|
|
353
540
|
]);
|
|
354
541
|
const memo = new Map();
|
|
355
542
|
|
|
356
|
-
function getTransitiveProps(methodName, seen = new Set()) {
|
|
357
|
-
if (memo.has(methodName)) return memo.get(methodName);
|
|
358
|
-
if (seen.has(methodName)) return new Set();
|
|
359
|
-
|
|
360
|
-
seen.add(methodName);
|
|
361
|
-
const info = methodInfo.get(methodName);
|
|
362
|
-
const props = new Set(info?.props ?? []);
|
|
363
|
-
|
|
364
|
-
if (info) {
|
|
365
|
-
for (const calledMethod of info.calledMethods) {
|
|
366
|
-
const calledProps = getTransitiveProps(calledMethod, seen);
|
|
367
|
-
for (const propName of calledProps) props.add(propName);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
seen.delete(methodName);
|
|
372
|
-
memo.set(methodName, props);
|
|
373
|
-
return props;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
543
|
const propToMethods = new Map();
|
|
377
544
|
for (const methodName of entryMethods) {
|
|
378
545
|
const info = methodInfo.get(methodName);
|
|
379
546
|
if (!info || info.isPrivate) continue;
|
|
380
547
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
for (const propName of getTransitiveProps(methodName)) {
|
|
548
|
+
for (const propName of getTransitiveProps(methodInfo, memo, methodName)) {
|
|
384
549
|
if (!propertyNames.has(propName)) continue;
|
|
385
550
|
|
|
386
551
|
let methods = propToMethods.get(propName);
|
|
@@ -395,191 +560,220 @@ function getMethodUsages(classNode, propertyNames) {
|
|
|
395
560
|
return propToMethods;
|
|
396
561
|
}
|
|
397
562
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
563
|
+
// Gets the text value of an AST name node.
|
|
564
|
+
const getNameText = name =>
|
|
565
|
+
ts.isIdentifier(name) ||
|
|
566
|
+
ts.isStringLiteral(name) ||
|
|
567
|
+
ts.isPrivateIdentifier(name)
|
|
568
|
+
? name.text
|
|
569
|
+
: null;
|
|
570
|
+
|
|
571
|
+
// Gets the names of all methods that are called
|
|
572
|
+
// from the component's static HTML template.
|
|
573
|
+
// This is only called by getMethodUsages.
|
|
574
|
+
function getTemplateCalledMethods(classNode) {
|
|
575
|
+
// Find the last `static html = html\`...\`` declaration
|
|
576
|
+
// because duplicate static class fields use last-one-wins semantics.
|
|
577
|
+
let template;
|
|
578
|
+
for (const member of classNode.members) {
|
|
579
|
+
// If it's a "static html =" property assignment node ...
|
|
580
|
+
if (
|
|
581
|
+
ts.isPropertyDeclaration(member) &&
|
|
582
|
+
hasStaticModifier(member) &&
|
|
583
|
+
getNameText(member.name) === 'html' &&
|
|
584
|
+
member.initializer
|
|
585
|
+
) {
|
|
586
|
+
// If the value is a tagged template literal with the "html" tag ...
|
|
587
|
+
if (
|
|
588
|
+
ts.isTaggedTemplateExpression(member.initializer) &&
|
|
589
|
+
ts.isIdentifier(member.initializer.tag) &&
|
|
590
|
+
member.initializer.tag.text === 'html'
|
|
591
|
+
) {
|
|
592
|
+
template = member.initializer.template;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
401
595
|
}
|
|
402
|
-
return `usedBy: [${methodNames.map(name => `${quote}${name}${quote}`).join(', ')}]`;
|
|
403
|
-
}
|
|
404
596
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
597
|
+
const methodNames = new Set();
|
|
598
|
+
if (template) {
|
|
599
|
+
// Finds all method names called in the HTML template
|
|
600
|
+
// matching `this.method()`.
|
|
601
|
+
const text = template.getText();
|
|
602
|
+
const CALL_RE = /this\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
|
|
603
|
+
for (const match of text.matchAll(CALL_RE)) {
|
|
604
|
+
methodNames.add(match[1]);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return methodNames;
|
|
409
608
|
}
|
|
410
609
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
);
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
|
|
610
|
+
// Recursively follow method-call chains to accumulate all accessed properties.
|
|
611
|
+
// This is only called by getMethodUsages.
|
|
612
|
+
function getTransitiveProps(methodInfo, memo, methodName, seen = new Set()) {
|
|
613
|
+
// Starting from methods that are reachable from
|
|
614
|
+
// css/template/computed properties, walk through nested method calls
|
|
615
|
+
// and accumulate every component property touched along the way.
|
|
616
|
+
if (memo.has(methodName)) return memo.get(methodName);
|
|
617
|
+
if (seen.has(methodName)) return new Set();
|
|
618
|
+
|
|
619
|
+
seen.add(methodName);
|
|
620
|
+
const info = methodInfo.get(methodName);
|
|
621
|
+
const props = new Set(info?.props ?? []);
|
|
622
|
+
|
|
623
|
+
if (info) {
|
|
624
|
+
for (const calledMethod of info.calledMethods) {
|
|
625
|
+
const calledProps = getTransitiveProps(
|
|
626
|
+
methodInfo,
|
|
627
|
+
memo,
|
|
628
|
+
calledMethod,
|
|
629
|
+
seen
|
|
630
|
+
);
|
|
631
|
+
for (const propName of calledProps) props.add(propName);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
426
634
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
);
|
|
431
|
-
const multiline = original.includes('\n');
|
|
432
|
-
if (!multiline) return `{ ${existingTexts.join(', ')} }`;
|
|
433
|
-
|
|
434
|
-
const memberIndent = getIndent(text, member.getStart(sourceFile));
|
|
435
|
-
const firstExisting = existingMembers[0];
|
|
436
|
-
const innerIndent = firstExisting
|
|
437
|
-
? getIndent(text, firstExisting.getStart(sourceFile))
|
|
438
|
-
: memberIndent + ' ';
|
|
439
|
-
return `{\n${existingTexts.map(part => `${innerIndent}${part}`).join(',\n')}\n${memberIndent}}`;
|
|
635
|
+
seen.delete(methodName);
|
|
636
|
+
memo.set(methodName, props);
|
|
637
|
+
return props;
|
|
440
638
|
}
|
|
441
639
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
640
|
+
// Collects imported Wrec class names. For example,
|
|
641
|
+
// the following line would treat "MyWrec" as a class
|
|
642
|
+
// that can be extended in order to implement a wrec component:
|
|
643
|
+
// import { Wrec as MyWrec } from 'wrec`
|
|
644
|
+
// This also determines the quote character (single or double)
|
|
645
|
+
// used for those imports.
|
|
646
|
+
function getWrecImportInfo(sourceFile) {
|
|
647
|
+
// Start with the unaliased class name and a default quote style
|
|
648
|
+
// in case the file imports Wrec later or not at all.
|
|
649
|
+
const names = new Set(['Wrec']);
|
|
650
|
+
let quote = "'";
|
|
450
651
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
652
|
+
// Support aliased imports such as `import {Wrec as Base} from 'wrec'`
|
|
653
|
+
// so subclass detection still works.
|
|
654
|
+
for (const statement of sourceFile.statements) {
|
|
655
|
+
// Ignore anything that is not an import declaration
|
|
656
|
+
// with a string module path.
|
|
657
|
+
if (
|
|
658
|
+
!ts.isImportDeclaration(statement) ||
|
|
659
|
+
!statement.importClause ||
|
|
660
|
+
!ts.isStringLiteral(statement.moduleSpecifier)
|
|
661
|
+
) {
|
|
662
|
+
continue;
|
|
463
663
|
}
|
|
464
664
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
.filter(ts.isPropertyAssignment)
|
|
470
|
-
.map(property => getNameText(property.name))
|
|
471
|
-
.filter(Boolean)
|
|
472
|
-
);
|
|
473
|
-
|
|
474
|
-
const propToMethods = getMethodUsages(node, propertyNames);
|
|
475
|
-
for (const member of propertiesObject.properties) {
|
|
476
|
-
if (!ts.isPropertyAssignment(member)) continue;
|
|
665
|
+
// Only inspect imports that come from Wrec itself or its supported variants.
|
|
666
|
+
const moduleName = statement.moduleSpecifier.text;
|
|
667
|
+
const isWrecModule = moduleName === 'wrec' || moduleName.endsWith('/wrec');
|
|
668
|
+
if (!isWrecModule) continue;
|
|
477
669
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
670
|
+
// Skip default or namespace imports
|
|
671
|
+
// because we only care about named imports.
|
|
672
|
+
const namedBindings = statement.importClause.namedBindings;
|
|
673
|
+
if (!namedBindings || !ts.isNamedImports(namedBindings)) continue;
|
|
481
674
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
ts.isPropertyAssignment(property) &&
|
|
490
|
-
getNameText(property.name) === 'usedBy'
|
|
491
|
-
)
|
|
492
|
-
);
|
|
493
|
-
const hadUsedBy =
|
|
494
|
-
existingMembers.length !== configObject.properties.length;
|
|
495
|
-
const needsUsedBy = methodNames.length > 0;
|
|
496
|
-
if (!hadUsedBy && !needsUsedBy) continue;
|
|
675
|
+
for (const element of namedBindings.elements) {
|
|
676
|
+
// Resolve the original imported name so aliased imports
|
|
677
|
+
// like `Wrec as Base` still count as Wrec imports.
|
|
678
|
+
const importedName = element.propertyName?.text ?? element.name.text;
|
|
679
|
+
if (importedName === 'Wrec') {
|
|
680
|
+
// Store the local identifier that this file uses to refer to Wrec.
|
|
681
|
+
names.add(element.name.text);
|
|
497
682
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
);
|
|
503
|
-
if (nextText !== currentText) {
|
|
504
|
-
edits.push({
|
|
505
|
-
start: configObject.getStart(sourceFile),
|
|
506
|
-
end: configObject.end,
|
|
507
|
-
propName,
|
|
508
|
-
oldText: currentText,
|
|
509
|
-
text: nextText
|
|
510
|
-
});
|
|
683
|
+
// Capture whether the source file uses single or double quotes
|
|
684
|
+
// so generated code can follow the file's existing style.
|
|
685
|
+
const moduleText = statement.moduleSpecifier.getText(sourceFile);
|
|
686
|
+
quote = moduleText[0];
|
|
511
687
|
}
|
|
512
688
|
}
|
|
513
689
|
}
|
|
514
690
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
changed: false,
|
|
518
|
-
edits: [],
|
|
519
|
-
foundWrecSubclass: false,
|
|
520
|
-
text: sourceFile.text
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
if (edits.length === 0) {
|
|
525
|
-
return {
|
|
526
|
-
changed: false,
|
|
527
|
-
edits: [],
|
|
528
|
-
foundWrecSubclass: true,
|
|
529
|
-
text: sourceFile.text
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
let nextSource = sourceFile.text;
|
|
534
|
-
edits.sort((a, b) => b.start - a.start);
|
|
535
|
-
for (const edit of edits) {
|
|
536
|
-
nextSource =
|
|
537
|
-
nextSource.slice(0, edit.start) + edit.text + nextSource.slice(edit.end);
|
|
538
|
-
}
|
|
691
|
+
return {names, quote};
|
|
692
|
+
}
|
|
539
693
|
|
|
540
|
-
|
|
694
|
+
// Determines if a class member is static.
|
|
695
|
+
function hasStaticModifier(member) {
|
|
696
|
+
return Boolean(
|
|
697
|
+
member.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword)
|
|
698
|
+
);
|
|
541
699
|
}
|
|
542
700
|
|
|
543
|
-
|
|
701
|
+
// Determines if a class member represents an instance method.
|
|
702
|
+
// This is only called by getMethodUsages.
|
|
703
|
+
function isInstanceMethodMember(member) {
|
|
704
|
+
return (
|
|
705
|
+
!hasStaticModifier(member) &&
|
|
706
|
+
(ts.isMethodDeclaration(member) ||
|
|
707
|
+
ts.isGetAccessorDeclaration(member) ||
|
|
708
|
+
ts.isSetAccessorDeclaration(member)) &&
|
|
709
|
+
member.body
|
|
710
|
+
);
|
|
711
|
+
}
|
|
544
712
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
ts.ScriptTarget.Latest,
|
|
552
|
-
true,
|
|
553
|
-
scriptKind
|
|
713
|
+
// Determines if a path refers to a JavaScript or TypeScript source file.
|
|
714
|
+
function isSupportedSourceFile(filePath, excludeTests = false) {
|
|
715
|
+
return (
|
|
716
|
+
/\.(js|ts)$/.test(filePath) &&
|
|
717
|
+
!/\.d\.ts$/.test(filePath) &&
|
|
718
|
+
(!excludeTests || !filePath.includes('.test.'))
|
|
554
719
|
);
|
|
720
|
+
}
|
|
555
721
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
console.error('No class extending Wrec was found.');
|
|
564
|
-
process.exit(1);
|
|
722
|
+
// Handles CLI arguments and runs the script.
|
|
723
|
+
function main() {
|
|
724
|
+
const args = process.argv.slice(2);
|
|
725
|
+
const inputPaths = args.filter(arg => !arg.startsWith('--'));
|
|
726
|
+
|
|
727
|
+
if (inputPaths.length !== 1) {
|
|
728
|
+
throw new Error('Specify a single source file');
|
|
565
729
|
}
|
|
566
|
-
if (!changed) continue;
|
|
567
730
|
|
|
568
|
-
|
|
731
|
+
const dry = args.includes('--dry');
|
|
732
|
+
const result = evaluateSourceFile(inputPaths[0], {dry});
|
|
569
733
|
if (dry) {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
console.log(`${edit.propName} - ${suggestion}`);
|
|
734
|
+
// Report the proposed changes.
|
|
735
|
+
for (const {propName, suggestion} of result.suggestions) {
|
|
736
|
+
console.info(`${propName} - ${suggestion}`);
|
|
574
737
|
}
|
|
738
|
+
|
|
739
|
+
// Exit with a non-zero when there is at least one change
|
|
740
|
+
// so that condition can be checked.
|
|
741
|
+
if (result.changed) process.exit(1);
|
|
742
|
+
} else if (result.changed) {
|
|
743
|
+
console.info('updated source file');
|
|
575
744
|
} else {
|
|
576
|
-
|
|
745
|
+
console.info('no changes needed');
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Records a `this` property access and
|
|
750
|
+
// tracks it as a method call when applicable.
|
|
751
|
+
function recordThisAccess(props, calledMethods, node, name) {
|
|
752
|
+
props.add(name);
|
|
753
|
+
if (ts.isCallExpression(node.parent) && node.parent.expression === node) {
|
|
754
|
+
calledMethods.add(name);
|
|
577
755
|
}
|
|
578
|
-
if (verbose && !dry) console.log('updated');
|
|
579
756
|
}
|
|
580
757
|
|
|
581
|
-
|
|
758
|
+
// Validates that a source file exists and has a supported extension.
|
|
759
|
+
function validateFile(absFilePath) {
|
|
760
|
+
if (!fs.existsSync(absFilePath)) throw new Error('File not found');
|
|
582
761
|
|
|
583
|
-
|
|
584
|
-
|
|
762
|
+
const stat = fs.statSync(absFilePath);
|
|
763
|
+
if (!stat.isFile()) throw new Error('Not a file');
|
|
764
|
+
|
|
765
|
+
if (!/\.(js|ts)$/.test(absFilePath) || /\.d\.ts$/.test(absFilePath)) {
|
|
766
|
+
throw new Error('Unsupported file type');
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// If this is being run as a script,
|
|
771
|
+
// versus being imported (likely by test code) ...
|
|
772
|
+
if (import.meta.main) {
|
|
773
|
+
try {
|
|
774
|
+
main();
|
|
775
|
+
} catch (error) {
|
|
776
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
777
|
+
process.exit(1);
|
|
778
|
+
}
|
|
585
779
|
}
|