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 CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "wrec",
3
3
  "description": "a small library that greatly simplifies building web components",
4
4
  "author": "R. Mark Volkmann",
5
- "version": "0.32.0",
5
+ "version": "0.32.1",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
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 ` +
@@ -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 method calls made through `this`.
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(props, calledMethods, node, node.argumentExpression.text);
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 names called in CSS property value expressions
414
- // matching `this.method()`.
415
- const text = template.getText();
416
- for (const match of text.matchAll(CALL_RE)) {
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 the method calls in the string JavaScript expression.
483
+ // Find all method calls and getter references
484
+ // in the string JavaScript expression.
477
485
  const computed = configProperty.initializer.text;
478
- for (const match of computed.matchAll(CALL_RE)) {
479
- methodNames.add(match[1]);
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 = getNameText(member.name);
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 names called in the HTML template
577
- // matching `this.method()`.
578
- const text = template.getText();
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