wrec 0.24.9 → 0.24.10
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 +113 -16
package/package.json
CHANGED
package/scripts/lint.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// - invalid `form-assoc` values
|
|
21
21
|
// - invalid `useState` map entries
|
|
22
22
|
// - unsupported HTML attributes in templates
|
|
23
|
+
// - invalid HTML element nesting in templates
|
|
23
24
|
|
|
24
25
|
import fs from 'node:fs';
|
|
25
26
|
import path from 'node:path';
|
|
@@ -79,6 +80,24 @@ const HTML_TAG_ATTRIBUTES = new Map([
|
|
|
79
80
|
['tr', new Set([])],
|
|
80
81
|
['ul', new Set([])]
|
|
81
82
|
]);
|
|
83
|
+
const HTML_ALLOWED_PARENTS = new Map([
|
|
84
|
+
['legend', new Set(['fieldset'])],
|
|
85
|
+
['li', new Set(['ol', 'ul'])],
|
|
86
|
+
['option', new Set(['select'])],
|
|
87
|
+
['tbody', new Set(['table'])],
|
|
88
|
+
['td', new Set(['tr'])],
|
|
89
|
+
['th', new Set(['tr'])],
|
|
90
|
+
['thead', new Set(['table'])],
|
|
91
|
+
['tr', new Set(['table', 'tbody', 'thead'])]
|
|
92
|
+
]);
|
|
93
|
+
const HTML_ALLOWED_CHILDREN = new Map([
|
|
94
|
+
['select', new Set(['option'])],
|
|
95
|
+
['table', new Set(['tbody', 'thead', 'tr'])],
|
|
96
|
+
['tbody', new Set(['tr'])],
|
|
97
|
+
['thead', new Set(['tr'])],
|
|
98
|
+
['tr', new Set(['td', 'th'])],
|
|
99
|
+
['ul', new Set(['li'])]
|
|
100
|
+
]);
|
|
82
101
|
const THIS_CALL_RE = /this\.([A-Za-z_$][\w$]*)\s*\(/g;
|
|
83
102
|
const THIS_REF_RE = /this\.([A-Za-z_$][\w$]*)(\.[A-Za-z_$][\w$]*)*/g;
|
|
84
103
|
const PLACEHOLDER_PREFIX = '__WREC_PLACEHOLDER__';
|
|
@@ -348,6 +367,32 @@ function collectWrecClasses(sourceFile) {
|
|
|
348
367
|
return classes;
|
|
349
368
|
}
|
|
350
369
|
|
|
370
|
+
function collectSupportedPropertyNames(classNode) {
|
|
371
|
+
const supportedProps = new Set();
|
|
372
|
+
|
|
373
|
+
for (const member of classNode.members) {
|
|
374
|
+
if (!isStaticMember(member)) continue;
|
|
375
|
+
if (!ts.isPropertyDeclaration(member)) continue;
|
|
376
|
+
|
|
377
|
+
const name = getMemberName(member);
|
|
378
|
+
if (
|
|
379
|
+
name !== 'properties' ||
|
|
380
|
+
!member.initializer ||
|
|
381
|
+
!ts.isObjectLiteralExpression(member.initializer)
|
|
382
|
+
) {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
for (const property of member.initializer.properties) {
|
|
387
|
+
if (!ts.isPropertyAssignment(property)) continue;
|
|
388
|
+
const propName = getMemberName(property);
|
|
389
|
+
if (propName) supportedProps.add(propName);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return supportedProps;
|
|
394
|
+
}
|
|
395
|
+
|
|
351
396
|
function findDefinedTagNames(sourceFile) {
|
|
352
397
|
const tagNames = new Map();
|
|
353
398
|
|
|
@@ -698,6 +743,7 @@ function formatReport(
|
|
|
698
743
|
findings.extraArguments.length > 0 ||
|
|
699
744
|
findings.incompatibleArguments.length > 0 ||
|
|
700
745
|
findings.invalidEventHandlers.length > 0 ||
|
|
746
|
+
findings.invalidHtmlNesting.length > 0 ||
|
|
701
747
|
findings.unsupportedHtmlAttributes.length > 0 ||
|
|
702
748
|
findings.unsupportedEventNames.length > 0 ||
|
|
703
749
|
findings.typeErrors.length > 0;
|
|
@@ -814,6 +860,11 @@ function formatReport(
|
|
|
814
860
|
findings.invalidUseStateMaps.forEach(message => lines.push(` ${message}`));
|
|
815
861
|
}
|
|
816
862
|
|
|
863
|
+
if (findings.invalidHtmlNesting.length > 0) {
|
|
864
|
+
lines.push('invalid html nesting:');
|
|
865
|
+
findings.invalidHtmlNesting.forEach(message => lines.push(` ${message}`));
|
|
866
|
+
}
|
|
867
|
+
|
|
817
868
|
if (findings.extraArguments.length > 0) {
|
|
818
869
|
lines.push('extra arguments:');
|
|
819
870
|
findings.extraArguments.forEach(finding => {
|
|
@@ -867,11 +918,14 @@ function getComponentPropertyMaps(filePath, sourceText, seen = new Set()) {
|
|
|
867
918
|
seen.add(resolved);
|
|
868
919
|
|
|
869
920
|
const text = sourceText ?? fs.readFileSync(resolved, 'utf8');
|
|
870
|
-
const
|
|
871
|
-
const sourceFile =
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
921
|
+
const scriptKind = resolved.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS;
|
|
922
|
+
const sourceFile = ts.createSourceFile(
|
|
923
|
+
resolved,
|
|
924
|
+
text,
|
|
925
|
+
ts.ScriptTarget.ESNext,
|
|
926
|
+
true,
|
|
927
|
+
scriptKind
|
|
928
|
+
);
|
|
875
929
|
const tagNames = findDefinedTagNames(sourceFile);
|
|
876
930
|
const propertyMaps = new Map();
|
|
877
931
|
|
|
@@ -880,8 +934,7 @@ function getComponentPropertyMaps(filePath, sourceText, seen = new Set()) {
|
|
|
880
934
|
? tagNames.get(classNode.name.text)
|
|
881
935
|
: undefined;
|
|
882
936
|
if (!tagName) continue;
|
|
883
|
-
|
|
884
|
-
propertyMaps.set(tagName, new Set(supportedProps.keys()));
|
|
937
|
+
propertyMaps.set(tagName, collectSupportedPropertyNames(classNode));
|
|
885
938
|
}
|
|
886
939
|
|
|
887
940
|
for (const statement of sourceFile.statements) {
|
|
@@ -1128,10 +1181,15 @@ function isNumericLikeType(type) {
|
|
|
1128
1181
|
|
|
1129
1182
|
function isWrecComponentFile(filePath) {
|
|
1130
1183
|
const sourceText = fs.readFileSync(filePath, 'utf8');
|
|
1131
|
-
const
|
|
1132
|
-
const sourceFile =
|
|
1133
|
-
|
|
1134
|
-
|
|
1184
|
+
const scriptKind = filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS;
|
|
1185
|
+
const sourceFile = ts.createSourceFile(
|
|
1186
|
+
filePath,
|
|
1187
|
+
sourceText,
|
|
1188
|
+
ts.ScriptTarget.ESNext,
|
|
1189
|
+
true,
|
|
1190
|
+
scriptKind
|
|
1191
|
+
);
|
|
1192
|
+
return collectWrecClasses(sourceFile).length > 0;
|
|
1135
1193
|
}
|
|
1136
1194
|
|
|
1137
1195
|
function isStaticMember(node) {
|
|
@@ -1183,7 +1241,7 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1183
1241
|
if (!sourceFile) throw new Error(`unable to parse ${filePath}`);
|
|
1184
1242
|
|
|
1185
1243
|
const checker = baseProgram.getTypeChecker();
|
|
1186
|
-
const classNode =
|
|
1244
|
+
const classNode = collectWrecClasses(sourceFile)[0];
|
|
1187
1245
|
if (!classNode) throw new Error('file must define a subclass of Wrec');
|
|
1188
1246
|
const componentPropertyMaps = getComponentPropertyMaps(filePath, sourceText);
|
|
1189
1247
|
|
|
@@ -1205,6 +1263,7 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1205
1263
|
invalidDefaultValues: [],
|
|
1206
1264
|
invalidEventHandlers: [],
|
|
1207
1265
|
invalidFormAssocValues: [],
|
|
1266
|
+
invalidHtmlNesting: [],
|
|
1208
1267
|
invalidUseStateMaps: [],
|
|
1209
1268
|
invalidUsedByReferences: [],
|
|
1210
1269
|
invalidValuesConfigurations: [],
|
|
@@ -1243,10 +1302,7 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1243
1302
|
if (!augmentedSourceFile) throw new Error(`unable to analyze ${filePath}`);
|
|
1244
1303
|
|
|
1245
1304
|
const augmentedChecker = augmentedProgram.getTypeChecker();
|
|
1246
|
-
const augmentedClassNode =
|
|
1247
|
-
augmentedSourceFile,
|
|
1248
|
-
augmentedChecker
|
|
1249
|
-
);
|
|
1305
|
+
const augmentedClassNode = collectWrecClasses(augmentedSourceFile)[0];
|
|
1250
1306
|
if (!augmentedClassNode) {
|
|
1251
1307
|
throw new Error('unable to find Wrec subclass after augmentation');
|
|
1252
1308
|
}
|
|
@@ -1306,6 +1362,7 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1306
1362
|
findings.invalidDefaultValues.sort();
|
|
1307
1363
|
findings.invalidEventHandlers.sort();
|
|
1308
1364
|
findings.invalidFormAssocValues.sort();
|
|
1365
|
+
findings.invalidHtmlNesting.sort();
|
|
1309
1366
|
findings.invalidUseStateMaps.sort();
|
|
1310
1367
|
findings.invalidUsedByReferences.sort();
|
|
1311
1368
|
findings.invalidValuesConfigurations.sort();
|
|
@@ -1677,8 +1734,48 @@ function validateValueBindingEvent(node, attrName, findings) {
|
|
|
1677
1734
|
);
|
|
1678
1735
|
}
|
|
1679
1736
|
|
|
1737
|
+
function getHtmlTagName(node) {
|
|
1738
|
+
const tagName = node.rawTagName || node.tagName;
|
|
1739
|
+
return typeof tagName === 'string' ? tagName.toLowerCase() : '';
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function validateHtmlNesting(node, findings) {
|
|
1743
|
+
const tagName = getHtmlTagName(node);
|
|
1744
|
+
if (!tagName || tagName.includes('-')) return;
|
|
1745
|
+
|
|
1746
|
+
const parentNode = node.parentNode?.nodeType === 1 ? node.parentNode : null;
|
|
1747
|
+
const parentTagName = parentNode ? getHtmlTagName(parentNode) : '';
|
|
1748
|
+
const allowedParents = HTML_ALLOWED_PARENTS.get(tagName);
|
|
1749
|
+
if (
|
|
1750
|
+
allowedParents &&
|
|
1751
|
+
(!parentTagName || !allowedParents.has(parentTagName))
|
|
1752
|
+
) {
|
|
1753
|
+
findings.invalidHtmlNesting.push(
|
|
1754
|
+
`<${tagName}> must be nested inside ${[...allowedParents]
|
|
1755
|
+
.map(name => `<${name}>`)
|
|
1756
|
+
.join(' or ')}, but parent is ${parentTagName ? `<${parentTagName}>` : 'the document root'}`
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
const allowedChildren = HTML_ALLOWED_CHILDREN.get(tagName);
|
|
1761
|
+
if (!allowedChildren) return;
|
|
1762
|
+
|
|
1763
|
+
for (const child of node.childNodes ?? []) {
|
|
1764
|
+
if (child.nodeType !== 1) continue;
|
|
1765
|
+
const childTagName = getHtmlTagName(child);
|
|
1766
|
+
if (!childTagName || childTagName.includes('-')) continue;
|
|
1767
|
+
if (allowedChildren.has(childTagName)) continue;
|
|
1768
|
+
|
|
1769
|
+
findings.invalidHtmlNesting.push(
|
|
1770
|
+
`<${childTagName}> is not allowed directly inside <${tagName}>`
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1680
1775
|
function walkHtmlNode(node, expressions, findings, componentPropertyMaps) {
|
|
1681
1776
|
if (node.nodeType === 1) {
|
|
1777
|
+
validateHtmlNesting(node, findings);
|
|
1778
|
+
|
|
1682
1779
|
for (const [attrName, attrValue] of Object.entries(node.attributes)) {
|
|
1683
1780
|
if (!attrValue) continue;
|
|
1684
1781
|
validateFormAssocAttribute(attrName, attrValue, findings);
|