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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/lint.js +140 -47
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "wrec",
3
3
  "description": "a small library that greatly simplifies building web components",
4
4
  "author": "R. Mark Volkmann",
5
- "version": "0.24.6",
5
+ "version": "0.24.8",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
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
- // - incompatible method arguments
19
- // - unsupported HTML attributes in html templates
20
- // - unsupported event names
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
- ['input', new Set(['checked', 'max', 'min', 'name', 'placeholder', 'step', 'type', 'value'])],
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 (supportedProps.has(propName) && !duplicateProperties.includes(propName)) {
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(classNode, findings, componentPropertyMaps) {
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(([a], [b]) =>
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(` [${expr.kind}]${formatLocation(expr.location)} ${expr.text}`);
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 => lines.push(` ${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 => lines.push(` ${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 => lines.push(` ${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 => lines.push(` ${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 => lines.push(` ${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 ? tagNames.get(classNode.name.text) : undefined;
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 (!ts.isStringLiteral(element) && !ts.isNoSubstitutionTemplateLiteral(element)) {
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 (!checker.isArrayType(parameterType) && !checker.isTupleType(parameterType)) {
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(filePath, supportedProps, allExpressions, findings, options);
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 [, , filePath, ...rest] = process.argv;
1264
- if (rest.length > 0) {
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: path.relative(rootDir, matchedFile) || path.basename(matchedFile),
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 (!supportedProps.has(referencedName) && !classMethods.has(referencedName)) {
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 (!(valueType.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral))) {
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 (!(valueType.flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral))) {
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 (!(valueType.flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral))) {
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) ? typeProp.initializer : undefined;
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.split(':').map(part =>
1521
- part.trim()
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`