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.
- package/package.json +1 -1
- package/scripts/lint.js +198 -29
package/package.json
CHANGED
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
|
|
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);
|