wrec 0.32.0 → 0.32.1
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/package.json +1 -1
- package/scripts/lint.js +35 -0
- package/scripts/used-by.js +63 -25
package/package.json
CHANGED
package/scripts/lint.js
CHANGED
|
@@ -119,6 +119,7 @@ const THIS_CALL_RE = /this\.([A-Za-z_$][\w$]*)\s*\(/g;
|
|
|
119
119
|
const THIS_REF_RE = /this\.([A-Za-z_$][\w$]*)(\.[A-Za-z_$][\w$]*)*/g;
|
|
120
120
|
const PLACEHOLDER_PREFIX = '__WREC_PLACEHOLDER__';
|
|
121
121
|
const RESERVED_PROPERTY_NAMES = new Set(['class', 'style']);
|
|
122
|
+
const GETTER_PREFIX = 'get ';
|
|
122
123
|
const SUPPORTED_EVENT_NAMES = new Set([
|
|
123
124
|
'blur',
|
|
124
125
|
'change',
|
|
@@ -348,6 +349,17 @@ function collectClassMethods(classNode) {
|
|
|
348
349
|
return methods;
|
|
349
350
|
}
|
|
350
351
|
|
|
352
|
+
// Collects all getter names defined in a component class.
|
|
353
|
+
function collectGetterNames(classNode) {
|
|
354
|
+
const getters = new Set();
|
|
355
|
+
for (const member of classNode.members) {
|
|
356
|
+
if (!ts.isGetAccessorDeclaration(member)) continue;
|
|
357
|
+
const name = getMemberName(member);
|
|
358
|
+
if (name) getters.add(name);
|
|
359
|
+
}
|
|
360
|
+
return getters;
|
|
361
|
+
}
|
|
362
|
+
|
|
351
363
|
// Finds the synthetic `__wrec_expr_*` helper functions that were added by
|
|
352
364
|
// `buildAugmentedSource` and returns their bodies in index order.
|
|
353
365
|
// This gives the linter a stable list of typed code nodes
|
|
@@ -1062,6 +1074,11 @@ function getExpressionText(sourceFile, expression) {
|
|
|
1062
1074
|
return expression.getText(sourceFile).trim();
|
|
1063
1075
|
}
|
|
1064
1076
|
|
|
1077
|
+
// Returns the name from a getter reference.
|
|
1078
|
+
function getGetterName(reference) {
|
|
1079
|
+
return reference.slice(GETTER_PREFIX.length).trim();
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1065
1082
|
// Returns a lowercased HTML tag name for a parsed HTML node.
|
|
1066
1083
|
function getHtmlTagName(node) {
|
|
1067
1084
|
const tagName = node.rawTagName || node.tagName;
|
|
@@ -1243,6 +1260,11 @@ function isCallCallee(node) {
|
|
|
1243
1260
|
return ts.isCallExpression(node.parent) && node.parent.expression === node;
|
|
1244
1261
|
}
|
|
1245
1262
|
|
|
1263
|
+
// Returns whether a reference refers to a getter method.
|
|
1264
|
+
function isGetterReference(reference) {
|
|
1265
|
+
return reference.startsWith(GETTER_PREFIX);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1246
1268
|
// Returns whether a declaration represents an imported binding.
|
|
1247
1269
|
function isImportLikeDeclaration(node) {
|
|
1248
1270
|
return (
|
|
@@ -1325,6 +1347,7 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1325
1347
|
propertyEntries,
|
|
1326
1348
|
reservedProperties
|
|
1327
1349
|
} = extractProperties(sourceFile, checker, classNode);
|
|
1350
|
+
const getterNames = collectGetterNames(classNode);
|
|
1328
1351
|
const allMethods = collectClassMethods(classNode);
|
|
1329
1352
|
const findings = {
|
|
1330
1353
|
duplicateProperties,
|
|
@@ -1393,6 +1416,7 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1393
1416
|
checker,
|
|
1394
1417
|
supportedProps,
|
|
1395
1418
|
propertyEntries,
|
|
1419
|
+
getterNames,
|
|
1396
1420
|
allMethods,
|
|
1397
1421
|
findings
|
|
1398
1422
|
);
|
|
@@ -1826,6 +1850,7 @@ function validatePropertyConfigs(
|
|
|
1826
1850
|
checker,
|
|
1827
1851
|
supportedProps,
|
|
1828
1852
|
propertyEntries,
|
|
1853
|
+
getterNames,
|
|
1829
1854
|
classMethods,
|
|
1830
1855
|
findings
|
|
1831
1856
|
) {
|
|
@@ -1852,6 +1877,16 @@ function validatePropertyConfigs(
|
|
|
1852
1877
|
|
|
1853
1878
|
if (methods) {
|
|
1854
1879
|
for (const methodName of methods) {
|
|
1880
|
+
if (isGetterReference(methodName)) {
|
|
1881
|
+
const getterName = getGetterName(methodName);
|
|
1882
|
+
if (getterNames.has(getterName)) continue;
|
|
1883
|
+
findings.invalidUsedByReferences.push(
|
|
1884
|
+
`property "${propName}" usedBy references ` +
|
|
1885
|
+
`missing getter "${getterName}"`
|
|
1886
|
+
);
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1855
1890
|
if (!classMethods.has(methodName)) {
|
|
1856
1891
|
findings.invalidUsedByReferences.push(
|
|
1857
1892
|
`property "${propName}" usedBy references ` +
|
package/scripts/used-by.js
CHANGED
|
@@ -28,6 +28,10 @@ import {
|
|
|
28
28
|
} from './ast-utils.js';
|
|
29
29
|
|
|
30
30
|
const cwd = process.cwd();
|
|
31
|
+
const CALL_RE = /this\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
|
|
32
|
+
const REFS_RE =
|
|
33
|
+
/this\.([A-Za-z_$][A-Za-z0-9_$]*)(\.[A-Za-z_$][A-Za-z0-9_$]*)*/g;
|
|
34
|
+
const GETTER_PREFIX = 'get ';
|
|
31
35
|
|
|
32
36
|
// Records property names introduced by
|
|
33
37
|
// an identifier or object binding pattern.
|
|
@@ -264,17 +268,17 @@ function buildConfigText(sourceFile, member, methodNames, quote) {
|
|
|
264
268
|
return `{${openSpacing}${propertyStrings.join(', ')}${closeSpacing}}`;
|
|
265
269
|
}
|
|
266
270
|
|
|
267
|
-
// Walks a method body to collect component property reads
|
|
268
|
-
// and
|
|
271
|
+
// Walks a method or accessor body to collect component property reads
|
|
272
|
+
// and dependency targets reached through `this`.
|
|
269
273
|
// This is only called by getMethodUsages.
|
|
270
|
-
function collectMethodBodyUsage(node, props, calledMethods) {
|
|
274
|
+
function collectMethodBodyUsage(node, getterNames, props, calledMethods) {
|
|
271
275
|
if (
|
|
272
276
|
ts.isPropertyAccessExpression(node) &&
|
|
273
277
|
node.expression.kind === ts.SyntaxKind.ThisKeyword
|
|
274
278
|
) {
|
|
275
279
|
// Handles direct property access like `this.foo`
|
|
276
280
|
// and records method calls like `this.foo()`.
|
|
277
|
-
recordThisAccess(props, calledMethods, node, node.name.text);
|
|
281
|
+
recordThisAccess(props, calledMethods, getterNames, node, node.name.text);
|
|
278
282
|
} else if (
|
|
279
283
|
ts.isElementAccessExpression(node) &&
|
|
280
284
|
node.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
@@ -283,7 +287,13 @@ function collectMethodBodyUsage(node, props, calledMethods) {
|
|
|
283
287
|
) {
|
|
284
288
|
// Handles string-based element access like `this['foo']`
|
|
285
289
|
// and records method calls like `this['foo']()`.
|
|
286
|
-
recordThisAccess(
|
|
290
|
+
recordThisAccess(
|
|
291
|
+
props,
|
|
292
|
+
calledMethods,
|
|
293
|
+
getterNames,
|
|
294
|
+
node,
|
|
295
|
+
node.argumentExpression.text
|
|
296
|
+
);
|
|
287
297
|
} else if (
|
|
288
298
|
ts.isVariableDeclaration(node) &&
|
|
289
299
|
node.initializer &&
|
|
@@ -315,7 +325,7 @@ function collectMethodBodyUsage(node, props, calledMethods) {
|
|
|
315
325
|
}
|
|
316
326
|
|
|
317
327
|
ts.forEachChild(node, child =>
|
|
318
|
-
collectMethodBodyUsage(child, props, calledMethods)
|
|
328
|
+
collectMethodBodyUsage(child, getterNames, props, calledMethods)
|
|
319
329
|
);
|
|
320
330
|
}
|
|
321
331
|
|
|
@@ -385,7 +395,6 @@ export function evaluateSourceText(filePath, text) {
|
|
|
385
395
|
// This is only called by getMethodUsages.
|
|
386
396
|
function getCssCalledMethods(classNode) {
|
|
387
397
|
const methodNames = new Set();
|
|
388
|
-
const CALL_RE = /this\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
|
|
389
398
|
let template;
|
|
390
399
|
|
|
391
400
|
for (const member of classNode.members) {
|
|
@@ -410,11 +419,10 @@ function getCssCalledMethods(classNode) {
|
|
|
410
419
|
// If no matching declaration was found, return an empty Set.
|
|
411
420
|
if (!template) return methodNames;
|
|
412
421
|
|
|
413
|
-
// Finds all method
|
|
414
|
-
//
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
methodNames.add(match[1]);
|
|
422
|
+
// Finds all method calls and getter references
|
|
423
|
+
// in CSS property value expressions.
|
|
424
|
+
for (const target of getExpressionTargets(template.getText())) {
|
|
425
|
+
methodNames.add(target);
|
|
418
426
|
}
|
|
419
427
|
|
|
420
428
|
return methodNames;
|
|
@@ -425,7 +433,6 @@ function getCssCalledMethods(classNode) {
|
|
|
425
433
|
// This is only called by getMethodUsages.
|
|
426
434
|
function getComputedCalledMethods(classNode) {
|
|
427
435
|
const methodNames = new Set();
|
|
428
|
-
const CALL_RE = /this\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
|
|
429
436
|
let propertiesNode;
|
|
430
437
|
|
|
431
438
|
for (const member of classNode.members) {
|
|
@@ -473,10 +480,11 @@ function getComputedCalledMethods(classNode) {
|
|
|
473
480
|
// If the property value isn't a string then skip it.
|
|
474
481
|
if (!ts.isStringLiteralLike(configProperty.initializer)) continue;
|
|
475
482
|
|
|
476
|
-
// Find all
|
|
483
|
+
// Find all method calls and getter references
|
|
484
|
+
// in the string JavaScript expression.
|
|
477
485
|
const computed = configProperty.initializer.text;
|
|
478
|
-
for (const
|
|
479
|
-
methodNames.add(
|
|
486
|
+
for (const target of getExpressionTargets(computed)) {
|
|
487
|
+
methodNames.add(target);
|
|
480
488
|
}
|
|
481
489
|
}
|
|
482
490
|
}
|
|
@@ -484,6 +492,23 @@ function getComputedCalledMethods(classNode) {
|
|
|
484
492
|
return methodNames;
|
|
485
493
|
}
|
|
486
494
|
|
|
495
|
+
// Collects method-call and getter-reference targets from expression text.
|
|
496
|
+
function getExpressionTargets(text) {
|
|
497
|
+
const targets = new Set();
|
|
498
|
+
for (const match of text.matchAll(CALL_RE)) {
|
|
499
|
+
targets.add(match[1]);
|
|
500
|
+
}
|
|
501
|
+
for (const match of text.matchAll(REFS_RE)) {
|
|
502
|
+
targets.add(getGetterDependency(match[1]));
|
|
503
|
+
}
|
|
504
|
+
return targets;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Returns the dependency target string for a getter reference.
|
|
508
|
+
function getGetterDependency(name) {
|
|
509
|
+
return `${GETTER_PREFIX}${name}`;
|
|
510
|
+
}
|
|
511
|
+
|
|
487
512
|
// Returns the leading indentation in the line
|
|
488
513
|
// that begins at a given position (`pos`) inside `text`.
|
|
489
514
|
function getIndent(text, pos) {
|
|
@@ -495,19 +520,26 @@ function getIndent(text, pos) {
|
|
|
495
520
|
// Returns a map where the keys are property names and
|
|
496
521
|
// the values are Sets of public methods that use it transitively.
|
|
497
522
|
function getMethodUsages(classNode, propertyNames) {
|
|
523
|
+
const getterNames = new Set();
|
|
498
524
|
const methodInfo = new Map();
|
|
499
525
|
|
|
526
|
+
for (const member of classNode.members) {
|
|
527
|
+
if (!ts.isGetAccessorDeclaration(member)) continue;
|
|
528
|
+
const getterName = getNameText(member.name);
|
|
529
|
+
if (getterName) getterNames.add(getterName);
|
|
530
|
+
}
|
|
531
|
+
|
|
500
532
|
for (const member of classNode.members) {
|
|
501
533
|
// If the member doesn't represent an instance method, skip it.
|
|
502
534
|
if (!isInstanceMethodMember(member)) continue;
|
|
503
535
|
|
|
504
536
|
// If the member doesn't have a string name, skip it.
|
|
505
|
-
const methodName =
|
|
537
|
+
const methodName = getUsageTargetName(member);
|
|
506
538
|
if (!methodName) continue;
|
|
507
539
|
|
|
508
540
|
const props = new Set();
|
|
509
541
|
const calledMethods = new Set();
|
|
510
|
-
collectMethodBodyUsage(member.body, props, calledMethods);
|
|
542
|
+
collectMethodBodyUsage(member.body, getterNames, props, calledMethods);
|
|
511
543
|
methodInfo.set(methodName, {
|
|
512
544
|
calledMethods,
|
|
513
545
|
isPrivate: ts.isPrivateIdentifier(member.name),
|
|
@@ -573,12 +605,9 @@ function getTemplateCalledMethods(classNode) {
|
|
|
573
605
|
|
|
574
606
|
const methodNames = new Set();
|
|
575
607
|
if (template) {
|
|
576
|
-
// Finds all method
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
const CALL_RE = /this\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
|
|
580
|
-
for (const match of text.matchAll(CALL_RE)) {
|
|
581
|
-
methodNames.add(match[1]);
|
|
608
|
+
// Finds all method calls and getter references in the HTML template.
|
|
609
|
+
for (const target of getExpressionTargets(template.getText())) {
|
|
610
|
+
methodNames.add(target);
|
|
582
611
|
}
|
|
583
612
|
}
|
|
584
613
|
return methodNames;
|
|
@@ -614,6 +643,13 @@ function getTransitiveProps(methodInfo, memo, methodName, seen = new Set()) {
|
|
|
614
643
|
return props;
|
|
615
644
|
}
|
|
616
645
|
|
|
646
|
+
// Returns the usedBy dependency target name for a class member.
|
|
647
|
+
function getUsageTargetName(member) {
|
|
648
|
+
const name = getNameText(member.name);
|
|
649
|
+
if (!name) return undefined;
|
|
650
|
+
return ts.isGetAccessorDeclaration(member) ? getGetterDependency(name) : name;
|
|
651
|
+
}
|
|
652
|
+
|
|
617
653
|
// Determines if a class member represents an instance method.
|
|
618
654
|
// This is only called by getMethodUsages.
|
|
619
655
|
function isInstanceMethodMember(member) {
|
|
@@ -671,10 +707,12 @@ function main() {
|
|
|
671
707
|
|
|
672
708
|
// Records a `this` property access and
|
|
673
709
|
// tracks it as a method call when applicable.
|
|
674
|
-
function recordThisAccess(props, calledMethods, node, name) {
|
|
710
|
+
function recordThisAccess(props, calledMethods, getterNames, node, name) {
|
|
675
711
|
props.add(name);
|
|
676
712
|
if (ts.isCallExpression(node.parent) && node.parent.expression === node) {
|
|
677
713
|
calledMethods.add(name);
|
|
714
|
+
} else if (getterNames.has(name)) {
|
|
715
|
+
calledMethods.add(getGetterDependency(name));
|
|
678
716
|
}
|
|
679
717
|
}
|
|
680
718
|
|