wrec 0.24.6 → 0.24.8
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 +140 -47
package/package.json
CHANGED
package/scripts/lint.js
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
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
|
+
// - extra arguments passed to methods and context functions
|
|
8
|
+
// - incompatible method arguments in expressions
|
|
21
9
|
// - arithmetic type errors in expressions
|
|
10
|
+
// - invalid computed property references and calls to non-method members
|
|
11
|
+
// - invalid event handler references
|
|
12
|
+
// - unsupported event names
|
|
13
|
+
// - duplicate property names
|
|
14
|
+
// - reserved property names
|
|
15
|
+
// - missing `type` in property configurations
|
|
16
|
+
// - invalid default values
|
|
17
|
+
// - invalid `values` configurations
|
|
18
|
+
// - invalid `usedBy` references
|
|
19
|
+
// - missing `formAssociated` when `formAssociatedCallback` is defined
|
|
20
|
+
// - invalid `form-assoc` values
|
|
21
|
+
// - invalid `useState` map entries
|
|
22
|
+
// - unsupported HTML attributes in templates
|
|
22
23
|
|
|
23
24
|
import fs from 'node:fs';
|
|
24
25
|
import path from 'node:path';
|
|
@@ -49,7 +50,19 @@ const HTML_TAG_ATTRIBUTES = new Map([
|
|
|
49
50
|
['fieldset', new Set(['name'])],
|
|
50
51
|
['form', new Set(['action', 'method', 'name'])],
|
|
51
52
|
['img', new Set(['alt', 'height', 'src', 'width'])],
|
|
52
|
-
[
|
|
53
|
+
[
|
|
54
|
+
'input',
|
|
55
|
+
new Set([
|
|
56
|
+
'checked',
|
|
57
|
+
'max',
|
|
58
|
+
'min',
|
|
59
|
+
'name',
|
|
60
|
+
'placeholder',
|
|
61
|
+
'step',
|
|
62
|
+
'type',
|
|
63
|
+
'value'
|
|
64
|
+
])
|
|
65
|
+
],
|
|
53
66
|
['label', new Set(['for'])],
|
|
54
67
|
['legend', new Set([])],
|
|
55
68
|
['li', new Set(['value'])],
|
|
@@ -161,6 +174,17 @@ function analyzeExpression(
|
|
|
161
174
|
.dotDotDotToken
|
|
162
175
|
);
|
|
163
176
|
|
|
177
|
+
if (!isRest && node.arguments.length > parameters.length) {
|
|
178
|
+
node.arguments.slice(parameters.length).forEach((argument, index) => {
|
|
179
|
+
findings.extraArguments.push({
|
|
180
|
+
argument: toUserFacingExpression(argument.getText()),
|
|
181
|
+
argumentIndex: parameters.length + index + 1,
|
|
182
|
+
methodName: toUserFacingExpression(callee.getText()),
|
|
183
|
+
parameterCount: parameters.length
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
164
188
|
node.arguments.forEach((argument, index) => {
|
|
165
189
|
let parameterSymbol = parameters[index];
|
|
166
190
|
let isRestArgument =
|
|
@@ -486,7 +510,10 @@ function extractProperties(sourceFile, checker, classNode) {
|
|
|
486
510
|
continue;
|
|
487
511
|
}
|
|
488
512
|
|
|
489
|
-
if (
|
|
513
|
+
if (
|
|
514
|
+
supportedProps.has(propName) &&
|
|
515
|
+
!duplicateProperties.includes(propName)
|
|
516
|
+
) {
|
|
490
517
|
duplicateProperties.push(propName);
|
|
491
518
|
}
|
|
492
519
|
if (
|
|
@@ -542,7 +569,11 @@ function extractProperties(sourceFile, checker, classNode) {
|
|
|
542
569
|
};
|
|
543
570
|
}
|
|
544
571
|
|
|
545
|
-
function extractTemplateExpressions(
|
|
572
|
+
function extractTemplateExpressions(
|
|
573
|
+
classNode,
|
|
574
|
+
findings,
|
|
575
|
+
componentPropertyMaps
|
|
576
|
+
) {
|
|
546
577
|
const expressions = [];
|
|
547
578
|
|
|
548
579
|
for (const member of classNode.members) {
|
|
@@ -645,7 +676,8 @@ function formatReport(
|
|
|
645
676
|
fileLabel = filePath,
|
|
646
677
|
showFileHeader = true,
|
|
647
678
|
showDetailsForCleanFile = true,
|
|
648
|
-
showNoIssuesMessage = true
|
|
679
|
+
showNoIssuesMessage = true,
|
|
680
|
+
verbose = false
|
|
649
681
|
} = options;
|
|
650
682
|
const lines = [];
|
|
651
683
|
|
|
@@ -663,6 +695,7 @@ function formatReport(
|
|
|
663
695
|
findings.undefinedProperties.length > 0 ||
|
|
664
696
|
findings.undefinedContextFunctions.length > 0 ||
|
|
665
697
|
findings.undefinedMethods.length > 0 ||
|
|
698
|
+
findings.extraArguments.length > 0 ||
|
|
666
699
|
findings.incompatibleArguments.length > 0 ||
|
|
667
700
|
findings.invalidEventHandlers.length > 0 ||
|
|
668
701
|
findings.unsupportedHtmlAttributes.length > 0 ||
|
|
@@ -671,13 +704,13 @@ function formatReport(
|
|
|
671
704
|
|
|
672
705
|
if (showFileHeader) lines.push(`file: ${fileLabel}`);
|
|
673
706
|
|
|
674
|
-
if (hasIssues || showDetailsForCleanFile) {
|
|
707
|
+
if (verbose && (hasIssues || showDetailsForCleanFile)) {
|
|
675
708
|
lines.push('properties:');
|
|
676
709
|
if (supportedProps.size === 0) {
|
|
677
710
|
lines.push(' none');
|
|
678
711
|
} else {
|
|
679
|
-
for (const [name, info] of [...supportedProps.entries()].sort(
|
|
680
|
-
a.localeCompare(b)
|
|
712
|
+
for (const [name, info] of [...supportedProps.entries()].sort(
|
|
713
|
+
([a], [b]) => a.localeCompare(b)
|
|
681
714
|
)) {
|
|
682
715
|
lines.push(` ${name}: ${info.typeText}`);
|
|
683
716
|
}
|
|
@@ -688,7 +721,9 @@ function formatReport(
|
|
|
688
721
|
lines.push(' none');
|
|
689
722
|
} else {
|
|
690
723
|
allExpressions.forEach(expr => {
|
|
691
|
-
lines.push(
|
|
724
|
+
lines.push(
|
|
725
|
+
` [${expr.kind}]${formatLocation(expr.location)} ${expr.text}`
|
|
726
|
+
);
|
|
692
727
|
});
|
|
693
728
|
}
|
|
694
729
|
}
|
|
@@ -726,12 +761,16 @@ function formatReport(
|
|
|
726
761
|
|
|
727
762
|
if (findings.invalidDefaultValues.length > 0) {
|
|
728
763
|
lines.push('invalid default values:');
|
|
729
|
-
findings.invalidDefaultValues.forEach(message =>
|
|
764
|
+
findings.invalidDefaultValues.forEach(message =>
|
|
765
|
+
lines.push(` ${message}`)
|
|
766
|
+
);
|
|
730
767
|
}
|
|
731
768
|
|
|
732
769
|
if (findings.invalidFormAssocValues.length > 0) {
|
|
733
770
|
lines.push('invalid form-assoc values:');
|
|
734
|
-
findings.invalidFormAssocValues.forEach(message =>
|
|
771
|
+
findings.invalidFormAssocValues.forEach(message =>
|
|
772
|
+
lines.push(` ${message}`)
|
|
773
|
+
);
|
|
735
774
|
}
|
|
736
775
|
|
|
737
776
|
if (findings.missingFormAssociatedProperty.length > 0) {
|
|
@@ -743,7 +782,9 @@ function formatReport(
|
|
|
743
782
|
|
|
744
783
|
if (findings.missingTypeProperties.length > 0) {
|
|
745
784
|
lines.push('missing type properties:');
|
|
746
|
-
findings.missingTypeProperties.forEach(message =>
|
|
785
|
+
findings.missingTypeProperties.forEach(message =>
|
|
786
|
+
lines.push(` ${message}`)
|
|
787
|
+
);
|
|
747
788
|
}
|
|
748
789
|
|
|
749
790
|
if (findings.undefinedProperties.length > 0) {
|
|
@@ -763,7 +804,9 @@ function formatReport(
|
|
|
763
804
|
|
|
764
805
|
if (findings.invalidEventHandlers.length > 0) {
|
|
765
806
|
lines.push('invalid event handler references:');
|
|
766
|
-
findings.invalidEventHandlers.forEach(message =>
|
|
807
|
+
findings.invalidEventHandlers.forEach(message =>
|
|
808
|
+
lines.push(` ${message}`)
|
|
809
|
+
);
|
|
767
810
|
}
|
|
768
811
|
|
|
769
812
|
if (findings.invalidUseStateMaps.length > 0) {
|
|
@@ -771,6 +814,15 @@ function formatReport(
|
|
|
771
814
|
findings.invalidUseStateMaps.forEach(message => lines.push(` ${message}`));
|
|
772
815
|
}
|
|
773
816
|
|
|
817
|
+
if (findings.extraArguments.length > 0) {
|
|
818
|
+
lines.push('extra arguments:');
|
|
819
|
+
findings.extraArguments.forEach(finding => {
|
|
820
|
+
lines.push(
|
|
821
|
+
` ${finding.methodName}: argument ${finding.argumentIndex} "${finding.argument}" exceeds the ${finding.parameterCount}-parameter signature`
|
|
822
|
+
);
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
774
826
|
if (findings.incompatibleArguments.length > 0) {
|
|
775
827
|
lines.push('incompatible arguments:');
|
|
776
828
|
findings.incompatibleArguments.forEach(finding => {
|
|
@@ -796,7 +848,9 @@ function formatReport(
|
|
|
796
848
|
|
|
797
849
|
if (findings.unsupportedEventNames.length > 0) {
|
|
798
850
|
lines.push('unsupported event names:');
|
|
799
|
-
findings.unsupportedEventNames.forEach(message =>
|
|
851
|
+
findings.unsupportedEventNames.forEach(message =>
|
|
852
|
+
lines.push(` ${message}`)
|
|
853
|
+
);
|
|
800
854
|
}
|
|
801
855
|
|
|
802
856
|
if (!hasIssues && showNoIssuesMessage) lines.push('no issues found');
|
|
@@ -822,7 +876,9 @@ function getComponentPropertyMaps(filePath, sourceText, seen = new Set()) {
|
|
|
822
876
|
const propertyMaps = new Map();
|
|
823
877
|
|
|
824
878
|
for (const classNode of collectWrecClasses(sourceFile)) {
|
|
825
|
-
const tagName = classNode.name
|
|
879
|
+
const tagName = classNode.name
|
|
880
|
+
? tagNames.get(classNode.name.text)
|
|
881
|
+
: undefined;
|
|
826
882
|
if (!tagName) continue;
|
|
827
883
|
const {supportedProps} = extractProperties(sourceFile, checker, classNode);
|
|
828
884
|
propertyMaps.set(tagName, new Set(supportedProps.keys()));
|
|
@@ -927,7 +983,10 @@ function getStringArrayLiteral(property) {
|
|
|
927
983
|
|
|
928
984
|
const values = [];
|
|
929
985
|
for (const element of property.initializer.elements) {
|
|
930
|
-
if (
|
|
986
|
+
if (
|
|
987
|
+
!ts.isStringLiteral(element) &&
|
|
988
|
+
!ts.isNoSubstitutionTemplateLiteral(element)
|
|
989
|
+
) {
|
|
931
990
|
return undefined;
|
|
932
991
|
}
|
|
933
992
|
values.push(element.text);
|
|
@@ -949,7 +1008,10 @@ function getParameterType(checker, parameterSymbol, location, isRestArgument) {
|
|
|
949
1008
|
location
|
|
950
1009
|
);
|
|
951
1010
|
if (!isRestArgument) return parameterType;
|
|
952
|
-
if (
|
|
1011
|
+
if (
|
|
1012
|
+
!checker.isArrayType(parameterType) &&
|
|
1013
|
+
!checker.isTupleType(parameterType)
|
|
1014
|
+
) {
|
|
953
1015
|
return parameterType;
|
|
954
1016
|
}
|
|
955
1017
|
|
|
@@ -1137,6 +1199,7 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1137
1199
|
const allMethods = collectClassMethods(classNode);
|
|
1138
1200
|
const findings = {
|
|
1139
1201
|
duplicateProperties,
|
|
1202
|
+
extraArguments: [],
|
|
1140
1203
|
incompatibleArguments: [],
|
|
1141
1204
|
invalidComputedProperties: [],
|
|
1142
1205
|
invalidDefaultValues: [],
|
|
@@ -1229,6 +1292,11 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1229
1292
|
});
|
|
1230
1293
|
|
|
1231
1294
|
findings.duplicateProperties.sort();
|
|
1295
|
+
findings.extraArguments.sort(
|
|
1296
|
+
(a, b) =>
|
|
1297
|
+
a.methodName.localeCompare(b.methodName) ||
|
|
1298
|
+
a.argumentIndex - b.argumentIndex
|
|
1299
|
+
);
|
|
1232
1300
|
findings.incompatibleArguments.sort(
|
|
1233
1301
|
(a, b) =>
|
|
1234
1302
|
a.methodName.localeCompare(b.methodName) ||
|
|
@@ -1251,7 +1319,13 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1251
1319
|
findings.unsupportedHtmlAttributes.sort();
|
|
1252
1320
|
findings.unsupportedEventNames.sort();
|
|
1253
1321
|
|
|
1254
|
-
return formatReport(
|
|
1322
|
+
return formatReport(
|
|
1323
|
+
filePath,
|
|
1324
|
+
supportedProps,
|
|
1325
|
+
allExpressions,
|
|
1326
|
+
findings,
|
|
1327
|
+
options
|
|
1328
|
+
);
|
|
1255
1329
|
}
|
|
1256
1330
|
|
|
1257
1331
|
export function lintFile(filePath, options = {}) {
|
|
@@ -1260,15 +1334,21 @@ export function lintFile(filePath, options = {}) {
|
|
|
1260
1334
|
}
|
|
1261
1335
|
|
|
1262
1336
|
function main() {
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1337
|
+
const args = process.argv.slice(2);
|
|
1338
|
+
const verbose = args.includes('--verbose');
|
|
1339
|
+
const positionalArgs = args.filter(arg => arg !== '--verbose');
|
|
1340
|
+
|
|
1341
|
+
if (positionalArgs.length > 1) {
|
|
1265
1342
|
fail('usage: node scripts/wrec-lint.js [file.js|file.ts]');
|
|
1266
1343
|
}
|
|
1267
1344
|
|
|
1345
|
+
const [filePath] = positionalArgs;
|
|
1346
|
+
|
|
1268
1347
|
if (filePath) {
|
|
1269
1348
|
process.stdout.write(
|
|
1270
1349
|
lintFile(validateFilePath(filePath), {
|
|
1271
|
-
showFileHeader: false
|
|
1350
|
+
showFileHeader: false,
|
|
1351
|
+
verbose
|
|
1272
1352
|
})
|
|
1273
1353
|
);
|
|
1274
1354
|
return;
|
|
@@ -1278,9 +1358,11 @@ function main() {
|
|
|
1278
1358
|
let previousHadIssues = false;
|
|
1279
1359
|
findWrecFiles(rootDir, matchedFile => {
|
|
1280
1360
|
const report = lintFile(matchedFile, {
|
|
1281
|
-
fileLabel:
|
|
1361
|
+
fileLabel:
|
|
1362
|
+
path.relative(rootDir, matchedFile) || path.basename(matchedFile),
|
|
1282
1363
|
showDetailsForCleanFile: false,
|
|
1283
|
-
showNoIssuesMessage: false
|
|
1364
|
+
showNoIssuesMessage: false,
|
|
1365
|
+
verbose
|
|
1284
1366
|
});
|
|
1285
1367
|
const currentHasIssues = report.trim().includes('\n');
|
|
1286
1368
|
if (previousHadIssues) process.stdout.write('\n');
|
|
@@ -1308,7 +1390,10 @@ function validateComputedProperty(
|
|
|
1308
1390
|
) {
|
|
1309
1391
|
for (const match of computedText.matchAll(THIS_REF_RE)) {
|
|
1310
1392
|
const referencedName = match[1];
|
|
1311
|
-
if (
|
|
1393
|
+
if (
|
|
1394
|
+
!supportedProps.has(referencedName) &&
|
|
1395
|
+
!classMethods.has(referencedName)
|
|
1396
|
+
) {
|
|
1312
1397
|
findings.invalidComputedProperties.push(
|
|
1313
1398
|
`property "${propName}" computed references missing property "${referencedName}"`
|
|
1314
1399
|
);
|
|
@@ -1334,21 +1419,27 @@ function validateDefaultValue(checker, typeExpression, valueExpression) {
|
|
|
1334
1419
|
const valueTypeName = checker.typeToString(valueType);
|
|
1335
1420
|
|
|
1336
1421
|
if (typeKind === 'String') {
|
|
1337
|
-
if (
|
|
1422
|
+
if (
|
|
1423
|
+
!(valueType.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral))
|
|
1424
|
+
) {
|
|
1338
1425
|
return {typeName: 'string', valueTypeName};
|
|
1339
1426
|
}
|
|
1340
1427
|
return undefined;
|
|
1341
1428
|
}
|
|
1342
1429
|
|
|
1343
1430
|
if (typeKind === 'Number') {
|
|
1344
|
-
if (
|
|
1431
|
+
if (
|
|
1432
|
+
!(valueType.flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral))
|
|
1433
|
+
) {
|
|
1345
1434
|
return {typeName: 'number', valueTypeName};
|
|
1346
1435
|
}
|
|
1347
1436
|
return undefined;
|
|
1348
1437
|
}
|
|
1349
1438
|
|
|
1350
1439
|
if (typeKind === 'Boolean') {
|
|
1351
|
-
if (
|
|
1440
|
+
if (
|
|
1441
|
+
!(valueType.flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral))
|
|
1442
|
+
) {
|
|
1352
1443
|
return {typeName: 'boolean', valueTypeName};
|
|
1353
1444
|
}
|
|
1354
1445
|
return undefined;
|
|
@@ -1401,7 +1492,9 @@ function validatePropertyConfigs(
|
|
|
1401
1492
|
const valuesProp = getObjectProperty(config, 'values');
|
|
1402
1493
|
|
|
1403
1494
|
const typeExpression =
|
|
1404
|
-
typeProp && ts.isPropertyAssignment(typeProp)
|
|
1495
|
+
typeProp && ts.isPropertyAssignment(typeProp)
|
|
1496
|
+
? typeProp.initializer
|
|
1497
|
+
: undefined;
|
|
1405
1498
|
|
|
1406
1499
|
if (!typeExpression) {
|
|
1407
1500
|
findings.missingTypeProperties.push(
|
|
@@ -1517,9 +1610,9 @@ function validateFormAssocAttribute(attrName, attrValue, findings) {
|
|
|
1517
1610
|
const pairs = attrValue.split(',');
|
|
1518
1611
|
for (const pair of pairs) {
|
|
1519
1612
|
const trimmed = pair.trim();
|
|
1520
|
-
const [propName, fieldName, ...rest] = trimmed
|
|
1521
|
-
|
|
1522
|
-
|
|
1613
|
+
const [propName, fieldName, ...rest] = trimmed
|
|
1614
|
+
.split(':')
|
|
1615
|
+
.map(part => part.trim());
|
|
1523
1616
|
if (!trimmed || rest.length > 0 || !propName || !fieldName) {
|
|
1524
1617
|
findings.invalidFormAssocValues.push(
|
|
1525
1618
|
`form-assoc="${attrValue}" is invalid; expected "property:field" or a comma-separated list of them`
|