wrec 0.35.0 → 0.35.2

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/lint.js +91 -6
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "wrec",
3
3
  "description": "a library that greatly simplifies building web components",
4
4
  "author": "R. Mark Volkmann",
5
- "version": "0.35.0",
5
+ "version": "0.35.2",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
package/scripts/lint.js CHANGED
@@ -451,6 +451,21 @@ function collectSupportedPropertyNames(classNode) {
451
451
  return supportedProps;
452
452
  }
453
453
 
454
+ // Collects declared TypeScript property types from a component class.
455
+ function collectDeclaredPropertyTypes(classNode) {
456
+ const declaredProps = new Map();
457
+
458
+ for (const member of classNode.members) {
459
+ if (!isDeclarePropertyDeclaration(member)) continue;
460
+
461
+ const propName = getMemberName(member);
462
+ if (!propName || !member.type) continue;
463
+ declaredProps.set(propName, member.type);
464
+ }
465
+
466
+ return declaredProps;
467
+ }
468
+
454
469
  // Validates that useState mappings point at existing component properties.
455
470
  function collectUseStateMapErrors(classNode, supportedProps, findings) {
456
471
  // Walks the class body looking for useState calls with mapping objects.
@@ -815,6 +830,7 @@ function formatReport(
815
830
  findings.duplicateProperties.length > 0 ||
816
831
  findings.extraArguments.length > 0 ||
817
832
  findings.incompatibleArguments.length > 0 ||
833
+ findings.incompatibleDeclareTypes.length > 0 ||
818
834
  findings.invalidCheckedBindings.length > 0 ||
819
835
  findings.invalidComputedProperties.length > 0 ||
820
836
  findings.invalidDefaultValues.length > 0 ||
@@ -889,6 +905,13 @@ function formatReport(
889
905
  });
890
906
  }
891
907
 
