wrec 0.31.2 → 0.31.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/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.4",
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) {
@@ -1090,6 +1107,24 @@ function getParameterType(checker, parameterSymbol, location, isRestArgument) {
1090
1107
  return typeArguments[0] ?? parameterType;
1091
1108
  }
1092
1109
 
1110
+ // Returns the Wrec property config type name for a normalized type string.
1111
+ function getPropertyConfigTypeName(typeName) {
1112
+ switch (typeName) {
1113
+ case 'array':
1114
+ return 'Array';
1115
+ case 'boolean':
1116
+ return 'Boolean';
1117
+ case 'number':
1118
+ return 'Number';
1119
+ case 'object':
1120
+ return 'Object';
1121
+ case 'string':
1122
+ return 'String';
1123
+ default:
1124
+ return typeName;
1125
+ }
1126
+ }
1127
+
1093
1128
  // Derives a readable property type string from syntax or the type checker.
1094
1129
  function getPropertyTypeText(checker, sourceFile, expression) {
1095
1130
  const typeText = getTypeSyntaxText(sourceFile, expression);
@@ -1295,6 +1330,7 @@ export function lintSource(filePath, sourceText, options = {}) {
1295
1330
  duplicateProperties,
1296
1331
  extraArguments: [],
1297
1332
  incompatibleArguments: [],
1333
+ invalidCheckedBindings: [],
1298
1334
  invalidComputedProperties: [],
1299
1335
  invalidDefaultValues: [],
1300
1336
  invalidEventHandlers: [],
@@ -1377,19 +1413,13 @@ export function lintSource(filePath, sourceText, options = {}) {
1377
1413
 
1378
1414
  helperCodeNodes.forEach((codeNode, index) => {
1379
1415
  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
- );
1416
+ analyzeCodeNode(codeNode, augmentedChecker, augmentedClassNode, findings, {
1417
+ classMethods: allMethods,
1418
+ contextKeys: new Set(contextKeys),
1419
+ checkContextCalls: allCodeItems[index]?.checkContextCalls ?? true,
1420
+ eventHandler: allCodeItems[index]?.eventHandler ?? false,
1421
+ sourceFile: augmentedSourceFile
1422
+ });
1393
1423
  });
1394
1424
 
1395
1425
  findings.duplicateProperties.sort();
@@ -1403,6 +1433,7 @@ export function lintSource(filePath, sourceText, options = {}) {
1403
1433
  a.methodName.localeCompare(b.methodName) ||
1404
1434
  a.parameterName.localeCompare(b.parameterName)
1405
1435
  );
1436
+ findings.invalidCheckedBindings.sort();
1406
1437
  findings.invalidComputedProperties.sort();
1407
1438
  findings.invalidDefaultValues.sort();
1408
1439
  findings.invalidEventHandlers.sort();
@@ -1552,6 +1583,39 @@ function uniquePush(array, value) {
1552
1583
  if (!array.includes(value)) array.push(value);
1553
1584
  }
1554
1585
 
1586
+ // Validates checked bindings for checkbox and radio input elements.
1587
+ function validateCheckedBinding(
1588
+ node,
1589
+ attrName,
1590
+ attrValue,
1591
+ findings,
1592
+ supportedProps
1593
+ ) {
1594
+ if (getHtmlTagName(node) !== 'input') return;
1595
+
1596
+ const [baseAttrName] = attrName.split(':');
1597
+ if (baseAttrName !== 'checked') return;
1598
+
1599
+ const inputType = getInputType(node);
1600
+ if (inputType !== 'checkbox' && inputType !== 'radio') return;
1601
+
1602
+ const propName = getPropertyNameInAttribute(attrValue);
1603
+ if (!propName) return;
1604
+
1605
+ const propInfo = supportedProps.get(propName);
1606
+ if (!propInfo) return;
1607
+
1608
+ const expectedType = inputType === 'checkbox' ? 'boolean' : 'string';
1609
+ if (propInfo.typeText === expectedType) return;
1610
+
1611
+ const expectedTypeName = getPropertyConfigTypeName(expectedType);
1612
+
1613
+ findings.invalidCheckedBindings.push(
1614
+ `input type="${inputType}" attribute "${attrName}" refers to ` +
1615
+ `property "${propName}" whose type is not ${expectedTypeName}`
1616
+ );
1617
+ }
1618
+
1555
1619
  // Validates computed property references and method calls.
1556
1620
  function validateComputedProperty(
1557
1621
  propName,
@@ -1596,7 +1660,7 @@ function validateDefaultValue(checker, typeExpression, valueExpression) {
1596
1660
  if (
1597
1661
  !(valueType.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral))
1598
1662
  ) {
1599
- return {typeName: 'string', valueTypeName};
1663
+ return {typeName: 'String', valueTypeName};
1600
1664
  }
1601
1665
  return undefined;
1602
1666
  }
@@ -1605,7 +1669,7 @@ function validateDefaultValue(checker, typeExpression, valueExpression) {
1605
1669
  if (
1606
1670
  !(valueType.flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral))
1607
1671
  ) {
1608
- return {typeName: 'number', valueTypeName};
1672
+ return {typeName: 'Number', valueTypeName};
1609
1673
  }
1610
1674
  return undefined;
1611
1675
  }
@@ -1614,14 +1678,14 @@ function validateDefaultValue(checker, typeExpression, valueExpression) {
1614
1678
  if (
1615
1679
  !(valueType.flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral))
1616
1680
  ) {
1617
- return {typeName: 'boolean', valueTypeName};
1681
+ return {typeName: 'Boolean', valueTypeName};
1618
1682
  }
1619
1683
  return undefined;
1620
1684
  }
1621
1685
 
1622
1686
  if (typeKind === 'Array') {
1623
1687
  if (!checker.isArrayType(valueType) && !checker.isTupleType(valueType)) {
1624
- return {typeName: 'array', valueTypeName};
1688
+ return {typeName: 'Array', valueTypeName};
1625
1689
  }
1626
1690
  return undefined;
1627
1691
  }
@@ -1632,7 +1696,7 @@ function validateDefaultValue(checker, typeExpression, valueExpression) {
1632
1696
  !checker.isArrayType(valueType) &&
1633
1697
  !checker.isTupleType(valueType);
1634
1698
  if (!isObjectLike) {
1635
- return {typeName: 'object', valueTypeName};
1699
+ return {typeName: 'Object', valueTypeName};
1636
1700
  }
1637
1701
  }
1638
1702
 
@@ -1926,6 +1990,13 @@ function walkHtmlNode(
1926
1990
  findings,
1927
1991
  componentPropertyMaps
1928
1992
  );
1993
+ validateCheckedBinding(
1994
+ node,
1995
+ attrName,
1996
+ attrValue,
1997
+ findings,
1998
+ supportedProps
1999
+ );
1929
2000
  validateHtmlAttribute(node, attrName, findings);
1930
2001
  validateValueBindingEvent(node, attrName, findings);
1931
2002
  if (attrName === 'ref') {