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 +1 -1
- package/package.json +1 -1
- package/scripts/lint.js +78 -27
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
|
|
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
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
|
|
401
|
-
|
|
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
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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') {
|