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 +1 -1
- package/package.json +1 -1
- package/scripts/lint.js +103 -32
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) {
|
|
@@ -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
|
-
|
|
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
|
-
);
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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') {
|