wrec 0.24.6 → 0.24.7
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 +112 -47
package/package.json
CHANGED
package/scripts/lint.js
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// This linter checks Wrec components for:
|
|
4
|
-
// - duplicate property names
|
|
5
|
-
// - invalid computed property references and non-method calls
|
|
6
|
-
// - invalid default values
|
|
7
|
-
// - invalid form-assoc values
|
|
8
|
-
// - invalid event handler references
|
|
9
|
-
// - invalid useState map entries
|
|
10
|
-
// - invalid usedBy references
|
|
11
|
-
// - invalid values configurations
|
|
12
|
-
// - missing formAssociated property
|
|
13
|
-
// - missing type properties in property configurations
|
|
14
|
-
// - reserved property names
|
|
15
|
-
// - undefined context functions called in expressions
|
|
16
|
-
// - undefined instance methods called in expressions
|
|
3
|
+
// This linter checks Wrec components for these issues:
|
|
17
4
|
// - undefined properties accessed in expressions
|
|
18
|
-
// -
|
|
19
|
-
// -
|
|
20
|
-
// -
|
|
5
|
+
// - undefined instance methods called in expressions
|
|
6
|
+
// - undefined context functions called in expressions
|
|
7
|
+
// - incompatible method arguments in expressions
|
|
21
8
|
// - arithmetic type errors in expressions
|
|
9
|
+
// - invalid computed property references and calls to non-method members
|
|
10
|
+
// - invalid event handler references
|
|
11
|
+
// - unsupported event names
|
|
12
|
+
// - duplicate property names
|
|
13
|
+
// - reserved property names
|
|
14
|
+
// - missing `type` in property configurations
|
|
15
|
+
// - invalid default values
|
|
16
|
+
// - invalid `values` configurations
|
|
17
|
+
// - invalid `usedBy` references
|
|
18
|
+
// - missing `formAssociated` when `formAssociatedCallback` is defined
|
|
19
|
+
// - invalid `form-assoc` values
|
|
20
|
+
// - invalid `useState` map entries
|
|
21
|
+
// - unsupported HTML attributes in templates
|
|
22
22
|
|
|
23
23
|
import fs from 'node:fs';
|
|
24
24
|
import path from 'node:path';
|
|
@@ -49,7 +49,19 @@ const HTML_TAG_ATTRIBUTES = new Map([
|
|
|
49
49
|
['fieldset', new Set(['name'])],
|
|
50
50
|
['form', new Set(['action', 'method', 'name'])],
|
|
51
51
|
['img', new Set(['alt', 'height', 'src', 'width'])],
|
|
52
|
-
[
|
|
52
|
+
[
|
|
53
|
+
'input',
|
|
54
|
+
new Set([
|
|
55
|
+
'checked',
|
|
56
|
+
'max',
|
|
57
|
+
'min',
|
|
58
|
+
'name',
|
|
59
|
+
'placeholder',
|
|
60
|
+
'step',
|
|
61
|
+
'type',
|
|
62
|
+
'value'
|
|
63
|
+
])
|
|
64
|
+
],
|
|
53
65
|
['label', new Set(['for'])],
|
|
54
66
|
['legend', new Set([])],
|
|
55
67
|
['li', new Set(['value'])],
|
|
@@ -486,7 +498,10 @@ function extractProperties(sourceFile, checker, classNode) {
|
|
|
486
498
|
continue;
|
|
487
499
|
}
|
|
488
500
|
|
|
489
|
-
if (
|
|
501
|
+
if (
|
|
502
|
+
supportedProps.has(propName) &&
|
|
503
|
+
!duplicateProperties.includes(propName)
|
|
504
|
+
) {
|
|
490
505
|
duplicateProperties.push(propName);
|
|
491
506
|
}
|
|
492
507
|
if (
|
|
@@ -542,7 +557,11 @@ function extractProperties(sourceFile, checker, classNode) {
|
|
|
542
557
|
};
|
|
543
558
|
}
|
|
544
559
|
|
|
545
|
-
function extractTemplateExpressions(
|
|
560
|
+
function extractTemplateExpressions(
|
|
561
|
+
classNode,
|
|
562
|
+
findings,
|
|
563
|
+
componentPropertyMaps
|
|
564
|
+
) {
|
|
546
565
|
const expressions = [];
|
|
547
566
|
|
|
548
567
|
for (const member of classNode.members) {
|
|
@@ -645,7 +664,8 @@ function formatReport(
|
|
|
645
664
|
fileLabel = filePath,
|
|
646
665
|
showFileHeader = true,
|
|
647
666
|
showDetailsForCleanFile = true,
|
|
648
|
-
showNoIssuesMessage = true
|
|
667
|
+
showNoIssuesMessage = true,
|
|
668
|
+
verbose = false
|
|
649
669
|
} = options;
|
|
650
670
|
const lines = [];
|
|
651
671
|
|
|
@@ -671,13 +691,13 @@ function formatReport(
|
|
|
671
691
|
|
|
672
692
|
if (showFileHeader) lines.push(`file: ${fileLabel}`);
|
|
673
693
|
|
|
674
|
-
if (hasIssues || showDetailsForCleanFile) {
|
|
694
|
+
if (verbose && (hasIssues || showDetailsForCleanFile)) {
|
|
675
695
|
lines.push('properties:');
|
|
676
696
|
if (supportedProps.size === 0) {
|
|
677
697
|
lines.push(' none');
|
|
678
698
|
} else {
|
|
679
|
-
for (const [name, info] of [...supportedProps.entries()].sort(
|
|
680
|
-
a.localeCompare(b)
|
|
699
|
+
for (const [name, info] of [...supportedProps.entries()].sort(
|
|
700
|
+
([a], [b]) => a.localeCompare(b)
|
|
681
701
|
)) {
|
|
682
702
|
lines.push(` ${name}: ${info.typeText}`);
|
|
683
703
|
}
|
|
@@ -688,7 +708,9 @@ function formatReport(
|
|
|
688
708
|
lines.push(' none');
|
|
689
709
|
} else {
|
|
690
710
|
allExpressions.forEach(expr => {
|
|
691
|
-
lines.push(
|
|
711
|
+
lines.push(
|
|
712
|
+
` [${expr.kind}]${formatLocation(expr.location)} ${expr.text}`
|
|
713
|
+
);
|
|
692
714
|
});
|
|
693
715
|
}
|
|
694
716
|
}
|
|
@@ -726,12 +748,16 @@ function formatReport(
|
|
|
726
748
|
|
|
727
749
|
if (findings.invalidDefaultValues.length > 0) {
|
|
728
750
|
lines.push('invalid default values:');
|
|
729
|
-
findings.invalidDefaultValues.forEach(message =>
|
|
751
|
+
findings.invalidDefaultValues.forEach(message =>
|
|
752
|
+
lines.push(` ${message}`)
|
|
753
|
+
);
|
|
730
754
|
}
|
|
731
755
|
|
|
732
756
|
if (findings.invalidFormAssocValues.length > 0) {
|
|
733
757
|
lines.push('invalid form-assoc values:');
|
|
734
|
-
findings.invalidFormAssocValues.forEach(message =>
|
|
758
|
+
findings.invalidFormAssocValues.forEach(message =>
|
|
759
|
+
lines.push(` ${message}`)
|
|
760
|
+
);
|
|
735
761
|
}
|
|
736
762
|
|
|
737
763
|
if (findings.missingFormAssociatedProperty.length > 0) {
|
|
@@ -743,7 +769,9 @@ function formatReport(
|
|
|
743
769
|
|
|
744
770
|
if (findings.missingTypeProperties.length > 0) {
|
|
745
771
|
lines.push('missing type properties:');
|
|
746
|
-
findings.missingTypeProperties.forEach(message =>
|
|
772
|
+
findings.missingTypeProperties.forEach(message =>
|
|
773
|
+
lines.push(` ${message}`)
|
|
774
|
+
);
|
|
747
775
|
}
|
|
748
776
|
|
|
749
777
|
if (findings.undefinedProperties.length > 0) {
|
|
@@ -763,7 +791,9 @@ function formatReport(
|
|
|
763
791
|
|
|
764
792
|
if (findings.invalidEventHandlers.length > 0) {
|
|
765
793
|
lines.push('invalid event handler references:');
|
|
766
|
-
findings.invalidEventHandlers.forEach(message =>
|
|
794
|
+
findings.invalidEventHandlers.forEach(message =>
|
|
795
|
+
lines.push(` ${message}`)
|
|
796
|
+
);
|
|
767
797
|
}
|
|
768
798
|
|
|
769
799
|
if (findings.invalidUseStateMaps.length > 0) {
|
|
@@ -796,7 +826,9 @@ function formatReport(
|
|
|
796
826
|
|
|
797
827
|
if (findings.unsupportedEventNames.length > 0) {
|
|
798
828
|
lines.push('unsupported event names:');
|
|
799
|
-
findings.unsupportedEventNames.forEach(message =>
|
|
829
|
+
findings.unsupportedEventNames.forEach(message =>
|
|
830
|
+
lines.push(` ${message}`)
|
|
831
|
+
);
|
|
800
832
|
}
|
|
801
833
|
|
|
802
834
|
if (!hasIssues && showNoIssuesMessage) lines.push('no issues found');
|
|
@@ -822,7 +854,9 @@ function getComponentPropertyMaps(filePath, sourceText, seen = new Set()) {
|
|
|
822
854
|
const propertyMaps = new Map();
|
|
823
855
|
|
|
824
856
|
for (const classNode of collectWrecClasses(sourceFile)) {
|
|
825
|
-
const tagName = classNode.name
|
|
857
|
+
const tagName = classNode.name
|
|
858
|
+
? tagNames.get(classNode.name.text)
|
|
859
|
+
: undefined;
|
|
826
860
|
if (!tagName) continue;
|
|
827
861
|
const {supportedProps} = extractProperties(sourceFile, checker, classNode);
|
|
828
862
|
propertyMaps.set(tagName, new Set(supportedProps.keys()));
|
|
@@ -927,7 +961,10 @@ function getStringArrayLiteral(property) {
|
|
|
927
961
|
|
|
928
962
|
const values = [];
|
|
929
963
|
for (const element of property.initializer.elements) {
|
|
930
|
-
if (
|
|
964
|
+
if (
|
|
965
|
+
!ts.isStringLiteral(element) &&
|
|
966
|
+
!ts.isNoSubstitutionTemplateLiteral(element)
|
|
967
|
+
) {
|
|
931
968
|
return undefined;
|
|
932
969
|
}
|
|
933
970
|
values.push(element.text);
|
|
@@ -949,7 +986,10 @@ function getParameterType(checker, parameterSymbol, location, isRestArgument) {
|
|
|
949
986
|
location
|
|
950
987
|
);
|
|
951
988
|
if (!isRestArgument) return parameterType;
|
|
952
|
-
if (
|
|
989
|
+
if (
|
|
990
|
+
!checker.isArrayType(parameterType) &&
|
|
991
|
+
!checker.isTupleType(parameterType)
|
|
992
|
+
) {
|
|
953
993
|
return parameterType;
|
|
954
994
|
}
|
|
955
995
|
|
|
@@ -1251,7 +1291,13 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1251
1291
|
findings.unsupportedHtmlAttributes.sort();
|
|
1252
1292
|
findings.unsupportedEventNames.sort();
|
|
1253
1293
|
|
|
1254
|
-
return formatReport(
|
|
1294
|
+
return formatReport(
|
|
1295
|
+
filePath,
|
|
1296
|
+
supportedProps,
|
|
1297
|
+
allExpressions,
|
|
1298
|
+
findings,
|
|
1299
|
+
options
|
|
1300
|
+
);
|
|
1255
1301
|
}
|
|
1256
1302
|
|
|
1257
1303
|
export function lintFile(filePath, options = {}) {
|
|
@@ -1260,15 +1306,21 @@ export function lintFile(filePath, options = {}) {
|
|
|
1260
1306
|
}
|
|
1261
1307
|
|
|
1262
1308
|
function main() {
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1309
|
+
const args = process.argv.slice(2);
|
|
1310
|
+
const verbose = args.includes('--verbose');
|
|
1311
|
+
const positionalArgs = args.filter(arg => arg !== '--verbose');
|
|
1312
|
+
|
|
1313
|
+
if (positionalArgs.length > 1) {
|
|
1265
1314
|
fail('usage: node scripts/wrec-lint.js [file.js|file.ts]');
|
|
1266
1315
|
}
|
|
1267
1316
|
|
|
1317
|
+
const [filePath] = positionalArgs;
|
|
1318
|
+
|
|
1268
1319
|
if (filePath) {
|
|
1269
1320
|
process.stdout.write(
|
|
1270
1321
|
lintFile(validateFilePath(filePath), {
|
|
1271
|
-
showFileHeader: false
|
|
1322
|
+
showFileHeader: false,
|
|
1323
|
+
verbose
|
|
1272
1324
|
})
|
|
1273
1325
|
);
|
|
1274
1326
|
return;
|
|
@@ -1278,9 +1330,11 @@ function main() {
|
|
|
1278
1330
|
let previousHadIssues = false;
|
|
1279
1331
|
findWrecFiles(rootDir, matchedFile => {
|
|
1280
1332
|
const report = lintFile(matchedFile, {
|
|
1281
|
-
fileLabel:
|
|
1333
|
+
fileLabel:
|
|
1334
|
+
path.relative(rootDir, matchedFile) || path.basename(matchedFile),
|
|
1282
1335
|
showDetailsForCleanFile: false,
|
|
1283
|
-
showNoIssuesMessage: false
|
|
1336
|
+
showNoIssuesMessage: false,
|
|
1337
|
+
verbose
|
|
1284
1338
|
});
|
|
1285
1339
|
const currentHasIssues = report.trim().includes('\n');
|
|
1286
1340
|
if (previousHadIssues) process.stdout.write('\n');
|
|
@@ -1308,7 +1362,10 @@ function validateComputedProperty(
|
|
|
1308
1362
|
) {
|
|
1309
1363
|
for (const match of computedText.matchAll(THIS_REF_RE)) {
|
|
1310
1364
|
const referencedName = match[1];
|
|
1311
|
-
if (
|
|
1365
|
+
if (
|
|
1366
|
+
!supportedProps.has(referencedName) &&
|
|
1367
|
+
!classMethods.has(referencedName)
|
|
1368
|
+
) {
|
|
1312
1369
|
findings.invalidComputedProperties.push(
|
|
1313
1370
|
`property "${propName}" computed references missing property "${referencedName}"`
|
|
1314
1371
|
);
|
|
@@ -1334,21 +1391,27 @@ function validateDefaultValue(checker, typeExpression, valueExpression) {
|
|
|
1334
1391
|
const valueTypeName = checker.typeToString(valueType);
|
|
1335
1392
|
|
|
1336
1393
|
if (typeKind === 'String') {
|
|
1337
|
-
if (
|
|
1394
|
+
if (
|
|
1395
|
+
!(valueType.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral))
|
|
1396
|
+
) {
|
|
1338
1397
|
return {typeName: 'string', valueTypeName};
|
|
1339
1398
|
}
|
|
1340
1399
|
return undefined;
|
|
1341
1400
|
}
|
|
1342
1401
|
|
|
1343
1402
|
if (typeKind === 'Number') {
|
|
1344
|
-
if (
|
|
1403
|
+
if (
|
|
1404
|
+
!(valueType.flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral))
|
|
1405
|
+
) {
|
|
1345
1406
|
return {typeName: 'number', valueTypeName};
|
|
1346
1407
|
}
|
|
1347
1408
|
return undefined;
|
|
1348
1409
|
}
|
|
1349
1410
|
|
|
1350
1411
|
if (typeKind === 'Boolean') {
|
|
1351
|
-
if (
|
|
1412
|
+
if (
|
|
1413
|
+
!(valueType.flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral))
|
|
1414
|
+
) {
|
|
1352
1415
|
return {typeName: 'boolean', valueTypeName};
|
|
1353
1416
|
}
|
|
1354
1417
|
return undefined;
|
|
@@ -1401,7 +1464,9 @@ function validatePropertyConfigs(
|
|
|
1401
1464
|
const valuesProp = getObjectProperty(config, 'values');
|
|
1402
1465
|
|
|
1403
1466
|
const typeExpression =
|
|
1404
|
-
typeProp && ts.isPropertyAssignment(typeProp)
|
|
1467
|
+
typeProp && ts.isPropertyAssignment(typeProp)
|
|
1468
|
+
? typeProp.initializer
|
|
1469
|
+
: undefined;
|
|
1405
1470
|
|
|
1406
1471
|
if (!typeExpression) {
|
|
1407
1472
|
findings.missingTypeProperties.push(
|
|
@@ -1517,9 +1582,9 @@ function validateFormAssocAttribute(attrName, attrValue, findings) {
|
|
|
1517
1582
|
const pairs = attrValue.split(',');
|
|
1518
1583
|
for (const pair of pairs) {
|
|
1519
1584
|
const trimmed = pair.trim();
|
|
1520
|
-
const [propName, fieldName, ...rest] = trimmed
|
|
1521
|
-
|
|
1522
|
-
|
|
1585
|
+
const [propName, fieldName, ...rest] = trimmed
|
|
1586
|
+
.split(':')
|
|
1587
|
+
.map(part => part.trim());
|
|
1523
1588
|
if (!trimmed || rest.length > 0 || !propName || !fieldName) {
|
|
1524
1589
|
findings.invalidFormAssocValues.push(
|
|
1525
1590
|
`form-assoc="${attrValue}" is invalid; expected "property:field" or a comma-separated list of them`
|