wrec 0.39.4 → 0.39.6

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 +198 -29
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.39.4",
5
+ "version": "0.39.6",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
package/scripts/lint.js CHANGED
@@ -16,6 +16,7 @@
16
16
  // - incompatible declare property types
17
17
  // - arithmetic and other type errors in expressions
18
18
  // - invalid computed property references
19
+ // - computed property cycles
19
20
  // - invalid event handler references
20
21
  // - unsupported event names
21
22
  // - duplicate property names
@@ -28,6 +29,7 @@
28
29
  // - missing required members like `static html` and `formAssociated`
29
30
  // - invalid `form-assoc` values
30
31
  // - invalid `useState` map entries
32
+ // - invalid native form-control value bindings
31
33
  // - unsupported HTML attributes in templates
32
34
  // - invalid HTML element nesting in templates
33
35
  // - invalid ref attribute targets
@@ -46,10 +48,33 @@ import {
46
48
  hasStaticModifier
47
49
  } from './ast-utils.js';
48
50
 
51
+ // Regular expressions
49
52
  const CSS_PROPERTY_RE = /([a-zA-Z-]+)\s*:\s*([^;}]+)/g;
50
53
  const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/;
51
54
  const PROPERTY_REF_RE = /^this\.([A-Za-z_$][\w$]*)$/;
52
55
  const REFS_TEST_RE = /this\.[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)*/;
