wrec 0.31.2 → 0.31.3

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <img alt="shipwreck" src="shipwreck.png" style="width: 256px">
4
4
 
5
- Wrec is a small library that greatly simplifies building web components.
5
+ Wrec is a library that greatly simplifies building web components.
6
6
  It is described in detail, with several working examples,
7
7
  in [my blog](https://mvolkmann.github.io/blog/wrec/).
8
8
  Also, see my series of
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.31.2",
5
+ "version": "0.31.3",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
package/scripts/lint.js CHANGED
@@ -30,6 +30,8 @@
30
30
  // - invalid HTML element nesting in templates
31
31
  // - invalid ref attribute targets
32
32
  // - duplicate ref attribute values
33
+ // - checkbox checked bindings that do not reference Boolean properties
34
+ // - radio checked bindings that do not reference String properties
33
35
 
34
36
  import fs from 'node:fs';
35
37
  import path from 'node:path';
@@ -43,8 +45,9 @@ import {
43
45
  } from './ast-utils.js';
44
46
 
45
47
  const CSS_PROPERTY_RE = /([a-zA-Z-]+)\s*:\s*([^;}]+)/g;
46
- const REFS_TEST_RE = /this\.[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)*/;
47
48
  const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/;
49
+ const PROPERTY_REF_RE = /^this\.([A-Za-z_$][\w$]*)$/;
50
+ const REFS_TEST_RE = /this\.[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)*/;
48
51
  const HTML_GLOBAL_ATTRIBUTES = new Set([
49
52
  'aria-label',
50
53
  'class',
@@ -142,13 +145,7 @@ const componentPropertyCache = new Map();
142
145
 
143
146
  // Analyzes code for invalid property access,
144
147
  // method calls, and arithmetic usage.
145
- function analyzeCodeNode(
146
- codeNode,
147
- checker,
148
- classNode,
149
- findings,
150
- metadata
151
- ) {
148
+ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
152
149
  if (metadata.eventHandler && ts.isIdentifier(codeNode)) {
153
150
  if (!metadata.classMethods.has(codeNode.text)) {
154
151
  uniquePush(
@@ -315,9 +312,7 @@ function buildAugmentedSource(
315
312
  : `${classNode.name.text} & __WrecSupportedProps`;
316
313
  const rewrittenText = item.text.replace(/\bthis\b/g, WREC_REF_NAME);
317
314
  const helperBody =
318
- item.shape === 'block'
319
- ? rewrittenText
320
- : `return (${rewrittenText});`;
315
+ item.shape === 'block' ? rewrittenText : `return (${rewrittenText});`;
321
316
  return `
322
317
  function __wrec_expr_${index}() {
323
318
  const ${WREC_REF_NAME} = null as unknown as ${targetType};
@@ -397,9 +392,11 @@ function collectMethodCodeItems(classNode) {
397
392
  context: 'instance',
398
393
  eventHandler: false,
399
394
  kind: 'method',
400
- location: member.getSourceFile().getLineAndCharacterOfPosition(
401
- member.name.getStart(member.getSourceFile())
402
- ),
395
+ location: member
396
+ .getSourceFile()
397
+ .getLineAndCharacterOfPosition(
398
+ member.name.getStart(member.getSourceFile())
399
+ ),
403
400
  shape: 'block',
404
401
  text: member.body.getText()
405
402
  });
@@ -799,6 +796,7 @@ function formatReport(
799
796
  findings.duplicateProperties.length > 0 ||
800
797
  findings.reservedProperties.length > 0 ||
801
798
  findings.invalidUsedByReferences.length > 0 ||
799
+ findings.invalidCheckedBindings.length > 0 ||
802
800
  findings.invalidComputedProperties.length > 0 ||
803
801
  findings.invalidRefAttributes.length > 0 ||
804
802
  findings.invalidValuesConfigurations.length > 0 ||
@@ -868,6 +866,13 @@ function formatReport(
868
866
  );
869
867
  }
870
868
 
869
+ if (findings.invalidCheckedBindings.length > 0) {
870
+ lines.push('invalid checked bindings:');
871
+ findings.invalidCheckedBindings.forEach(message =>
872
+ lines.push(` ${message}`)
873
+ );
874
+ }
875
+
871
876
  if (findings.invalidRefAttributes.length > 0) {
872
877
  lines.push('invalid ref attributes:');
873
878
  findings.invalidRefAttributes.forEach(message =>
@@ -1046,6 +1051,12 @@ function getComponentPropertyMaps(filePath, sourceText, seen = new Set()) {
1046
1051
  return propertyMaps;
1047
1052
  }
1048
1053
 
1054
+ // Returns the referenced property name for a single `this.prop` binding.
1055
+ function getPropertyNameInAttribute(attrValue) {
1056
+ const match = attrValue.trim().match(PROPERTY_REF_RE);
1057
+ return match ? match[1] : undefined;
1058
+ }
1059
+
1049
1060
  // Returns trimmed source text for a TypeScript expression node.
1050
1061
  function getExpressionText(sourceFile, expression) {
1051
1062
  return expression.getText(sourceFile).trim();
@@ -1057,6 +1068,12 @@ function getHtmlTagName(node) {
1057
1068
  return typeof tagName === 'string' ? tagName.toLowerCase() : '';
1058
1069
  }
1059
1070
 
1071
+ // Returns the literal input type attribute value when one is present.
1072
+ function getInputType(node) {
1073
+ const type = node.getAttribute('type');
1074
+ return typeof type === 'string' ? type.toLowerCase() : undefined;
1075
+ }
1076
+
1060
1077
  // Gets an object-literal property with the given key.
1061
1078
  function getObjectProperty(objectLiteral, key) {
1062
1079
  for (const property of objectLiteral.properties) {
@@ -1295,6 +1312,7 @@ export function lintSource(filePath, sourceText, options = {}) {
1295
1312
  duplicateProperties,
1296
1313
  extraArguments: [],
1297
1314
  incompatibleArguments: [],
1315
+ invalidCheckedBindings: [],
1298
1316
  invalidComputedProperties: [],
1299
1317
  invalidDefaultValues: [],
1300
1318
  invalidEventHandlers: [],
@@ -1377,19 +1395,13 @@ export function lintSource(filePath, sourceText, options = {}) {
1377
1395
 
1378
1396
  helperCodeNodes.forEach((codeNode, index) => {
1379
1397
  if (!codeNode) return;
1380
- analyzeCodeNode(
1381
- codeNode,
1382
- augmentedChecker,
1383
- augmentedClassNode,
1384
- findings,
1385
- {
1386
- classMethods: allMethods,
1387
- contextKeys: new Set(contextKeys),
1388
- checkContextCalls: allCodeItems[index]?.checkContextCalls ?? true,
1389
- eventHandler: allCodeItems[index]?.eventHandler ?? false,
1390
- sourceFile: augmentedSourceFile
1391
- }
1392
- );
1398
+ analyzeCodeNode(codeNode, augmentedChecker, augmentedClassNode, findings, {
1399
+ classMethods: allMethods,
1400
+ contextKeys: new Set(contextKeys),
1401
+ checkContextCalls: allCodeItems[index]?.checkContextCalls ?? true,
1402
+ eventHandler: allCodeItems[index]?.eventHandler ?? false,
1403
+ sourceFile: augmentedSourceFile
1404
+ });
1393
1405
  });
1394
1406
 
1395
1407
  findings.duplicateProperties.sort();
@@ -1403,6 +1415,7 @@ export function lintSource(filePath, sourceText, options = {}) {
1403
1415
  a.methodName.localeCompare(b.methodName) ||
1404
1416
  a.parameterName.localeCompare(b.parameterName)
1405
1417
  );
1418
+ findings.invalidCheckedBindings.sort();
1406
1419
  findings.invalidComputedProperties.sort();
1407
1420
  findings.invalidDefaultValues.sort();
1408
1421
  findings.invalidEventHandlers.sort();
@@ -1552,6 +1565,37 @@ function uniquePush(array, value) {
1552
1565
  if (!array.includes(value)) array.push(value);
1553
1566
  }
1554
1567
 
1568
+ // Validates checked bindings for checkbox and radio input elements.
1569
+ function validateCheckedBinding(
1570
+ node,
1571
+ attrName,
1572
+ attrValue,
1573
+ findings,
1574
+ supportedProps
1575
+ ) {
1576
+ if (getHtmlTagName(node) !== 'input') return;
1577
+
1578
+ const [baseAttrName] = attrName.split(':');
1579
+ if (baseAttrName !== 'checked') return;
1580
+
1581
+ const inputType = getInputType(node);
1582
+ if (inputType !== 'checkbox' && inputType !== 'radio') return;
1583
+
1584
+ const propName = getPropertyNameInAttribute(attrValue);
1585
+ if (!propName) return;
1586
+
1587
+ const propInfo = supportedProps.get(propName);
1588
+ if (!propInfo) return;
1589
+
1590
+ const expectedType = inputType === 'checkbox' ? 'boolean' : 'string';
1591
+ if (propInfo.typeText === expectedType) return;
1592
+
1593
+ findings.invalidCheckedBindings.push(
1594
+ `input type="${inputType}" attribute "${attrName}" refers to ` +
1595
+ `property "${propName}" whose type is not ${expectedType}`
1596
+ );
1597
+ }
1598
+
1555
1599
  // Validates computed property references and method calls.
1556
1600
  function validateComputedProperty(
1557
1601
  propName,
@@ -1926,6 +1970,13 @@ function walkHtmlNode(
1926
1970
  findings,
1927
1971
  componentPropertyMaps
1928
1972
  );
1973
+ validateCheckedBinding(
1974
+ node,
1975
+ attrName,
1976
+ attrValue,
1977
+ findings,
1978
+ supportedProps
1979
+ );
1929
1980
  validateHtmlAttribute(node, attrName, findings);
1930
1981
  validateValueBindingEvent(node, attrName, findings);
1931
1982
  if (attrName === 'ref') {