908
+ if (findings.incompatibleDeclareTypes.length > 0) {
909
+ lines.push('incompatible declare types:');
910
+ findings.incompatibleDeclareTypes.forEach(message =>
911
+ lines.push(` ${message}`)
912
+ );
913
+ }
914
+
892
915
  if (findings.invalidCheckedBindings.length > 0) {
893
916
  lines.push('invalid checked bindings:');
894
917
  findings.invalidCheckedBindings.forEach(message =>
@@ -1166,6 +1189,11 @@ function getPropertyTypeText(checker, sourceFile, expression) {
1166
1189
  return checker.typeToString(type);
1167
1190
  }
1168
1191
 
1192
+ // Gets the TypeScript type text for a declared property member.
1193
+ function getPropertyTypeTextFromNode(sourceFile, typeNode) {
1194
+ return typeNode.getText(sourceFile).trim();
1195
+ }
1196
+
1169
1197
  // Returns an array of string literal values from a property when possible.
1170
1198
  function getStringArrayLiteral(property) {
1171
1199
  if (!property || !ts.isPropertyAssignment(property)) return undefined;
@@ -1275,6 +1303,17 @@ function isCallCallee(node) {
1275
1303
  return ts.isCallExpression(node.parent) && node.parent.expression === node;
1276
1304
  }
1277
1305
 
1306
+ // Returns whether a class member is a declared property declaration.
1307
+ function isDeclarePropertyDeclaration(member) {
1308
+ return (
1309
+ ts.isPropertyDeclaration(member) &&
1310
+ ts.canHaveModifiers(member) &&
1311
+ ts
1312
+ .getModifiers(member)
1313
+ ?.some(mod => mod.kind === ts.SyntaxKind.DeclareKeyword)
1314
+ );
1315
+ }
1316
+
1278
1317
  // Returns whether a reference refers to a getter method.
1279
1318
  function isGetterReference(reference) {
1280
1319
  return reference.startsWith(GETTER_PREFIX);
@@ -1362,12 +1401,14 @@ export function lintSource(filePath, sourceText, options = {}) {
1362
1401
  propertyEntries,
1363
1402
  reservedProperties
1364
1403
  } = extractProperties(sourceFile, checker, classNode);
1404
+ const declaredPropertyTypes = collectDeclaredPropertyTypes(classNode);
1365
1405
  const getterNames = collectGetterNames(classNode);
1366
1406
  const allMethods = collectClassMethods(classNode);
1367
1407
  const findings = {
1368
1408
  duplicateProperties,
1369
1409
  extraArguments: [],
1370
1410
  incompatibleArguments: [],
1411
+ incompatibleDeclareTypes: [],
1371
1412
  invalidCheckedBindings: [],
1372
1413
  invalidComputedProperties: [],
1373
1414
  invalidDefaultValues: [],
@@ -1430,6 +1471,8 @@ export function lintSource(filePath, sourceText, options = {}) {
1430
1471
 
1431
1472
  validatePropertyConfigs(
1432
1473
  checker,
1474
+ sourceFile,
1475
+ declaredPropertyTypes,
1433
1476
  supportedProps,
1434
1477
  propertyEntries,
1435
1478
  getterNames,
@@ -1473,6 +1516,7 @@ export function lintSource(filePath, sourceText, options = {}) {
1473
1516
  a.methodName.localeCompare(b.methodName) ||
1474
1517
  a.parameterName.localeCompare(b.parameterName)
1475
1518
  );
1519
+ findings.incompatibleDeclareTypes.sort();
1476
1520
  findings.invalidCheckedBindings.sort();
1477
1521
  findings.invalidComputedProperties.sort();
1478
1522
  findings.invalidDefaultValues.sort();
@@ -1558,6 +1602,23 @@ function requiresContextFunction(symbol, sourceFile) {
1558
1602
  });
1559
1603
  }
1560
1604
 
1605
+ // Returns whether a type represents an object-like value other than an array.
1606
+ function isNonArrayObjectLikeType(checker, type) {
1607
+ if (type.isUnion()) {
1608
+ return type.types.every(member => isNonArrayObjectLikeType(checker, member));
1609
+ }
1610
+
1611
+ if (type.isIntersection()) {
1612
+ return type.types.every(member => isNonArrayObjectLikeType(checker, member));
1613
+ }
1614
+
1615
+ return (
1616
+ Boolean(type.flags & (ts.TypeFlags.Object | ts.TypeFlags.NonPrimitive)) &&
1617
+ !checker.isArrayType(type) &&
1618
+ !checker.isTupleType(type)
1619
+ );
1620
+ }
1621
+
1561
1622
  // Resolves a relative import path to an existing source file.
1562
1623
  function resolveImportPath(baseDir, importPath) {
1563
1624
  if (!importPath.startsWith('.')) return undefined;
@@ -1578,6 +1639,22 @@ function toUserFacingExpression(text) {
1578
1639
  return text.replaceAll(WREC_REF_NAME, 'this');
1579
1640
  }
1580
1641
 
1642
+ // Returns whether a static property config type is compatible with a declare type.
1643
+ function typeExpressionMatchesDeclaredType(
1644
+ checker,
1645
+ typeExpression,
1646
+ declaredTypeNode
1647
+ ) {
1648
+ const declaredType = checker.getTypeFromTypeNode(declaredTypeNode);
1649
+
1650
+ if (typeExpressionKind(typeExpression) === 'Object') {
1651
+ return isNonArrayObjectLikeType(checker, declaredType);
1652
+ }
1653
+
1654
+ const runtimeType = checker.getTypeAtLocation(typeExpression);
1655
+ return checker.isTypeAssignableTo(runtimeType, declaredType);
1656
+ }
1657
+
1581
1658
  // Classifies a constructor-based type expression by its identifier name.
1582
1659
  function typeExpressionKind(expression) {
1583
1660
  if (!expression) return undefined;
@@ -1732,11 +1809,7 @@ function validateDefaultValue(checker, typeExpression, valueExpression) {
1732
1809
  }
1733
1810
 
1734
1811
  if (typeKind === 'Object') {
1735
- const isObjectLike =
1736
- Boolean(valueType.flags & ts.TypeFlags.Object) &&
1737
- !checker.isArrayType(valueType) &&
1738
- !checker.isTupleType(valueType);
1739
- if (!isObjectLike) {
1812
+ if (!isNonArrayObjectLikeType(checker, valueType)) {
1740
1813
  return {typeName: 'Object', valueTypeName};
1741
1814
  }
1742
1815
  }
@@ -1865,6 +1938,8 @@ function validateHtmlNesting(node, findings) {
1865
1938
  // Validates all configured component property metadata entries.
1866
1939
  function validatePropertyConfigs(
1867
1940
  checker,
1941
+ sourceFile,
1942
+ declaredPropertyTypes,
1868
1943
  supportedProps,
1869
1944
  propertyEntries,
1870
1945
  getterNames,
@@ -1872,9 +1947,10 @@ function validatePropertyConfigs(
1872
1947
  findings
1873
1948
  ) {
1874
1949
  for (const {config, propName} of propertyEntries) {
1950
+ const computedProp = getObjectProperty(config, 'computed');
1951
+ const declaredTypeNode = declaredPropertyTypes.get(propName);
1875
1952
  const typeProp = getObjectProperty(config, 'type');
1876
1953
  const usedByProp = getObjectProperty(config, 'usedBy');
1877
- const computedProp = getObjectProperty(config, 'computed');
1878
1954
  const valueProp = getObjectProperty(config, 'value');
1879
1955
  const valuesProp = getObjectProperty(config, 'values');
1880
1956
 
@@ -1894,6 +1970,15 @@ function validatePropertyConfigs(
1894
1970
  `property "${propName}" type must be one of ` +
1895
1971
  'Boolean, Number, String, Object, or Array'
1896
1972
  );
1973
+ } else if (declaredTypeNode) {
1974
+ if (!typeExpressionMatchesDeclaredType(checker, typeExpression, declaredTypeNode)) {
1975
+ findings.incompatibleDeclareTypes.push(
1976
+ `property "${propName}" declare type ` +
1977
+ `"${getPropertyTypeTextFromNode(sourceFile, declaredTypeNode)}" ` +
1978
+ `is not compatible with static properties type ` +
1979
+ `"${getPropertyConfigTypeName(typeExpressionKind(typeExpression))}"`
1980
+ );
1981
+ }
1897
1982
  }
1898
1983
 
1899
1984
  if (usedByProp && ts.isPropertyAssignment(usedByProp)) {