56
+ const THIS_CALL_RE = /this\.([A-Za-z_$][\w$]*)\s*\(/g;
57
+ const THIS_REF_RE = /this\.([A-Za-z_$][\w$]*)(\.[A-Za-z_$][\w$]*)*/g;
58
+
59
+ const GETTER_PREFIX = 'get ';
60
+ const HTML_ALLOWED_CHILDREN = new Map([
61
+ ['select', new Set(['option'])],
62
+ ['table', new Set(['tbody', 'thead', 'tr'])],
63
+ ['tbody', new Set(['tr'])],
64
+ ['thead', new Set(['tr'])],
65
+ ['tr', new Set(['td', 'th'])],
66
+ ['ul', new Set(['li'])]
67
+ ]);
68
+ const HTML_ALLOWED_PARENTS = new Map([
69
+ ['legend', new Set(['fieldset'])],
70
+ ['li', new Set(['ol', 'ul'])],
71
+ ['option', new Set(['select'])],
72
+ ['tbody', new Set(['table'])],
73
+ ['td', new Set(['tr'])],
74
+ ['th', new Set(['tr'])],
75
+ ['thead', new Set(['table'])],
76
+ ['tr', new Set(['table', 'tbody', 'thead'])]
77
+ ]);
53
78
  const HTML_GLOBAL_ATTRIBUTES = new Set([
54
79
  'aria-label',
55
80
  'class',
@@ -99,37 +124,9 @@ const HTML_TAG_ATTRIBUTES = new Map([
99
124
  ['tr', new Set([])],
100
125
  ['ul', new Set([])]
101
126
  ]);
102
- const HTML_ALLOWED_PARENTS = new Map([
103
- ['legend', new Set(['fieldset'])],
104
- ['li', new Set(['ol', 'ul'])],
105
- ['option', new Set(['select'])],
106
- ['tbody', new Set(['table'])],
107
- ['td', new Set(['tr'])],
108
- ['th', new Set(['tr'])],
109
- ['thead', new Set(['table'])],
110
- ['tr', new Set(['table', 'tbody', 'thead'])]
111
- ]);
112
- const HTML_ALLOWED_CHILDREN = new Map([
113
- ['select', new Set(['option'])],
114
- ['table', new Set(['tbody', 'thead', 'tr'])],
115
- ['tbody', new Set(['tr'])],
116
- ['thead', new Set(['tr'])],
117
- ['tr', new Set(['td', 'th'])],
118
- ['ul', new Set(['li'])]
119
- ]);
120
- const THIS_CALL_RE = /this\.([A-Za-z_$][\w$]*)\s*\(/g;
121
- const THIS_REF_RE = /this\.([A-Za-z_$][\w$]*)(\.[A-Za-z_$][\w$]*)*/g;
127
+ const NATIVE_FORM_CONTROL_TAGS = new Set(['input', 'select', 'textarea']);
122
128
  const PLACEHOLDER_PREFIX = '__WREC_PLACEHOLDER__';
123
129
  const RESERVED_PROPERTY_NAMES = new Set(['class', 'style']);
124
- const SUPPORTED_PROPERTY_TYPE_NAMES = new Set([
125
- 'Array',
126
- 'Boolean',
127
- 'HTMLElement',
128
- 'Number',
129
- 'Object',
130
- 'String'
131
- ]);
132
- const GETTER_PREFIX = 'get ';
133
130
  const SUPPORTED_EVENT_NAMES = new Set([
134
131
  'blur',
135
132
  'change',
@@ -151,7 +148,16 @@ const SUPPORTED_EVENT_NAMES = new Set([
151
148
  'mouseup',
152
149
  'paste'
153
150
  ]);
151
+ const SUPPORTED_PROPERTY_TYPE_NAMES = new Set([
152
+ 'Array',
153
+ 'Boolean',
154
+ 'HTMLElement',
155
+ 'Number',
156
+ 'Object',
157
+ 'String'
158
+ ]);
154
159
  const WREC_REF_NAME = '__wrec';
160
+
155
161
  const componentPropertyCache = new Map();
156
162
 
157
163
  // Analyzes code for invalid property access,
@@ -370,6 +376,20 @@ function collectGetterNames(classNode) {
370
376
  return getters;
371
377
  }
372
378
 
379
+ // Collects computed-property dependencies on other computed properties.
380
+ function collectComputedDependencies(computedText, computedPropNames) {
381
+ const dependencies = new Set();
382
+
383
+ for (const match of computedText.matchAll(THIS_REF_RE)) {
384
+ const referencedName = match[1];
385
+ if (computedPropNames.has(referencedName)) {
386
+ dependencies.add(referencedName);
387
+ }
388
+ }
389
+
390
+ return [...dependencies].sort();
391
+ }
392
+
373
393
  // Finds the synthetic `__wrec_expr_*` helper functions that were added by
374
394
  // `buildAugmentedSource` and returns their bodies in index order.
375
395
  // This gives the linter a stable list of typed code nodes
@@ -844,6 +864,7 @@ function formatReport(
844
864
  findings.invalidTypeProperties.length > 0 ||
845
865
  findings.invalidUsedByReferences.length > 0 ||
846
866
  findings.invalidUseStateMaps.length > 0 ||
867
+ findings.invalidValueBindings.length > 0 ||
847
868
  findings.invalidValuesConfigurations.length > 0 ||
848
869
  findings.missingRequiredMembers.length > 0 ||
849
870
  findings.missingTypeProperties.length > 0 ||
@@ -981,6 +1002,13 @@ function formatReport(
981
1002
  findings.invalidUseStateMaps.forEach(message => lines.push(` ${message}`));
982
1003
  }
983
1004
 
1005
+ if (findings.invalidValueBindings.length > 0) {
1006
+ lines.push('invalid value bindings:');
1007
+ findings.invalidValueBindings.forEach(message =>
1008
+ lines.push(` ${message}`)
1009
+ );
1010
+ }
1011
+
984
1012
  if (findings.invalidValuesConfigurations.length > 0) {
985
1013
  lines.push('invalid values configurations:');
986
1014
  findings.invalidValuesConfigurations.forEach(message =>
@@ -1230,6 +1258,35 @@ function getStringArrayLiteral(property) {
1230
1258
  return values;
1231
1259
  }
1232
1260
 
1261
+ // Returns an invalid values configuration message when one is found.
1262
+ function getValuesConfigurationError(property) {
1263
+ if (!property || !ts.isPropertyAssignment(property)) return undefined;
1264
+
1265
+ const {initializer} = property;
1266
+ if (!ts.isArrayLiteralExpression(initializer)) {
1267
+ return 'values must be a literal array of strings';
1268
+ }
1269
+
1270
+ if (initializer.elements.length === 0) {
1271
+ return 'values must not be empty';
1272
+ }
1273
+
1274
+ const seenValues = new Set();
1275
+ for (const element of initializer.elements) {
1276
+ if (
1277
+ !ts.isStringLiteral(element) &&
1278
+ !ts.isNoSubstitutionTemplateLiteral(element)
1279
+ ) {
1280
+ return 'values must contain only string literals';
1281
+ }
1282
+
1283
+ if (seenValues.has(element.text)) {
1284
+ return `values contains duplicate entry "${element.text}"`;
1285
+ }
1286
+ seenValues.add(element.text);
1287
+ }
1288
+ }
1289
+
1233
1290
  // Returns either a single string value or a string array value as an array.
1234
1291
  function getStringOrStringArrayLiteral(property) {
1235
1292
  if (!property || !ts.isPropertyAssignment(property)) return undefined;
@@ -1357,6 +1414,11 @@ function isImportLikeDeclaration(node) {
1357
1414
  );
1358
1415
  }
1359
1416
 
1417
+ // Returns whether an HTML node is a native form control with a value property.
1418
+ function isNativeFormControl(node) {
1419
+ return NATIVE_FORM_CONTROL_TAGS.has(getHtmlTagName(node));
1420
+ }
1421
+
1360
1422
  // Returns whether a type represents an object-like value other than an array.
1361
1423
  function isNonArrayObjectLikeType(checker, type) {
1362
1424
  if (type.isUnion()) {
@@ -1468,6 +1530,7 @@ export function lintSource(filePath, sourceText, options = {}) {
1468
1530
  invalidTypeProperties: [],
1469
1531
  invalidUsedByReferences: [],
1470
1532
  invalidUseStateMaps: [],
1533
+ invalidValueBindings: [],
1471
1534
  invalidValuesConfigurations: [],
1472
1535
  missingRequiredMembers: [],
1473
1536
  missingTypeProperties: [],
@@ -1581,6 +1644,7 @@ export function lintSource(filePath, sourceText, options = {}) {
1581
1644
  findings.invalidTypeProperties.sort();
1582
1645
  findings.invalidUsedByReferences.sort();
1583
1646
  findings.invalidUseStateMaps.sort();
1647
+ findings.invalidValueBindings.sort();
1584
1648
  findings.invalidValuesConfigurations.sort();
1585
1649
  findings.missingRequiredMembers.sort();
1586
1650
  findings.missingTypeProperties.sort();
@@ -1803,6 +1867,34 @@ function validateCheckedBinding(
1803
1867
  );
1804
1868
  }
1805
1869
 
1870
+ // Validates value bindings on native form controls.
1871
+ function validateValueBinding(
1872
+ node,
1873
+ attrName,
1874
+ attrValue,
1875
+ findings,
1876
+ supportedProps
1877
+ ) {
1878
+ const [baseAttrName] = attrName.split(':');
1879
+ if (baseAttrName !== 'value') return;
1880
+ if (!isNativeFormControl(node)) return;
1881
+
1882
+ const propName = getPropertyNameInAttribute(attrValue);
1883
+ if (!propName) return;
1884
+
1885
+ const propInfo = supportedProps.get(propName);
1886
+ if (!propInfo) return;
1887
+
1888
+ if (propInfo.typeText === 'number' || propInfo.typeText === 'string') {
1889
+ return;
1890
+ }
1891
+
1892
+ findings.invalidValueBindings.push(
1893
+ `${getHtmlTagName(node)} attribute "${attrName}" refers to property ` +
1894
+ `"${propName}" whose type is not String or Number`
1895
+ );
1896
+ }
1897
+
1806
1898
  // Validates computed property references and method calls.
1807
1899
  function validateComputedProperty(
1808
1900
  propName,
@@ -1835,6 +1927,51 @@ function validateComputedProperty(
1835
1927
  }
1836
1928
  }
1837
1929
 
1930
+ // Validates that computed properties do not form dependency cycles.
1931
+ function validateComputedPropertyCycles(computedDependencies, findings) {
1932
+ const computedNames = [...computedDependencies.keys()].sort();
1933
+ const dependencyCountMap = new Map();
1934
+ const dependentsMap = new Map();
1935
+ const queue = [];
1936
+
1937
+ for (const computedName of computedNames) {
1938
+ const dependencies = computedDependencies.get(computedName) ?? [];
1939
+ dependencyCountMap.set(computedName, dependencies.length);
1940
+ if (dependencies.length === 0) queue.push(computedName);
1941
+
1942
+ for (const dependencyName of dependencies) {
1943
+ let dependents = dependentsMap.get(dependencyName);
1944
+ if (!dependents) {
1945
+ dependents = [];
1946
+ dependentsMap.set(dependencyName, dependents);
1947
+ }
1948
+ dependents.push(computedName);
1949
+ }
1950
+ }
1951
+
1952
+ const orderedNames = [];
1953
+ for (let index = 0; index < queue.length; index++) {
1954
+ const computedName = queue[index];
1955
+ orderedNames.push(computedName);
1956
+
1957
+ const dependents = (dependentsMap.get(computedName) ?? []).sort();
1958
+ for (const dependentName of dependents) {
1959
+ const nextCount = dependencyCountMap.get(dependentName) - 1;
1960
+ dependencyCountMap.set(dependentName, nextCount);
1961
+ if (nextCount === 0) queue.push(dependentName);
1962
+ }
1963
+ }
1964
+
1965
+ if (orderedNames.length === computedNames.length) return;
1966
+
1967
+ const cycleNames = computedNames.filter(
1968
+ computedName => dependencyCountMap.get(computedName) > 0
1969
+ );
1970
+ findings.invalidComputedProperties.push(
1971
+ `computed properties form a cycle: ${cycleNames.join(', ')}`
1972
+ );
1973
+ }
1974
+
1838
1975
  // Validates that a default value matches the declared property type.
1839
1976
  function validateDefaultValue(checker, typeExpression, valueExpression) {
1840
1977
  if (!typeExpression || !valueExpression) return undefined;
@@ -2022,6 +2159,21 @@ function validatePropertyConfigs(
2022
2159
  classMethods,
2023
2160
  findings
2024
2161
  ) {
2162
+ const computedDependencies = new Map();
2163
+ const computedPropNames = new Set();
2164
+
2165
+ for (const {config, propName} of propertyEntries) {
2166
+ const computedProp = getObjectProperty(config, 'computed');
2167
+ if (
2168
+ computedProp &&
2169
+ ts.isPropertyAssignment(computedProp) &&
2170
+ (ts.isStringLiteral(computedProp.initializer) ||
2171
+ ts.isNoSubstitutionTemplateLiteral(computedProp.initializer))
2172
+ ) {
2173
+ computedPropNames.add(propName);
2174
+ }
2175
+ }
2176
+
2025
2177
  for (const {config, propName} of propertyEntries) {
2026
2178
  const computedProp = getObjectProperty(config, 'computed');
2027
2179
  const declaredTypeNode = declaredPropertyTypes.get(propName);
@@ -2029,6 +2181,7 @@ function validatePropertyConfigs(
2029
2181
  const usedByProp = getObjectProperty(config, 'usedBy');
2030
2182
  const valueProp = getObjectProperty(config, 'value');
2031
2183
  const valuesProp = getObjectProperty(config, 'values');
2184
+ const valuesConfigError = getValuesConfigurationError(valuesProp);
2032
2185
 
2033
2186
  const typeExpression =
2034
2187
  typeProp && ts.isPropertyAssignment(typeProp)
@@ -2104,6 +2257,13 @@ function validatePropertyConfigs(
2104
2257
  (ts.isStringLiteral(computedProp.initializer) ||
2105
2258
  ts.isNoSubstitutionTemplateLiteral(computedProp.initializer))
2106
2259
  ) {
2260
+ computedDependencies.set(
2261
+ propName,
2262
+ collectComputedDependencies(
2263
+ computedProp.initializer.text,
2264
+ computedPropNames
2265
+ )
2266
+ );
2107
2267
  validateComputedProperty(
2108
2268
  propName,
2109
2269
  computedProp.initializer.text,
@@ -2113,6 +2273,12 @@ function validatePropertyConfigs(
2113
2273
  );
2114
2274
  }
2115
2275
 
2276
+ if (valuesConfigError) {
2277
+ findings.invalidValuesConfigurations.push(
2278
+ `property "${propName}" ${valuesConfigError}`
2279
+ );
2280
+ }
2281
+
2116
2282
  const values = getStringArrayLiteral(valuesProp);
2117
2283
  if (values) {
2118
2284
  if (typeExpressionKind(typeExpression) !== 'String') {
@@ -2150,6 +2316,8 @@ function validatePropertyConfigs(
2150
2316
  }
2151
2317
  }
2152
2318
  }
2319
+
2320
+ validateComputedPropertyCycles(computedDependencies, findings);
2153
2321
  }
2154
2322
 
2155
2323
  // Validates that a ref attribute targets a unique HTMLElement property.
@@ -2234,6 +2402,7 @@ function walkHtmlNode(
2234
2402
  supportedProps
2235
2403
  );
2236
2404
  validateHtmlAttribute(node, attrName, findings);
2405
+ validateValueBinding(node, attrName, attrValue, findings, supportedProps);
2237
2406
  validateValueBindingEvent(node, attrName, findings);
2238
2407
  if (attrName === 'ref') {
2239
2408
  validateRefAttribute(attrValue, supportedProps, findings, seenRefProps);