wrec 0.40.2 → 0.42.0

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/scripts/lint.js CHANGED
@@ -37,16 +37,16 @@
37
37
  // - checkbox checked bindings that do not reference Boolean properties
38
38
  // - radio checked bindings that do not reference String properties
39
39
 
40
- import fs from 'node:fs';
41
- import path from 'node:path';
42
- import ts from 'typescript';
43
- import {parse} from 'node-html-parser';
40
+ import fs from "node:fs";
41
+ import path from "node:path";
42
+ import ts from "typescript";
43
+ import { parse } from "node-html-parser";
44
44
  import {
45
45
  collectWrecClasses,
46
46
  getMemberName,
47
47
  getPropertyAssignmentNames,
48
- hasStaticModifier
49
- } from './ast-utils.js';
48
+ hasStaticModifier,
49
+ } from "./ast-utils.js";
50
50
 
51
51
  // Regular expressions
52
52
  const CSS_PROPERTY_RE = /([a-zA-Z-]+)\s*:\s*([^;}]+)/g;
@@ -56,107 +56,95 @@ const REFS_TEST_RE = /this\.[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)*/;
56
56
  const THIS_CALL_RE = /this\.([A-Za-z_$][\w$]*)\s*\(/g;
57
57
  const THIS_REF_RE = /this\.([A-Za-z_$][\w$]*)(\.[A-Za-z_$][\w$]*)*/g;
58
58
 
59
- const GETTER_PREFIX = 'get ';
59
+ const GETTER_PREFIX = "get ";
60
60
  const HTML_ALLOWED_CHILDREN = new Map([
61
- ['select', new Set(['option'])],
62
- ['table', new Set(['tbody', 'thead', 'tr'])],
63
- ['tbody', new Set(['tr'])],
64
- ['thead', new Set(['tr'])],
65
- ['tr', new Set(['td', 'th'])],
66
- ['ul', new Set(['li'])]
61
+ ["select", new Set(["option"])],
62
+ ["table", new Set(["tbody", "thead", "tr"])],
63
+ ["tbody", new Set(["tr"])],
64
+ ["thead", new Set(["tr"])],
65
+ ["tr", new Set(["td", "th"])],
66
+ ["ul", new Set(["li"])],
67
67
  ]);
68
68
  const HTML_ALLOWED_PARENTS = new Map([
69
- ['legend', new Set(['fieldset'])],
70
- ['li', new Set(['ol', 'ul'])],
71
- ['option', new Set(['select'])],
72
- ['tbody', new Set(['table'])],
73
- ['td', new Set(['tr'])],
74
- ['th', new Set(['tr'])],
75
- ['thead', new Set(['table'])],
76
- ['tr', new Set(['table', 'tbody', 'thead'])]
69
+ ["legend", new Set(["fieldset"])],
70
+ ["li", new Set(["ol", "ul"])],
71
+ ["option", new Set(["select"])],
72
+ ["tbody", new Set(["table"])],
73
+ ["td", new Set(["tr"])],
74
+ ["th", new Set(["tr"])],
75
+ ["thead", new Set(["table"])],
76
+ ["tr", new Set(["table", "tbody", "thead"])],
77
77
  ]);
78
78
  const HTML_GLOBAL_ATTRIBUTES = new Set([
79
- 'aria-label',
80
- 'class',
81
- 'disabled',
82
- 'hidden',
83
- 'id',
84
- 'part',
85
- 'role',
86
- 'slot',
87
- 'style',
88
- 'tabindex',
89
- 'title'
79
+ "aria-label",
80
+ "class",
81
+ "disabled",
82
+ "hidden",
83
+ "id",
84
+ "part",
85
+ "role",
86
+ "slot",
87
+ "style",
88
+ "tabindex",
89
+ "title",
90
90
  ]);
91
91
  const HTML_TAG_ATTRIBUTES = new Map([
92
- ['a', new Set(['href', 'rel', 'target'])],
93
- ['button', new Set(['name', 'type', 'value'])],
94
- ['div', new Set([])],
95
- ['fieldset', new Set(['name'])],
96
- ['form', new Set(['action', 'method', 'name'])],
97
- ['img', new Set(['alt', 'height', 'src', 'width'])],
98
- [
99
- 'input',
100
- new Set([
101
- 'checked',
102
- 'max',
103
- 'min',
104
- 'name',
105
- 'placeholder',
106
- 'step',
107
- 'type',
108
- 'value'
109
- ])
110
- ],
111
- ['label', new Set(['for'])],
112
- ['legend', new Set([])],
113
- ['li', new Set(['value'])],
114
- ['option', new Set(['label', 'selected', 'value'])],
115
- ['p', new Set([])],
116
- ['select', new Set(['multiple', 'name', 'value'])],
117
- ['span', new Set([])],
118
- ['table', new Set([])],
119
- ['tbody', new Set([])],
120
- ['td', new Set(['colspan', 'rowspan'])],
121
- ['textarea', new Set(['name', 'placeholder', 'rows', 'value'])],
122
- ['th', new Set(['colspan', 'rowspan', 'scope'])],
123
- ['thead', new Set([])],
124
- ['tr', new Set([])],
125
- ['ul', new Set([])]
92
+ ["a", new Set(["href", "rel", "target"])],
93
+ ["button", new Set(["name", "type", "value"])],
94
+ ["div", new Set([])],
95
+ ["fieldset", new Set(["name"])],
96
+ ["form", new Set(["action", "method", "name"])],
97
+ ["img", new Set(["alt", "height", "src", "width"])],
98
+ ["input", new Set(["checked", "max", "min", "name", "placeholder", "step", "type", "value"])],
99
+ ["label", new Set(["for"])],
100
+ ["legend", new Set([])],
101
+ ["li", new Set(["value"])],
102
+ ["option", new Set(["label", "selected", "value"])],
103
+ ["p", new Set([])],
104
+ ["select", new Set(["multiple", "name", "value"])],
105
+ ["span", new Set([])],
106
+ ["table", new Set([])],
107
+ ["tbody", new Set([])],
108
+ ["td", new Set(["colspan", "rowspan"])],
109
+ ["textarea", new Set(["name", "placeholder", "rows", "value"])],
110
+ ["th", new Set(["colspan", "rowspan", "scope"])],
111
+ ["thead", new Set([])],
112
+ ["tr", new Set([])],
113
+ ["ul", new Set([])],
126
114
  ]);
127
- const NATIVE_FORM_CONTROL_TAGS = new Set(['input', 'select', 'textarea']);
128
- const PLACEHOLDER_PREFIX = '__WREC_PLACEHOLDER__';
129
- const RESERVED_PROPERTY_NAMES = new Set(['class', 'style']);
115
+ const NATIVE_FORM_CONTROL_TAGS = new Set(["input", "select", "textarea"]);
116
+ const PLACEHOLDER_PREFIX = "__WREC_PLACEHOLDER__";
117
+ const RESERVED_PROPERTY_NAMES = new Set(["class", "style"]);
130
118
  const SUPPORTED_EVENT_NAMES = new Set([
131
- 'blur',
132
- 'change',
133
- 'click',
134
- 'dblclick',
135
- 'focus',
136
- 'focusin',
137
- 'focusout',
138
- 'input',
139
- 'keydown',
140
- 'keypress',
141
- 'keyup',
142
- 'mousedown',
143
- 'mouseenter',
144
- 'mouseleave',
145
- 'mousemove',
146
- 'mouseout',
147
- 'mouseover',
148
- 'mouseup',
149
- 'paste'
119
+ "blur",
120
+ "change",
121
+ "click",
122
+ "dblclick",
123
+ "focus",
124
+ "focusin",
125
+ "focusout",
126
+ "input",
127
+ "keydown",
128
+ "keypress",
129
+ "keyup",
130
+ "mousedown",
131
+ "mouseenter",
132
+ "mouseleave",
133
+ "mousemove",
134
+ "mouseout",
135
+ "mouseover",
136
+ "mouseup",
137
+ "paste",
150
138
  ]);
151
139
  const SUPPORTED_PROPERTY_TYPE_NAMES = new Set([
152
- 'Array',
153
- 'Boolean',
154
- 'HTMLElement',
155
- 'Number',
156
- 'Object',
157
- 'String'
140
+ "Array",
141
+ "Boolean",
142
+ "HTMLElement",
143
+ "Number",
144
+ "Object",
145
+ "String",
158
146
  ]);
159
- const WREC_REF_NAME = '__wrec';
147
+ const WREC_REF_NAME = "__wrec";
160
148
 
161
149
  const componentPropertyCache = new Map();
162
150
 
@@ -169,10 +157,10 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
169
157
  findings.invalidEventHandlers,
170
158
  metadata.location
171
159
  ? {
172
- location: metadata.location,
173
- message: `"${codeNode.text}" is not a defined instance method`
174
- }
175
- : `"${codeNode.text}" is not a defined instance method`
160
+ location: metadata.location,
161
+ message: `"${codeNode.text}" is not a defined instance method`,
162
+ }
163
+ : `"${codeNode.text}" is not a defined instance method`,
176
164
  );
177
165
  }
178
166
  }
@@ -187,12 +175,12 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
187
175
  if (isCallCallee(node)) {
188
176
  uniquePush(
189
177
  findings.undefinedMethods,
190
- metadata.location ? {location: metadata.location, message: name} : name
178
+ metadata.location ? { location: metadata.location, message: name } : name,
191
179
  );
192
180
  } else {
193
181
  uniquePush(
194
182
  findings.undefinedProperties,
195
- metadata.location ? {location: metadata.location, message: name} : name
183
+ metadata.location ? { location: metadata.location, message: name } : name,
196
184
  );
197
185
  }
198
186
  }
@@ -207,33 +195,27 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
207
195
  uniquePush(
208
196
  findings.undefinedContextFunctions,
209
197
  metadata.location
210
- ? {location: metadata.location, message: callee.text}
211
- : callee.text
198
+ ? { location: metadata.location, message: callee.text }
199
+ : callee.text,
212
200
  );
213
201
  }
214
202
  }
215
- } else if (
216
- ts.isPropertyAccessExpression(callee) &&
217
- isWrecRooted(callee.expression)
218
- ) {
203
+ } else if (ts.isPropertyAccessExpression(callee) && isWrecRooted(callee.expression)) {
219
204
  const ownerType = checker.getTypeAtLocation(callee.expression);
220
205
  const symbol = ownerType.getProperty(callee.name.text);
221
206
  if (!symbol) {
222
207
  uniquePush(
223
208
  findings.undefinedMethods,
224
209
  metadata.location
225
- ? {location: metadata.location, message: callee.name.text}
226
- : callee.name.text
210
+ ? { location: metadata.location, message: callee.name.text }
211
+ : callee.name.text,
227
212
  );
228
213
  }
229
214
  }
230
215
 
231
216
  const signature =
232
217
  checker.getResolvedSignature(node) ??
233
- checker.getSignaturesOfType(
234
- checker.getTypeAtLocation(callee),
235
- ts.SignatureKind.Call
236
- )[0];
218
+ checker.getSignaturesOfType(checker.getTypeAtLocation(callee), ts.SignatureKind.Call)[0];
237
219
 
238
220
  if (signature) {
239
221
  const parameters = signature.getParameters();
@@ -242,10 +224,7 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
242
224
  declaration &&
243
225
  ts.isFunctionLike(declaration) &&
244
226
  declaration.parameters.length > 0 &&
245
- Boolean(
246
- declaration.parameters[declaration.parameters.length - 1]
247
- .dotDotDotToken
248
- );
227
+ Boolean(declaration.parameters[declaration.parameters.length - 1].dotDotDotToken);
249
228
 
250
229
  if (!isRest && node.arguments.length > parameters.length) {
251
230
  node.arguments.slice(parameters.length).forEach((argument, index) => {
@@ -254,7 +233,7 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
254
233
  argumentIndex: parameters.length + index + 1,
255
234
  location: metadata.location ?? null,
256
235
  methodName: toUserFacingExpression(callee.getText()),
257
- parameterCount: parameters.length
236
+ parameterCount: parameters.length,
258
237
  });
259
238
  });
260
239
  }
@@ -262,9 +241,7 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
262
241
  node.arguments.forEach((argument, index) => {
263
242
  let parameterSymbol = parameters[index];
264
243
  let isRestArgument =
265
- Boolean(isRest) &&
266
- parameters.length > 0 &&
267
- index >= parameters.length - 1;
244
+ Boolean(isRest) && parameters.length > 0 && index >= parameters.length - 1;
268
245
  if (!parameterSymbol && isRest && parameters.length > 0) {
269
246
  parameterSymbol = parameters[parameters.length - 1];
270
247
  }
@@ -275,7 +252,7 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
275
252
  checker,
276
253
  parameterSymbol,
277
254
  declaration ?? classNode,
278
- isRestArgument
255
+ isRestArgument,
279
256
  );
280
257
  if (!checker.isTypeAssignableTo(argumentType, parameterType)) {
281
258
  findings.incompatibleArguments.push({
@@ -284,17 +261,14 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
284
261
  location: metadata.location ?? null,
285
262
  methodName: toUserFacingExpression(callee.getText()),
286
263
  parameterName: parameterSymbol.getName(),
287
- parameterType: checker.typeToString(parameterType)
264
+ parameterType: checker.typeToString(parameterType),
288
265
  });
289
266
  }
290
267
  });
291
268
  }
292
269
  }
293
270
 
294
- if (
295
- ts.isBinaryExpression(node) &&
296
- isArithmeticOperator(node.operatorToken.kind)
297
- ) {
271
+ if (ts.isBinaryExpression(node) && isArithmeticOperator(node.operatorToken.kind)) {
298
272
  const leftType = checker.getTypeAtLocation(node.left);
299
273
  const rightType = checker.getTypeAtLocation(node.right);
300
274
 
@@ -305,7 +279,7 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
305
279
  message:
306
280
  `left operand "${toUserFacingExpression(node.left.getText())}" ` +
307
281
  `has type ${checker.typeToString(leftType)}, ` +
308
- 'but arithmetic operators require number'
282
+ "but arithmetic operators require number",
309
283
  });
310
284
  }
311
285
 
@@ -316,7 +290,7 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
316
290
  message:
317
291
  `right operand "${toUserFacingExpression(node.right.getText())}" ` +
318
292
  `has type ${checker.typeToString(rightType)}, ` +
319
- 'but arithmetic operators require number'
293
+ "but arithmetic operators require number",
320
294
  });
321
295
  }
322
296
  }
@@ -333,34 +307,27 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
333
307
  // can be analyzed as if it were normal code. This gives TypeScript
334
308
  // enough // context to understand available properties, methods, and
335
309
  // context functions when the linter validates those expressions.
336
- function buildAugmentedSource(
337
- sourceFile,
338
- classNode,
339
- supportedProps,
340
- contextKeys,
341
- codeItems
342
- ) {
310
+ function buildAugmentedSource(sourceFile, classNode, supportedProps, contextKeys, codeItems) {
343
311
  const propLines = [];
344
312
  for (const [name, info] of supportedProps.entries()) {
345
313
  propLines.push(` ${JSON.stringify(name)}: ${info.typeText};`);
346
314
  }
347
315
 
348
316
  const contextLine = contextKeys.length
349
- ? `const {${contextKeys.join(', ')}} = ${classNode.name.text}.context;`
350
- : '';
317
+ ? `const {${contextKeys.join(", ")}} = ${classNode.name.text}.context;`
318
+ : "";
351
319
 
352
320
  const helperBlocks = codeItems.map((item, index) => {
353
321
  const targetType =
354
- item.context === 'static'
322
+ item.context === "static"
355
323
  ? `typeof ${classNode.name.text}`
356
324
  : `${classNode.name.text} & __WrecSupportedProps`;
357
325
  const rewrittenText = item.text.replace(/\bthis\b/g, WREC_REF_NAME);
358
- const helperBody =
359
- item.shape === 'block' ? rewrittenText : `return (${rewrittenText});`;
326
+ const helperBody = item.shape === "block" ? rewrittenText : `return (${rewrittenText});`;
360
327
  return `
361
328
  function __wrec_expr_${index}() {
362
329
  const ${WREC_REF_NAME} = null as unknown as ${targetType};
363
- ${item.checkContextCalls ? contextLine : ''}
330
+ ${item.checkContextCalls ? contextLine : ""}
364
331
  ${helperBody}
365
332
  }
366
333
  `;
@@ -368,11 +335,11 @@ function __wrec_expr_${index}() {
368
335
 
369
336
  const propInterface = `
370
337
  type __WrecSupportedProps = {
371
- ${propLines.join('\n')}
338
+ ${propLines.join("\n")}
372
339
  };
373
340
  `;
374
341
 
375
- return `${sourceFile.text}\n${propInterface}\n${helperBlocks.join('\n')}`;
342
+ return `${sourceFile.text}\n${propInterface}\n${helperBlocks.join("\n")}`;
376
343
  }
377
344
 
378
345
  // Collects all instance method and accessor names defined in a component class.
@@ -427,10 +394,7 @@ function collectHelperCodeNodes(augmentedSourceFile) {
427
394
  // Finds generated helper functions and
428
395
  // stores their bodies by index.
429
396
  function visit(node) {
430
- if (
431
- ts.isFunctionDeclaration(node) &&
432
- node.name?.text.startsWith('__wrec_expr_')
433
- ) {
397
+ if (ts.isFunctionDeclaration(node) && node.name?.text.startsWith("__wrec_expr_")) {
434
398
  const match = node.name.text.match(/(\d+)$/);
435
399
  const index = match ? Number(match[1]) : -1;
436
400
  if (index >= 0 && node.body) helpers[index] = node.body;
@@ -455,19 +419,17 @@ function collectMethodCodeItems(classNode) {
455
419
  ts.isSetAccessorDeclaration(member)
456
420
  ) {
457
421
  if (!member.body) continue;
458
- if (!member.body.getText().includes('this.')) continue;
422
+ if (!member.body.getText().includes("this.")) continue;
459
423
  codeItems.push({
460
424
  checkContextCalls: false,
461
- context: 'instance',
425
+ context: "instance",
462
426
  eventHandler: false,
463
- kind: 'method',
427
+ kind: "method",
464
428
  location: member
465
429
  .getSourceFile()
466
- .getLineAndCharacterOfPosition(
467
- member.name.getStart(member.getSourceFile())
468
- ),
469
- shape: 'block',
470
- text: member.body.getText()
430
+ .getLineAndCharacterOfPosition(member.name.getStart(member.getSourceFile())),
431
+ shape: "block",
432
+ text: member.body.getText(),
471
433
  });
472
434
  }
473
435
  }
@@ -486,7 +448,7 @@ function collectSupportedPropertyNames(classNode) {
486
448
 
487
449
  const name = getMemberName(member);
488
450
  if (
489
- name !== 'properties' ||
451
+ name !== "properties" ||
490
452
  !member.initializer ||
491
453
  !ts.isObjectLiteralExpression(member.initializer)
492
454
  ) {
@@ -524,7 +486,7 @@ function collectUseStateMapErrors(classNode, supportedProps, findings) {
524
486
  ts.isCallExpression(node) &&
525
487
  ts.isPropertyAccessExpression(node.expression) &&
526
488
  ts.isThis(node.expression.expression) &&
527
- node.expression.name.text === 'useState'
489
+ node.expression.name.text === "useState"
528
490
  ) {
529
491
  const mapArg = node.arguments[1];
530
492
  if (mapArg && ts.isObjectLiteralExpression(mapArg)) {
@@ -541,16 +503,12 @@ function collectUseStateMapErrors(classNode, supportedProps, findings) {
541
503
 
542
504
  const componentProp = property.initializer.text;
543
505
  if (!supportedProps.has(componentProp)) {
544
- findings.invalidUseStateMaps.push(
545
- {
546
- location: node
547
- .getSourceFile()
548
- .getLineAndCharacterOfPosition(property.getStart()),
549
- message:
550
- `useState maps state property "${statePath}" to ` +
551
- `missing component property "${componentProp}"`
552
- }
553
- );
506
+ findings.invalidUseStateMaps.push({
507
+ location: node.getSourceFile().getLineAndCharacterOfPosition(property.getStart()),
508
+ message:
509
+ `useState maps state property "${statePath}" to ` +
510
+ `missing component property "${componentProp}"`,
511
+ });
554
512
  }
555
513
  }
556
514
  }
@@ -576,7 +534,7 @@ function createProgram(filePath, sourceText) {
576
534
  skipLibCheck: true,
577
535
  strict: true,
578
536
  target: ts.ScriptTarget.ESNext,
579
- useDefineForClassFields: true
537
+ useDefineForClassFields: true,
580
538
  };
581
539
 
582
540
  const host = {
@@ -585,35 +543,22 @@ function createProgram(filePath, sourceText) {
585
543
  if (path.resolve(fileName) === filePath) return true;
586
544
  return defaultHost.fileExists(fileName);
587
545
  },
588
- getSourceFile(
589
- fileName,
590
- languageVersion,
591
- onError,
592
- shouldCreateNewSourceFile
593
- ) {
546
+ getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile) {
594
547
  if (path.resolve(fileName) === filePath) {
595
- const kind = fileName.endsWith('.ts')
596
- ? ts.ScriptKind.TS
597
- : ts.ScriptKind.JS;
598
- return ts.createSourceFile(
599
- fileName,
600
- sourceText,
601
- languageVersion,
602
- true,
603
- kind
604
- );
548
+ const kind = fileName.endsWith(".ts") ? ts.ScriptKind.TS : ts.ScriptKind.JS;
549
+ return ts.createSourceFile(fileName, sourceText, languageVersion, true, kind);
605
550
  }
606
551
  return defaultHost.getSourceFile(
607
552
  fileName,
608
553
  languageVersion,
609
554
  onError,
610
- shouldCreateNewSourceFile
555
+ shouldCreateNewSourceFile,
611
556
  );
612
557
  },
613
558
  readFile(fileName) {
614
559
  if (path.resolve(fileName) === filePath) return sourceText;
615
560
  return defaultHost.readFile(fileName);
616
- }
561
+ },
617
562
  };
618
563
 
619
564
  return ts.createProgram([filePath], compilerOptions, host);
@@ -638,28 +583,19 @@ function extractProperties(sourceFile, checker, classNode) {
638
583
  const name = getMemberName(member);
639
584
  if (!name || !member.initializer) continue;
640
585
 
641
- if (
642
- name === 'context' &&
643
- ts.isObjectLiteralExpression(member.initializer)
644
- ) {
586
+ if (name === "context" && ts.isObjectLiteralExpression(member.initializer)) {
645
587
  contextKeys = member.initializer.properties
646
- .map(property => getMemberName(property))
588
+ .map((property) => getMemberName(property))
647
589
  .filter(Boolean);
648
590
  continue;
649
591
  }
650
592
 
651
- if (
652
- name === 'formAssociated' &&
653
- member.initializer.kind === ts.SyntaxKind.TrueKeyword
654
- ) {
593
+ if (name === "formAssociated" && member.initializer.kind === ts.SyntaxKind.TrueKeyword) {
655
594
  formAssociated = true;
656
595
  continue;
657
596
  }
658
597
 
659
- if (
660
- name !== 'properties' ||
661
- !ts.isObjectLiteralExpression(member.initializer)
662
- ) {
598
+ if (name !== "properties" || !ts.isObjectLiteralExpression(member.initializer)) {
663
599
  continue;
664
600
  }
665
601
 
@@ -670,13 +606,13 @@ function extractProperties(sourceFile, checker, classNode) {
670
606
  continue;
671
607
  }
672
608
  const propertyLocation = sourceFile.getLineAndCharacterOfPosition(
673
- property.name.getStart(sourceFile)
609
+ property.name.getStart(sourceFile),
674
610
  );
675
611
 
676
612
  if (
677
613
  supportedProps.has(propName) &&
678
- !duplicateProperties.some(
679
- finding => getLocatedFindingMessage(finding).startsWith(`"${propName}" `)
614
+ !duplicateProperties.some((finding) =>
615
+ getLocatedFindingMessage(finding).startsWith(`"${propName}" `),
680
616
  )
681
617
  ) {
682
618
  duplicateProperties.push({
@@ -684,18 +620,16 @@ function extractProperties(sourceFile, checker, classNode) {
684
620
  message:
685
621
  `"${propName}" first declared at ` +
686
622
  `${formatLocation(propertyLocations.get(propName))}, ` +
687
- `duplicated at ${formatLocation(propertyLocation)}`
623
+ `duplicated at ${formatLocation(propertyLocation)}`,
688
624
  });
689
625
  }
690
626
  if (
691
627
  RESERVED_PROPERTY_NAMES.has(propName) &&
692
- !reservedProperties.some(
693
- finding => getLocatedFindingMessage(finding) === propName
694
- )
628
+ !reservedProperties.some((finding) => getLocatedFindingMessage(finding) === propName)
695
629
  ) {
696
630
  reservedProperties.push({
697
631
  location: propertyLocation,
698
- message: propName
632
+ message: propName,
699
633
  });
700
634
  }
701
635
  if (!propertyLocations.has(propName)) {
@@ -703,22 +637,18 @@ function extractProperties(sourceFile, checker, classNode) {
703
637
  }
704
638
 
705
639
  const config = property.initializer;
706
- propertyEntries.push({config, propName, property});
707
- const typeProp = getObjectProperty(config, 'type');
708
- const computedProp = getObjectProperty(config, 'computed');
640
+ propertyEntries.push({ config, propName, property });
641
+ const typeProp = getObjectProperty(config, "type");
642
+ const computedProp = getObjectProperty(config, "computed");
709
643
 
710
- let typeText = 'unknown';
644
+ let typeText = "unknown";
711
645
  let typeNode;
712
646
  if (typeProp && ts.isPropertyAssignment(typeProp)) {
713
- typeText = getPropertyTypeText(
714
- checker,
715
- sourceFile,
716
- typeProp.initializer
717
- );
647
+ typeText = getPropertyTypeText(checker, sourceFile, typeProp.initializer);
718
648
  typeNode = typeNodeFromConstructorExpression(typeProp.initializer);
719
649
  }
720
650
 
721
- supportedProps.set(propName, {typeNode, typeText});
651
+ supportedProps.set(propName, { typeNode, typeText });
722
652
 
723
653
  if (
724
654
  computedProp &&
@@ -727,11 +657,11 @@ function extractProperties(sourceFile, checker, classNode) {
727
657
  ts.isNoSubstitutionTemplateLiteral(computedProp.initializer))
728
658
  ) {
729
659
  computedExprs.push({
730
- kind: 'computed',
660
+ kind: "computed",
731
661
  text: computedProp.initializer.text.trim(),
732
662
  location: sourceFile.getLineAndCharacterOfPosition(
733
- computedProp.initializer.getStart(sourceFile)
734
- )
663
+ computedProp.initializer.getStart(sourceFile),
664
+ ),
735
665
  });
736
666
  }
737
667
  }
@@ -744,17 +674,12 @@ function extractProperties(sourceFile, checker, classNode) {
744
674
  duplicateProperties,
745
675
  formAssociated,
746
676
  propertyEntries,
747
- reservedProperties
677
+ reservedProperties,
748
678
  };
749
679
  }
750
680
 
751
681
  // Extracts analyzable expressions from static html and css templates.
752
- function extractTemplateExpressions(
753
- classNode,
754
- findings,
755
- componentPropertyMaps,
756
- supportedProps
757
- ) {
682
+ function extractTemplateExpressions(classNode, findings, componentPropertyMaps, supportedProps) {
758
683
  const expressions = [];
759
684
 
760
685
  for (const member of classNode.members) {
@@ -762,66 +687,60 @@ function extractTemplateExpressions(
762
687
  if (!ts.isPropertyDeclaration(member)) continue;
763
688
 
764
689
  const name = getMemberName(member);
765
- if ((name !== 'html' && name !== 'css') || !member.initializer) continue;
690
+ if ((name !== "html" && name !== "css") || !member.initializer) continue;
766
691
  if (!ts.isTaggedTemplateExpression(member.initializer)) continue;
767
692
 
768
693
  const tag = member.initializer.tag.getText();
769
- if (tag !== 'html' && tag !== 'css') continue;
694
+ if (tag !== "html" && tag !== "css") continue;
770
695
 
771
- const {template} = member.initializer;
696
+ const { template } = member.initializer;
772
697
  if (ts.isTemplateExpression(template)) {
773
698
  for (const span of template.templateSpans) {
774
- const trimmed = getExpressionText(
775
- member.getSourceFile(),
776
- span.expression
777
- );
699
+ const trimmed = getExpressionText(member.getSourceFile(), span.expression);
778
700
  if (trimmed) {
779
701
  const location = span.expression
780
702
  .getSourceFile()
781
703
  .getLineAndCharacterOfPosition(span.expression.getStart());
782
704
  expressions.push({
783
- context: 'static',
705
+ context: "static",
784
706
  eventHandler: false,
785
707
  kind: tag,
786
708
  text: trimmed,
787
- location
709
+ location,
788
710
  });
789
711
  }
790
712
  }
791
713
  }
792
714
 
793
715
  const rendered = getTemplateLiteralText(template);
794
- const resolveLocation = createTemplateLocationResolver(
795
- member.getSourceFile(),
796
- template
797
- );
716
+ const resolveLocation = createTemplateLocationResolver(member.getSourceFile(), template);
798
717
 
799
- if (tag === 'css') {
718
+ if (tag === "css") {
800
719
  CSS_PROPERTY_RE.lastIndex = 0;
801
720
  while (true) {
802
721
  const match = CSS_PROPERTY_RE.exec(rendered);
803
722
  if (!match) break;
804
- const rawValue = match[2] ?? '';
723
+ const rawValue = match[2] ?? "";
805
724
  const value = rawValue.trim();
806
725
  if (value && REFS_TEST_RE.test(value)) {
807
726
  const valueOffsetInMatch = match[0].lastIndexOf(rawValue);
808
727
  const leadingWhitespace = rawValue.length - rawValue.trimStart().length;
809
728
  const valueLocation = resolveLocation(
810
- match.index + valueOffsetInMatch + leadingWhitespace
729
+ match.index + valueOffsetInMatch + leadingWhitespace,
811
730
  );
812
731
  expressions.push({
813
- context: 'instance',
732
+ context: "instance",
814
733
  eventHandler: false,
815
- kind: 'css',
734
+ kind: "css",
816
735
  text: value,
817
- location: valueLocation
736
+ location: valueLocation,
818
737
  });
819
738
  }
820
739
  }
821
740
  continue;
822
741
  }
823
742
 
824
- const root = parse(rendered, {comment: true});
743
+ const root = parse(rendered, { comment: true });
825
744
  walkHtmlNode(
826
745
  root,
827
746
  expressions,
@@ -829,7 +748,7 @@ function extractTemplateExpressions(
829
748
  componentPropertyMaps,
830
749
  supportedProps,
831
750
  new Set(),
832
- resolveLocation
751
+ resolveLocation,
833
752
  );
834
753
  }
835
754
 
@@ -851,7 +770,7 @@ function findDefinedTagNames(sourceFile) {
851
770
  if (
852
771
  ts.isCallExpression(node) &&
853
772
  ts.isPropertyAccessExpression(node.expression) &&
854
- node.expression.name.text === 'define' &&
773
+ node.expression.name.text === "define" &&
855
774
  ts.isIdentifier(node.expression.expression) &&
856
775
  node.arguments.length > 0 &&
857
776
  ts.isStringLiteral(node.arguments[0])
@@ -868,9 +787,9 @@ function findDefinedTagNames(sourceFile) {
868
787
  // Recursively finds Wrec component files under a directory
869
788
  // and reports each match.
870
789
  function findWrecFiles(rootDir, onMatch) {
871
- const walk = currentDir => {
790
+ const walk = (currentDir) => {
872
791
  const entries = fs
873
- .readdirSync(currentDir, {withFileTypes: true})
792
+ .readdirSync(currentDir, { withFileTypes: true })
874
793
  .sort((a, b) => a.name.localeCompare(b.name));
875
794
 
876
795
  for (const entry of entries) {
@@ -882,7 +801,7 @@ function findWrecFiles(rootDir, onMatch) {
882
801
  }
883
802
 
884
803
  if (!entry.isFile()) continue;
885
- if (!fullPath.endsWith('.js') && !fullPath.endsWith('.ts')) continue;
804
+ if (!fullPath.endsWith(".js") && !fullPath.endsWith(".ts")) continue;
886
805
  if (isWrecComponentFile(fullPath)) onMatch(fullPath);
887
806
  }
888
807
  };
@@ -898,7 +817,7 @@ function createTemplateLocationResolver(sourceFile, template) {
898
817
  segments.push({
899
818
  renderedEnd: template.text.length,
900
819
  renderedStart: 0,
901
- sourceStart: template.getStart(sourceFile) + 1
820
+ sourceStart: template.getStart(sourceFile) + 1,
902
821
  });
903
822
  } else {
904
823
  let renderedStart = 0;
@@ -906,7 +825,7 @@ function createTemplateLocationResolver(sourceFile, template) {
906
825
  segments.push({
907
826
  renderedEnd: headText.length,
908
827
  renderedStart,
909
- sourceStart: template.head.getStart(sourceFile) + 1
828
+ sourceStart: template.head.getStart(sourceFile) + 1,
910
829
  });
911
830
  renderedStart += headText.length;
912
831
 
@@ -915,29 +834,27 @@ function createTemplateLocationResolver(sourceFile, template) {
915
834
  segments.push({
916
835
  renderedEnd: renderedStart + span.literal.text.length,
917
836
  renderedStart,
918
- sourceStart: span.literal.getStart(sourceFile) + 1
837
+ sourceStart: span.literal.getStart(sourceFile) + 1,
919
838
  });
920
839
  renderedStart += span.literal.text.length;
921
840
  });
922
841
  }
923
842
 
924
- return offset => {
843
+ return (offset) => {
925
844
  const segment =
926
845
  segments.find(
927
- candidate =>
928
- offset >= candidate.renderedStart && offset <= candidate.renderedEnd
846
+ (candidate) => offset >= candidate.renderedStart && offset <= candidate.renderedEnd,
929
847
  ) ?? segments[segments.length - 1];
930
848
  if (!segment) return null;
931
849
 
932
- const sourceOffset =
933
- segment.sourceStart + Math.max(0, offset - segment.renderedStart);
850
+ const sourceOffset = segment.sourceStart + Math.max(0, offset - segment.renderedStart);
934
851
  return sourceFile.getLineAndCharacterOfPosition(sourceOffset);
935
852
  };
936
853
  }
937
854
 
938
855
  // Formats an optional source location as line and column text.
939
856
  function formatLocation(location) {
940
- if (!location) return '';
857
+ if (!location) return "";
941
858
  return `:${location.line + 1}:${location.character + 1}`;
942
859
  }
943
860
 
@@ -961,13 +878,13 @@ function getHtmlAttributeLocation(node, attrName, resolveLocation) {
961
878
 
962
879
  // Formats a lint finding that may optionally include a source location.
963
880
  function formatMaybeLocatedFinding(finding) {
964
- if (typeof finding === 'string') return finding;
881
+ if (typeof finding === "string") return finding;
965
882
  return `${formatLocation(finding.location)} ${finding.message}`.trim();
966
883
  }
967
884
 
968
885
  // Gets the message text from a lint finding that may include a location.
969
886
  function getLocatedFindingMessage(finding) {
970
- return typeof finding === 'string' ? finding : finding.message;
887
+ return typeof finding === "string" ? finding : finding.message;
971
888
  }
972
889
 
973
890
  // Compares lint findings that may optionally include source locations.
@@ -976,19 +893,13 @@ function compareLocatedFindings(a, b) {
976
893
  }
977
894
 
978
895
  // Formats the collected lint findings into the command-line report output.
979
- function formatReport(
980
- filePath,
981
- supportedProps,
982
- allExpressions,
983
- findings,
984
- options = {}
985
- ) {
896
+ function formatReport(filePath, supportedProps, allExpressions, findings, options = {}) {
986
897
  const {
987
898
  fileLabel = filePath,
988
899
  showFileHeader = true,
989
900
  showDetailsForCleanFile = true,
990
901
  showNoIssuesMessage = true,
991
- verbose = false
902
+ verbose = false,
992
903
  } = options;
993
904
  const lines = [];
994
905
 
@@ -1022,224 +933,216 @@ function formatReport(
1022
933
  if (showFileHeader) lines.push(`file: ${fileLabel}`);
1023
934
 
1024
935
  if (verbose && (hasIssues || showDetailsForCleanFile)) {
1025
- lines.push('properties:');
936
+ lines.push("properties:");
1026
937
  if (supportedProps.size === 0) {
1027
- lines.push(' none');
938
+ lines.push(" none");
1028
939
  } else {
1029
- for (const [name, info] of [...supportedProps.entries()].sort(
1030
- ([a], [b]) => a.localeCompare(b)
940
+ for (const [name, info] of [...supportedProps.entries()].sort(([a], [b]) =>
941
+ a.localeCompare(b),
1031
942
  )) {
1032
943
  lines.push(` ${name}: ${info.typeText}`);
1033
944
  }
1034
945
  }
1035
946
 
1036
- lines.push('expressions:');
947
+ lines.push("expressions:");
1037
948
  if (allExpressions.length === 0) {
1038
- lines.push(' none');
949
+ lines.push(" none");
1039
950
  } else {
1040
- allExpressions.forEach(expr => {
1041
- lines.push(
1042
- ` [${expr.kind}]${formatLocation(expr.location)} ${expr.text}`
1043
- );
951
+ allExpressions.forEach((expr) => {
952
+ lines.push(` [${expr.kind}]${formatLocation(expr.location)} ${expr.text}`);
1044
953
  });
1045
954
  }
1046
955
  }
1047
956
 
1048
957
  if (findings.duplicateProperties.length > 0) {
1049
- lines.push('duplicate properties:');
1050
- findings.duplicateProperties.forEach(finding =>
1051
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
958
+ lines.push("duplicate properties:");
959
+ findings.duplicateProperties.forEach((finding) =>
960
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1052
961
  );
1053
962
  }
1054
963
 
1055
964
  if (findings.extraArguments.length > 0) {
1056
- lines.push('extra arguments:');
1057
- findings.extraArguments.forEach(finding => {
1058
- const locationPrefix = finding.location
1059
- ? `${formatLocation(finding.location)} `
1060
- : '';
965
+ lines.push("extra arguments:");
966
+ findings.extraArguments.forEach((finding) => {
967
+ const locationPrefix = finding.location ? `${formatLocation(finding.location)} ` : "";
1061
968
  lines.push(
1062
969
  ` ${locationPrefix}${finding.methodName}: argument ${finding.argumentIndex} ` +
1063
970
  `"${finding.argument}" exceeds the ` +
1064
- `${finding.parameterCount}-parameter signature`
971
+ `${finding.parameterCount}-parameter signature`,
1065
972
  );
1066
973
  });
1067
974
  }
1068
975
 
1069
976
  if (findings.incompatibleArguments.length > 0) {
1070
- lines.push('incompatible arguments:');
1071
- findings.incompatibleArguments.forEach(finding => {
1072
- const locationPrefix = finding.location
1073
- ? `${formatLocation(finding.location)} `
1074
- : '';
977
+ lines.push("incompatible arguments:");
978
+ findings.incompatibleArguments.forEach((finding) => {
979
+ const locationPrefix = finding.location ? `${formatLocation(finding.location)} ` : "";
1075
980
  lines.push(
1076
981
  ` ${locationPrefix}${finding.methodName}: argument "${finding.argument}" ` +
1077
982
  `has type ${finding.argumentType}, but parameter ` +
1078
- `"${finding.parameterName}" expects ${finding.parameterType}`
983
+ `"${finding.parameterName}" expects ${finding.parameterType}`,
1079
984
  );
1080
985
  });
1081
986
  }
1082
987
 
1083
988
  if (findings.incompatibleDeclareTypes.length > 0) {
1084
- lines.push('incompatible declare types:');
1085
- findings.incompatibleDeclareTypes.forEach(finding =>
1086
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
989
+ lines.push("incompatible declare types:");
990
+ findings.incompatibleDeclareTypes.forEach((finding) =>
991
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1087
992
  );
1088
993
  }
1089
994
 
1090
995
  if (findings.invalidCheckedBindings.length > 0) {
1091
- lines.push('invalid checked bindings:');
1092
- findings.invalidCheckedBindings.forEach(finding =>
1093
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
996
+ lines.push("invalid checked bindings:");
997
+ findings.invalidCheckedBindings.forEach((finding) =>
998
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1094
999
  );
1095
1000
  }
1096
1001
 
1097
1002
  if (findings.invalidComputedProperties.length > 0) {
1098
- lines.push('invalid computed properties:');
1099
- findings.invalidComputedProperties.forEach(finding =>
1100
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1003
+ lines.push("invalid computed properties:");
1004
+ findings.invalidComputedProperties.forEach((finding) =>
1005
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1101
1006
  );
1102
1007
  }
1103
1008
 
1104
1009
  if (findings.invalidDefaultValues.length > 0) {
1105
- lines.push('invalid default values:');
1106
- findings.invalidDefaultValues.forEach(finding =>
1107
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1010
+ lines.push("invalid default values:");
1011
+ findings.invalidDefaultValues.forEach((finding) =>
1012
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1108
1013
  );
1109
1014
  }
1110
1015
 
1111
1016
  if (findings.invalidEventHandlers.length > 0) {
1112
- lines.push('invalid event handler references:');
1113
- findings.invalidEventHandlers.forEach(finding =>
1114
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1017
+ lines.push("invalid event handler references:");
1018
+ findings.invalidEventHandlers.forEach((finding) =>
1019
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1115
1020
  );
1116
1021
  }
1117
1022
 
1118
1023
  if (findings.invalidFormAssocValues.length > 0) {
1119
- lines.push('invalid form-assoc values:');
1120
- findings.invalidFormAssocValues.forEach(finding =>
1121
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1024
+ lines.push("invalid form-assoc values:");
1025
+ findings.invalidFormAssocValues.forEach((finding) =>
1026
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1122
1027
  );
1123
1028
  }
1124
1029
 
1125
1030
  if (findings.invalidHtmlNesting.length > 0) {
1126
- lines.push('invalid html nesting:');
1127
- findings.invalidHtmlNesting.forEach(finding =>
1128
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1031
+ lines.push("invalid html nesting:");
1032
+ findings.invalidHtmlNesting.forEach((finding) =>
1033
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1129
1034
  );
1130
1035
  }
1131
1036
 
1132
1037
  if (findings.invalidRefAttributes.length > 0) {
1133
- lines.push('invalid ref attributes:');
1134
- findings.invalidRefAttributes.forEach(finding =>
1135
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1038
+ lines.push("invalid ref attributes:");
1039
+ findings.invalidRefAttributes.forEach((finding) =>
1040
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1136
1041
  );
1137
1042
  }
1138
1043
 
1139
1044
  if (findings.invalidTypeProperties.length > 0) {
1140
- lines.push('invalid type properties:');
1141
- findings.invalidTypeProperties.forEach(finding =>
1142
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1045
+ lines.push("invalid type properties:");
1046
+ findings.invalidTypeProperties.forEach((finding) =>
1047
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1143
1048
  );
1144
1049
  }
1145
1050
 
1146
1051
  if (findings.invalidUsedByReferences.length > 0) {
1147
- lines.push('invalid usedBy references:');
1148
- findings.invalidUsedByReferences.forEach(finding =>
1149
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1052
+ lines.push("invalid usedBy references:");
1053
+ findings.invalidUsedByReferences.forEach((finding) =>
1054
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1150
1055
  );
1151
1056
  }
1152
1057
 
1153
1058
  if (findings.invalidUseStateMaps.length > 0) {
1154
- lines.push('invalid useState map entries:');
1155
- findings.invalidUseStateMaps.forEach(finding =>
1156
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1059
+ lines.push("invalid useState map entries:");
1060
+ findings.invalidUseStateMaps.forEach((finding) =>
1061
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1157
1062
  );
1158
1063
  }
1159
1064
 
1160
1065
  if (findings.invalidValueBindings.length > 0) {
1161
- lines.push('invalid value bindings:');
1162
- findings.invalidValueBindings.forEach(finding =>
1163
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1066
+ lines.push("invalid value bindings:");
1067
+ findings.invalidValueBindings.forEach((finding) =>
1068
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1164
1069
  );
1165
1070
  }
1166
1071
 
1167
1072
  if (findings.invalidValuesConfigurations.length > 0) {
1168
- lines.push('invalid values configurations:');
1169
- findings.invalidValuesConfigurations.forEach(finding =>
1170
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1073
+ lines.push("invalid values configurations:");
1074
+ findings.invalidValuesConfigurations.forEach((finding) =>
1075
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1171
1076
  );
1172
1077
  }
1173
1078
 
1174
1079
  if (findings.missingRequiredMembers.length > 0) {
1175
- lines.push('missing required members:');
1176
- findings.missingRequiredMembers.forEach(finding =>
1177
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1080
+ lines.push("missing required members:");
1081
+ findings.missingRequiredMembers.forEach((finding) =>
1082
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1178
1083
  );
1179
1084
  }
1180
1085
 
1181
1086
  if (findings.missingTypeProperties.length > 0) {
1182
- lines.push('missing type properties:');
1183
- findings.missingTypeProperties.forEach(finding =>
1184
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1087
+ lines.push("missing type properties:");
1088
+ findings.missingTypeProperties.forEach((finding) =>
1089
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1185
1090
  );
1186
1091
  }
1187
1092
 
1188
1093
  if (findings.reservedProperties.length > 0) {
1189
- lines.push('reserved property names:');
1190
- findings.reservedProperties.forEach(finding =>
1191
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1094
+ lines.push("reserved property names:");
1095
+ findings.reservedProperties.forEach((finding) =>
1096
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1192
1097
  );
1193
1098
  }
1194
1099
 
1195
1100
  if (findings.typeErrors.length > 0) {
1196
- lines.push('type errors:');
1197
- findings.typeErrors.forEach(finding => {
1198
- const locationPrefix = finding.location
1199
- ? `${formatLocation(finding.location)} `
1200
- : '';
1101
+ lines.push("type errors:");
1102
+ findings.typeErrors.forEach((finding) => {
1103
+ const locationPrefix = finding.location ? `${formatLocation(finding.location)} ` : "";
1201
1104
  lines.push(` ${locationPrefix}${finding.expression}: ${finding.message}`);
1202
1105
  });
1203
1106
  }
1204
1107
 
1205
1108
  if (findings.undefinedContextFunctions.length > 0) {
1206
- lines.push('undefined context functions:');
1207
- findings.undefinedContextFunctions.forEach(finding =>
1208
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1109
+ lines.push("undefined context functions:");
1110
+ findings.undefinedContextFunctions.forEach((finding) =>
1111
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1209
1112
  );
1210
1113
  }
1211
1114
 
1212
1115
  if (findings.undefinedMethods.length > 0) {
1213
- lines.push('undefined methods:');
1214
- findings.undefinedMethods.forEach(finding =>
1215
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1116
+ lines.push("undefined methods:");
1117
+ findings.undefinedMethods.forEach((finding) =>
1118
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1216
1119
  );
1217
1120
  }
1218
1121
 
1219
1122
  if (findings.undefinedProperties.length > 0) {
1220
- lines.push('undefined properties:');
1221
- findings.undefinedProperties.forEach(finding =>
1222
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1123
+ lines.push("undefined properties:");
1124
+ findings.undefinedProperties.forEach((finding) =>
1125
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1223
1126
  );
1224
1127
  }
1225
1128
 
1226
1129
  if (findings.unsupportedEventNames.length > 0) {
1227
- lines.push('unsupported event names:');
1228
- findings.unsupportedEventNames.forEach(finding =>
1229
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1130
+ lines.push("unsupported event names:");
1131
+ findings.unsupportedEventNames.forEach((finding) =>
1132
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1230
1133
  );
1231
1134
  }
1232
1135
 
1233
1136
  if (findings.unsupportedHtmlAttributes.length > 0) {
1234
- lines.push('unsupported html attributes:');
1235
- findings.unsupportedHtmlAttributes.forEach(finding =>
1236
- lines.push(` ${formatMaybeLocatedFinding(finding)}`)
1137
+ lines.push("unsupported html attributes:");
1138
+ findings.unsupportedHtmlAttributes.forEach((finding) =>
1139
+ lines.push(` ${formatMaybeLocatedFinding(finding)}`),
1237
1140
  );
1238
1141
  }
1239
1142
 
1240
- if (!hasIssues && showNoIssuesMessage) lines.push('no issues found');
1143
+ if (!hasIssues && showNoIssuesMessage) lines.push("no issues found");
1241
1144
 
1242
- return `${lines.join('\n')}\n`;
1145
+ return `${lines.join("\n")}\n`;
1243
1146
  }
1244
1147
 
1245
1148
  // Builds a map from tag names to supported properties
@@ -1252,40 +1155,24 @@ function getComponentPropertyMaps(filePath, sourceText, seen = new Set()) {
1252
1155
  if (seen.has(resolved)) return new Map();
1253
1156
  seen.add(resolved);
1254
1157
 
1255
- const text = sourceText ?? fs.readFileSync(resolved, 'utf8');
1256
- const scriptKind = resolved.endsWith('.ts')
1257
- ? ts.ScriptKind.TS
1258
- : ts.ScriptKind.JS;
1259
- const sourceFile = ts.createSourceFile(
1260
- resolved,
1261
- text,
1262
- ts.ScriptTarget.ESNext,
1263
- true,
1264
- scriptKind
1265
- );
1158
+ const text = sourceText ?? fs.readFileSync(resolved, "utf8");
1159
+ const scriptKind = resolved.endsWith(".ts") ? ts.ScriptKind.TS : ts.ScriptKind.JS;
1160
+ const sourceFile = ts.createSourceFile(resolved, text, ts.ScriptTarget.ESNext, true, scriptKind);
1266
1161
  const tagNames = findDefinedTagNames(sourceFile);
1267
1162
  const propertyMaps = new Map();
1268
1163
 
1269
1164
  for (const classNode of collectWrecClasses(sourceFile)) {
1270
- const tagName = classNode.name
1271
- ? tagNames.get(classNode.name.text)
1272
- : undefined;
1165
+ const tagName = classNode.name ? tagNames.get(classNode.name.text) : undefined;
1273
1166
  if (!tagName) continue;
1274
1167
  propertyMaps.set(tagName, collectSupportedPropertyNames(classNode));
1275
1168
  }
1276
1169
 
1277
1170
  for (const statement of sourceFile.statements) {
1278
- if (
1279
- !ts.isImportDeclaration(statement) ||
1280
- !ts.isStringLiteral(statement.moduleSpecifier)
1281
- ) {
1171
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
1282
1172
  continue;
1283
1173
  }
1284
1174
 
1285
- const importPath = resolveImportPath(
1286
- path.dirname(resolved),
1287
- statement.moduleSpecifier.text
1288
- );
1175
+ const importPath = resolveImportPath(path.dirname(resolved), statement.moduleSpecifier.text);
1289
1176
  if (!importPath) continue;
1290
1177
 
1291
1178
  const importedMaps = getComponentPropertyMaps(importPath, undefined, seen);
@@ -1323,22 +1210,19 @@ function getGetterName(reference) {
1323
1210
  // Returns a lowercased HTML tag name for a parsed HTML node.
1324
1211
  function getHtmlTagName(node) {
1325
1212
  const tagName = node.rawTagName || node.tagName;
1326
- return typeof tagName === 'string' ? tagName.toLowerCase() : '';
1213
+ return typeof tagName === "string" ? tagName.toLowerCase() : "";
1327
1214
  }
1328
1215
 
1329
1216
  // Returns the literal input type attribute value when one is present.
1330
1217
  function getInputType(node) {
1331
- const type = node.getAttribute('type');
1332
- return typeof type === 'string' ? type.toLowerCase() : undefined;
1218
+ const type = node.getAttribute("type");
1219
+ return typeof type === "string" ? type.toLowerCase() : undefined;
1333
1220
  }
1334
1221
 
1335
1222
  // Gets an object-literal property with the given key.
1336
1223
  function getObjectProperty(objectLiteral, key) {
1337
1224
  for (const property of objectLiteral.properties) {
1338
- if (
1339
- !ts.isPropertyAssignment(property) &&
1340
- !ts.isShorthandPropertyAssignment(property)
1341
- ) {
1225
+ if (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) {
1342
1226
  continue;
1343
1227
  }
1344
1228
  const name = getMemberName(property);
@@ -1349,15 +1233,9 @@ function getObjectProperty(objectLiteral, key) {
1349
1233
  // Resolves the effective parameter type,
1350
1234
  // including element types for rest parameters.
1351
1235
  function getParameterType(checker, parameterSymbol, location, isRestArgument) {
1352
- const parameterType = checker.getTypeOfSymbolAtLocation(
1353
- parameterSymbol,
1354
- location
1355
- );
1236
+ const parameterType = checker.getTypeOfSymbolAtLocation(parameterSymbol, location);
1356
1237
  if (!isRestArgument) return parameterType;
1357
- if (
1358
- !checker.isArrayType(parameterType) &&
1359
- !checker.isTupleType(parameterType)
1360
- ) {
1238
+ if (!checker.isArrayType(parameterType) && !checker.isTupleType(parameterType)) {
1361
1239
  return parameterType;
1362
1240
  }
1363
1241
 
@@ -1368,16 +1246,16 @@ function getParameterType(checker, parameterSymbol, location, isRestArgument) {
1368
1246
  // Returns the Wrec property config type name for a normalized type string.
1369
1247
  function getPropertyConfigTypeName(typeName) {
1370
1248
  switch (typeName) {
1371
- case 'array':
1372
- return 'Array';
1373
- case 'boolean':
1374
- return 'Boolean';
1375
- case 'number':
1376
- return 'Number';
1377
- case 'object':
1378
- return 'Object';
1379
- case 'string':
1380
- return 'String';
1249
+ case "array":
1250
+ return "Array";
1251
+ case "boolean":
1252
+ return "Boolean";
1253
+ case "number":
1254
+ return "Number";
1255
+ case "object":
1256
+ return "Object";
1257
+ case "string":
1258
+ return "String";
1381
1259
  default:
1382
1260
  return typeName;
1383
1261
  }
@@ -1413,10 +1291,7 @@ function getStringArrayLiteral(property) {
1413
1291
 
1414
1292
  const values = [];
1415
1293
  for (const element of property.initializer.elements) {
1416
- if (
1417
- !ts.isStringLiteral(element) &&
1418
- !ts.isNoSubstitutionTemplateLiteral(element)
1419
- ) {
1294
+ if (!ts.isStringLiteral(element) && !ts.isNoSubstitutionTemplateLiteral(element)) {
1420
1295
  return undefined;
1421
1296
  }
1422
1297
  values.push(element.text);
@@ -1428,22 +1303,19 @@ function getStringArrayLiteral(property) {
1428
1303
  function getValuesConfigurationError(property) {
1429
1304
  if (!property || !ts.isPropertyAssignment(property)) return undefined;
1430
1305
 
1431
- const {initializer} = property;
1306
+ const { initializer } = property;
1432
1307
  if (!ts.isArrayLiteralExpression(initializer)) {
1433
- return 'values must be a literal array of strings';
1308
+ return "values must be a literal array of strings";
1434
1309
  }
1435
1310
 
1436
1311
  if (initializer.elements.length === 0) {
1437
- return 'values must not be empty';
1312
+ return "values must not be empty";
1438
1313
  }
1439
1314
 
1440
1315
  const seenValues = new Set();
1441
1316
  for (const element of initializer.elements) {
1442
- if (
1443
- !ts.isStringLiteral(element) &&
1444
- !ts.isNoSubstitutionTemplateLiteral(element)
1445
- ) {
1446
- return 'values must contain only string literals';
1317
+ if (!ts.isStringLiteral(element) && !ts.isNoSubstitutionTemplateLiteral(element)) {
1318
+ return "values must contain only string literals";
1447
1319
  }
1448
1320
 
1449
1321
  if (seenValues.has(element.text)) {
@@ -1494,29 +1366,23 @@ function getTypeSyntaxText(sourceFile, expression) {
1494
1366
 
1495
1367
  if (ts.isIdentifier(expression)) {
1496
1368
  switch (expression.text) {
1497
- case 'String':
1498
- return 'string';
1499
- case 'Number':
1500
- return 'number';
1501
- case 'Boolean':
1502
- return 'boolean';
1503
- case 'Array':
1504
- return 'unknown[]';
1505
- case 'Object':
1506
- return 'object';
1369
+ case "String":
1370
+ return "string";
1371
+ case "Number":
1372
+ return "number";
1373
+ case "Boolean":
1374
+ return "boolean";
1375
+ case "Array":
1376
+ return "unknown[]";
1377
+ case "Object":
1378
+ return "object";
1507
1379
  default:
1508
1380
  return expression.getText(sourceFile);
1509
1381
  }
1510
1382
  }
1511
1383
 
1512
- if (
1513
- ts.isCallExpression(expression) &&
1514
- ts.isIdentifier(expression.expression)
1515
- ) {
1516
- if (
1517
- expression.expression.text === 'Array' &&
1518
- expression.typeArguments?.length === 1
1519
- ) {
1384
+ if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression)) {
1385
+ if (expression.expression.text === "Array" && expression.typeArguments?.length === 1) {
1520
1386
  return `${expression.typeArguments[0].getText(sourceFile)}[]`;
1521
1387
  }
1522
1388
  }
@@ -1533,7 +1399,7 @@ function hasStaticHtmlDefinition(classNode) {
1533
1399
  for (const member of classNode.members) {
1534
1400
  if (!hasStaticModifier(member)) continue;
1535
1401
  if (!ts.isPropertyDeclaration(member)) continue;
1536
- if (getMemberName(member) === 'html') return true;
1402
+ if (getMemberName(member) === "html") return true;
1537
1403
  }
1538
1404
  return false;
1539
1405
  }
@@ -1559,9 +1425,7 @@ function isDeclarePropertyDeclaration(member) {
1559
1425
  return (
1560
1426
  ts.isPropertyDeclaration(member) &&
1561
1427
  ts.canHaveModifiers(member) &&
1562
- ts
1563
- .getModifiers(member)
1564
- ?.some(mod => mod.kind === ts.SyntaxKind.DeclareKeyword)
1428
+ ts.getModifiers(member)?.some((mod) => mod.kind === ts.SyntaxKind.DeclareKeyword)
1565
1429
  );
1566
1430
  }
1567
1431
 
@@ -1588,15 +1452,11 @@ function isNativeFormControl(node) {
1588
1452
  // Returns whether a type represents an object-like value other than an array.
1589
1453
  function isNonArrayObjectLikeType(checker, type) {
1590
1454
  if (type.isUnion()) {
1591
- return type.types.every(member =>
1592
- isNonArrayObjectLikeType(checker, member)
1593
- );
1455
+ return type.types.every((member) => isNonArrayObjectLikeType(checker, member));
1594
1456
  }
1595
1457
 
1596
1458
  if (type.isIntersection()) {
1597
- return type.types.every(member =>
1598
- isNonArrayObjectLikeType(checker, member)
1599
- );
1459
+ return type.types.every((member) => isNonArrayObjectLikeType(checker, member));
1600
1460
  }
1601
1461
 
1602
1462
  return (
@@ -1609,7 +1469,7 @@ function isNonArrayObjectLikeType(checker, type) {
1609
1469
  // Returns whether a type is fully numeric or any-like for arithmetic checks.
1610
1470
  function isNumericLikeType(type) {
1611
1471
  const parts = type.isUnion() ? type.types : [type];
1612
- return parts.every(part => {
1472
+ return parts.every((part) => {
1613
1473
  const flags = part.flags;
1614
1474
  return Boolean(
1615
1475
  flags &
@@ -1617,23 +1477,21 @@ function isNumericLikeType(type) {
1617
1477
  ts.TypeFlags.NumberLiteral |
1618
1478
  ts.TypeFlags.BigInt |
1619
1479
  ts.TypeFlags.BigIntLiteral |
1620
- ts.TypeFlags.Any)
1480
+ ts.TypeFlags.Any),
1621
1481
  );
1622
1482
  });
1623
1483
  }
1624
1484
 
1625
1485
  // Returns whether a file defines at least one Wrec component class.
1626
1486
  function isWrecComponentFile(filePath) {
1627
- const sourceText = fs.readFileSync(filePath, 'utf8');
1628
- const scriptKind = filePath.endsWith('.ts')
1629
- ? ts.ScriptKind.TS
1630
- : ts.ScriptKind.JS;
1487
+ const sourceText = fs.readFileSync(filePath, "utf8");
1488
+ const scriptKind = filePath.endsWith(".ts") ? ts.ScriptKind.TS : ts.ScriptKind.JS;
1631
1489
  const sourceFile = ts.createSourceFile(
1632
1490
  filePath,
1633
1491
  sourceText,
1634
1492
  ts.ScriptTarget.ESNext,
1635
1493
  true,
1636
- scriptKind
1494
+ scriptKind,
1637
1495
  );
1638
1496
  return collectWrecClasses(sourceFile).length > 0;
1639
1497
  }
@@ -1643,10 +1501,7 @@ function isWrecRooted(expression) {
1643
1501
  if (ts.isIdentifier(expression) && expression.text === WREC_REF_NAME) {
1644
1502
  return true;
1645
1503
  }
1646
- if (
1647
- ts.isPropertyAccessExpression(expression) ||
1648
- ts.isElementAccessExpression(expression)
1649
- ) {
1504
+ if (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression)) {
1650
1505
  return isWrecRooted(expression.expression);
1651
1506
  }
1652
1507
  return false;
@@ -1655,7 +1510,7 @@ function isWrecRooted(expression) {
1655
1510
  // Lints a component file by path after resolving and reading it.
1656
1511
  export function lintFile(filePath, options = {}) {
1657
1512
  const resolved = validateFilePath(filePath);
1658
- return lintSource(resolved, fs.readFileSync(resolved, 'utf8'), options);
1513
+ return lintSource(resolved, fs.readFileSync(resolved, "utf8"), options);
1659
1514
  }
1660
1515
 
1661
1516
  // Lints provided component source text and returns a formatted report.
@@ -1666,7 +1521,7 @@ export function lintSource(filePath, sourceText, options = {}) {
1666
1521
 
1667
1522
  const checker = baseProgram.getTypeChecker();
1668
1523
  const classNode = collectWrecClasses(sourceFile)[0];
1669
- if (!classNode) throw new Error('file must define a subclass of Wrec');
1524
+ if (!classNode) throw new Error("file must define a subclass of Wrec");
1670
1525
  const componentPropertyMaps = getComponentPropertyMaps(filePath, sourceText);
1671
1526
 
1672
1527
  const {
@@ -1676,7 +1531,7 @@ export function lintSource(filePath, sourceText, options = {}) {
1676
1531
  duplicateProperties,
1677
1532
  formAssociated,
1678
1533
  propertyEntries,
1679
- reservedProperties
1534
+ reservedProperties,
1680
1535
  } = extractProperties(sourceFile, checker, classNode);
1681
1536
  const declaredPropertyTypes = collectDeclaredPropertyTypes(classNode);
1682
1537
  const getterNames = collectGetterNames(classNode);
@@ -1706,48 +1561,44 @@ export function lintSource(filePath, sourceText, options = {}) {
1706
1561
  undefinedMethods: [],
1707
1562
  undefinedProperties: [],
1708
1563
  unsupportedEventNames: [],
1709
- unsupportedHtmlAttributes: []
1564
+ unsupportedHtmlAttributes: [],
1710
1565
  };
1711
1566
  const templateExprs = extractTemplateExpressions(
1712
1567
  classNode,
1713
1568
  findings,
1714
1569
  componentPropertyMaps,
1715
- supportedProps
1570
+ supportedProps,
1716
1571
  );
1717
1572
  const methodCodeItems = collectMethodCodeItems(classNode);
1718
1573
  const allExpressions = [...templateExprs, ...computedExprs];
1719
- const allCodeItems = [...allExpressions, ...methodCodeItems].map(item => ({
1720
- checkContextCalls: item.kind !== 'method',
1721
- shape: 'shape' in item ? item.shape : 'expression',
1722
- ...item
1574
+ const allCodeItems = [...allExpressions, ...methodCodeItems].map((item) => ({
1575
+ checkContextCalls: item.kind !== "method",
1576
+ shape: "shape" in item ? item.shape : "expression",
1577
+ ...item,
1723
1578
  }));
1724
1579
 
1725
- if (allMethods.has('formAssociatedCallback') && !formAssociated) {
1580
+ if (allMethods.has("formAssociatedCallback") && !formAssociated) {
1726
1581
  const callbackMember = classNode.members.find(
1727
- member =>
1728
- ts.isMethodDeclaration(member) &&
1729
- getMemberName(member) === 'formAssociatedCallback'
1582
+ (member) =>
1583
+ ts.isMethodDeclaration(member) && getMemberName(member) === "formAssociatedCallback",
1730
1584
  );
1731
- findings.missingRequiredMembers.push(
1732
- {
1733
- location: callbackMember
1734
- ? callbackMember
1585
+ findings.missingRequiredMembers.push({
1586
+ location: callbackMember
1587
+ ? callbackMember
1735
1588
  .getSourceFile()
1736
1589
  .getLineAndCharacterOfPosition(
1737
- callbackMember.name.getStart(callbackMember.getSourceFile())
1590
+ callbackMember.name.getStart(callbackMember.getSourceFile()),
1738
1591
  )
1739
- : null,
1740
- message:
1741
- 'formAssociatedCallback is defined, but static formAssociated is not true'
1742
- }
1743
- );
1592
+ : null,
1593
+ message: "formAssociatedCallback is defined, but static formAssociated is not true",
1594
+ });
1744
1595
  }
1745
1596
  if (!hasStaticHtmlDefinition(classNode)) {
1746
1597
  findings.missingRequiredMembers.push({
1747
1598
  location: sourceFile.getLineAndCharacterOfPosition(
1748
- classNode.name?.getStart(sourceFile) ?? classNode.getStart(sourceFile)
1599
+ classNode.name?.getStart(sourceFile) ?? classNode.getStart(sourceFile),
1749
1600
  ),
1750
- message: 'static html property must be defined'
1601
+ message: "static html property must be defined",
1751
1602
  });
1752
1603
  }
1753
1604
 
@@ -1756,7 +1607,7 @@ export function lintSource(filePath, sourceText, options = {}) {
1756
1607
  classNode,
1757
1608
  supportedProps,
1758
1609
  contextKeys,
1759
- allCodeItems
1610
+ allCodeItems,
1760
1611
  );
1761
1612
  const augmentedProgram = createProgram(filePath, augmentedSource);
1762
1613
  const augmentedSourceFile = augmentedProgram.getSourceFile(filePath);
@@ -1765,7 +1616,7 @@ export function lintSource(filePath, sourceText, options = {}) {
1765
1616
  const augmentedChecker = augmentedProgram.getTypeChecker();
1766
1617
  const augmentedClassNode = collectWrecClasses(augmentedSourceFile)[0];
1767
1618
  if (!augmentedClassNode) {
1768
- throw new Error('unable to find Wrec subclass after augmentation');
1619
+ throw new Error("unable to find Wrec subclass after augmentation");
1769
1620
  }
1770
1621
 
1771
1622
  const helperCodeNodes = collectHelperCodeNodes(augmentedSourceFile);
@@ -1778,24 +1629,20 @@ export function lintSource(filePath, sourceText, options = {}) {
1778
1629
  propertyEntries,
1779
1630
  getterNames,
1780
1631
  allMethods,
1781
- findings
1632
+ findings,
1782
1633
  );
1783
1634
  collectUseStateMapErrors(classNode, supportedProps, findings);
1784
1635
 
1785
- allExpressions.forEach(expr => {
1786
- if (
1787
- expr.eventHandler &&
1788
- IDENTIFIER_RE.test(expr.text) &&
1789
- !allMethods.has(expr.text)
1790
- ) {
1636
+ allExpressions.forEach((expr) => {
1637
+ if (expr.eventHandler && IDENTIFIER_RE.test(expr.text) && !allMethods.has(expr.text)) {
1791
1638
  uniquePush(
1792
1639
  findings.invalidEventHandlers,
1793
1640
  expr.location
1794
1641
  ? {
1795
- location: expr.location,
1796
- message: `"${expr.text}" is not a defined instance method`
1797
- }
1798
- : `"${expr.text}" is not a defined instance method`
1642
+ location: expr.location,
1643
+ message: `"${expr.text}" is not a defined instance method`,
1644
+ }
1645
+ : `"${expr.text}" is not a defined instance method`,
1799
1646
  );
1800
1647
  }
1801
1648
  });
@@ -1808,20 +1655,17 @@ export function lintSource(filePath, sourceText, options = {}) {
1808
1655
  checkContextCalls: allCodeItems[index]?.checkContextCalls ?? true,
1809
1656
  eventHandler: allCodeItems[index]?.eventHandler ?? false,
1810
1657
  location: allCodeItems[index]?.location ?? null,
1811
- sourceFile: augmentedSourceFile
1658
+ sourceFile: augmentedSourceFile,
1812
1659
  });
1813
1660
  });
1814
1661
 
1815
1662
  findings.duplicateProperties.sort(compareLocatedFindings);
1816
1663
  findings.extraArguments.sort(
1817
- (a, b) =>
1818
- a.methodName.localeCompare(b.methodName) ||
1819
- a.argumentIndex - b.argumentIndex
1664
+ (a, b) => a.methodName.localeCompare(b.methodName) || a.argumentIndex - b.argumentIndex,
1820
1665
  );
1821
1666
  findings.incompatibleArguments.sort(
1822
1667
  (a, b) =>
1823
- a.methodName.localeCompare(b.methodName) ||
1824
- a.parameterName.localeCompare(b.parameterName)
1668
+ a.methodName.localeCompare(b.methodName) || a.parameterName.localeCompare(b.parameterName),
1825
1669
  );
1826
1670
  findings.incompatibleDeclareTypes.sort(compareLocatedFindings);
1827
1671
  findings.invalidCheckedBindings.sort(compareLocatedFindings);
@@ -1846,30 +1690,22 @@ export function lintSource(filePath, sourceText, options = {}) {
1846
1690
  findings.unsupportedEventNames.sort(compareLocatedFindings);
1847
1691
  findings.unsupportedHtmlAttributes.sort(compareLocatedFindings);
1848
1692
 
1849
- return formatReport(
1850
- filePath,
1851
- supportedProps,
1852
- allExpressions,
1853
- findings,
1854
- options
1855
- );
1693
+ return formatReport(filePath, supportedProps, allExpressions, findings, options);
1856
1694
  }
1857
1695
 
1858
1696
  // Runs the command-line interface for the linter.
1859
1697
  function main() {
1860
1698
  const args = process.argv.slice(2);
1861
- const unknownFlags = args.filter(
1862
- arg => arg.startsWith('--') && arg !== '--verbose'
1863
- );
1699
+ const unknownFlags = args.filter((arg) => arg.startsWith("--") && arg !== "--verbose");
1864
1700
  if (unknownFlags.length > 0) {
1865
1701
  fail(`unknown option: ${unknownFlags[0]}`);
1866
1702
  }
1867
1703
 
1868
- const verbose = args.includes('--verbose');
1869
- const positionalArgs = args.filter(arg => arg !== '--verbose');
1704
+ const verbose = args.includes("--verbose");
1705
+ const positionalArgs = args.filter((arg) => arg !== "--verbose");
1870
1706
 
1871
1707
  if (positionalArgs.length > 1) {
1872
- fail('usage: node scripts/wrec-lint.js [--verbose] {file-path}');
1708
+ fail("usage: node scripts/wrec-lint.js [--verbose] {file-path}");
1873
1709
  }
1874
1710
 
1875
1711
  const [filePath] = positionalArgs;
@@ -1878,24 +1714,23 @@ function main() {
1878
1714
  process.stdout.write(
1879
1715
  lintFile(validateFilePath(filePath), {
1880
1716
  showFileHeader: false,
1881
- verbose
1882
- })
1717
+ verbose,
1718
+ }),
1883
1719
  );
1884
1720
  return;
1885
1721
  }
1886
1722
 
1887
1723
  const rootDir = process.cwd();
1888
1724
  let previousHadIssues = false;
1889
- findWrecFiles(rootDir, matchedFile => {
1725
+ findWrecFiles(rootDir, (matchedFile) => {
1890
1726
  const report = lintFile(matchedFile, {
1891
- fileLabel:
1892
- path.relative(rootDir, matchedFile) || path.basename(matchedFile),
1727
+ fileLabel: path.relative(rootDir, matchedFile) || path.basename(matchedFile),
1893
1728
  showDetailsForCleanFile: false,
1894
1729
  showNoIssuesMessage: false,
1895
- verbose
1730
+ verbose,
1896
1731
  });
1897
- const currentHasIssues = report.trim().includes('\n');
1898
- if (previousHadIssues) process.stdout.write('\n');
1732
+ const currentHasIssues = report.trim().includes("\n");
1733
+ if (previousHadIssues) process.stdout.write("\n");
1899
1734
  process.stdout.write(report);
1900
1735
  previousHadIssues = currentHasIssues;
1901
1736
  });
@@ -1904,7 +1739,7 @@ function main() {
1904
1739
  // Determines whether a symbol should be treated as a required context function.
1905
1740
  function requiresContextFunction(symbol, sourceFile) {
1906
1741
  const declarations = symbol.declarations ?? [];
1907
- return declarations.some(declaration => {
1742
+ return declarations.some((declaration) => {
1908
1743
  if (isImportLikeDeclaration(declaration)) return true;
1909
1744
  return declaration.getSourceFile() === sourceFile;
1910
1745
  });
@@ -1912,66 +1747,58 @@ function requiresContextFunction(symbol, sourceFile) {
1912
1747
 
1913
1748
  // Resolves a relative import path to an existing source file.
1914
1749
  function resolveImportPath(baseDir, importPath) {
1915
- if (!importPath.startsWith('.')) return undefined;
1750
+ if (!importPath.startsWith(".")) return undefined;
1916
1751
 
1917
1752
  const candidates = [
1918
1753
  path.resolve(baseDir, importPath),
1919
1754
  path.resolve(baseDir, `${importPath}.js`),
1920
1755
  path.resolve(baseDir, `${importPath}.ts`),
1921
- path.resolve(baseDir, importPath, 'index.js'),
1922
- path.resolve(baseDir, importPath, 'index.ts')
1756
+ path.resolve(baseDir, importPath, "index.js"),
1757
+ path.resolve(baseDir, importPath, "index.ts"),
1923
1758
  ];
1924
1759
 
1925
- return candidates.find(candidate => fs.existsSync(candidate));
1760
+ return candidates.find((candidate) => fs.existsSync(candidate));
1926
1761
  }
1927
1762
 
1928
1763
  // Rewrites synthetic receiver references back to this for reporting.
1929
1764
  function toUserFacingExpression(text) {
1930
- return text.replaceAll(WREC_REF_NAME, 'this');
1765
+ return text.replaceAll(WREC_REF_NAME, "this");
1931
1766
  }
1932
1767
 
1933
1768
  // Returns whether a static property config type is compatible with a declare type.
1934
- function typeExpressionMatchesDeclaredType(
1935
- checker,
1936
- typeExpression,
1937
- declaredTypeNode
1938
- ) {
1769
+ function typeExpressionMatchesDeclaredType(checker, typeExpression, declaredTypeNode) {
1939
1770
  const typeKind = typeExpressionKind(typeExpression);
1940
1771
  const declaredType = checker.getTypeFromTypeNode(declaredTypeNode);
1941
1772
 
1942
- if (typeKind === 'Boolean') {
1773
+ if (typeKind === "Boolean") {
1943
1774
  return checker.isTypeAssignableTo(checker.getBooleanType(), declaredType);
1944
1775
  }
1945
1776
 
1946
- if (typeKind === 'Number') {
1777
+ if (typeKind === "Number") {
1947
1778
  return checker.isTypeAssignableTo(checker.getNumberType(), declaredType);
1948
1779
  }
1949
1780
 
1950
- if (typeKind === 'String') {
1781
+ if (typeKind === "String") {
1951
1782
  return checker.isTypeAssignableTo(checker.getStringType(), declaredType);
1952
1783
  }
1953
1784
 
1954
- if (typeKind === 'Object') {
1785
+ if (typeKind === "Object") {
1955
1786
  return isNonArrayObjectLikeType(checker, declaredType);
1956
1787
  }
1957
1788
 
1958
- if (typeKind === 'Array') {
1959
- const declaredTypes = declaredType.isUnion()
1960
- ? declaredType.types
1961
- : [declaredType];
1789
+ if (typeKind === "Array") {
1790
+ const declaredTypes = declaredType.isUnion() ? declaredType.types : [declaredType];
1962
1791
  return declaredTypes.every(
1963
- type =>
1792
+ (type) =>
1964
1793
  Boolean(type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) ||
1965
1794
  checker.isArrayType(type) ||
1966
- checker.isTupleType(type)
1795
+ checker.isTupleType(type),
1967
1796
  );
1968
1797
  }
1969
1798
 
1970
- if (typeKind === 'HTMLElement') {
1799
+ if (typeKind === "HTMLElement") {
1971
1800
  const elementType = getConstructedType(checker, typeExpression);
1972
- return elementType
1973
- ? checker.isTypeAssignableTo(declaredType, elementType)
1974
- : false;
1801
+ return elementType ? checker.isTypeAssignableTo(declaredType, elementType) : false;
1975
1802
  }
1976
1803
 
1977
1804
  const runtimeType = checker.getTypeAtLocation(typeExpression);
@@ -1989,25 +1816,22 @@ function typeExpressionKind(expression) {
1989
1816
  function typeNodeFromConstructorExpression(expression) {
1990
1817
  if (ts.isIdentifier(expression)) {
1991
1818
  switch (expression.text) {
1992
- case 'String':
1819
+ case "String":
1993
1820
  return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
1994
- case 'Number':
1821
+ case "Number":
1995
1822
  return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
1996
- case 'Boolean':
1823
+ case "Boolean":
1997
1824
  return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
1998
- case 'Object':
1825
+ case "Object":
1999
1826
  return ts.factory.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword);
2000
1827
  default:
2001
1828
  return ts.factory.createTypeReferenceNode(expression.text);
2002
1829
  }
2003
1830
  }
2004
1831
 
2005
- if (
2006
- ts.isCallExpression(expression) &&
2007
- ts.isIdentifier(expression.expression)
2008
- ) {
1832
+ if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression)) {
2009
1833
  const name = expression.expression.text;
2010
- if (name === 'Array' && expression.typeArguments?.length === 1) {
1834
+ if (name === "Array" && expression.typeArguments?.length === 1) {
2011
1835
  return ts.factory.createArrayTypeNode(expression.typeArguments[0]);
2012
1836
  }
2013
1837
  }
@@ -2022,9 +1846,7 @@ function typeNodeFromConstructorExpression(expression) {
2022
1846
  // Pushes a value into an array only if it is not already present.
2023
1847
  function uniquePush(array, value) {
2024
1848
  const valueText = getLocatedFindingMessage(value);
2025
- if (
2026
- !array.some(existing => getLocatedFindingMessage(existing) === valueText)
2027
- ) {
1849
+ if (!array.some((existing) => getLocatedFindingMessage(existing) === valueText)) {
2028
1850
  array.push(value);
2029
1851
  }
2030
1852
  }
@@ -2036,15 +1858,15 @@ function validateCheckedBinding(
2036
1858
  attrValue,
2037
1859
  findings,
2038
1860
  supportedProps,
2039
- resolveLocation
1861
+ resolveLocation,
2040
1862
  ) {
2041
- if (getHtmlTagName(node) !== 'input') return;
1863
+ if (getHtmlTagName(node) !== "input") return;
2042
1864
 
2043
- const [baseAttrName] = attrName.split(':');
2044
- if (baseAttrName !== 'checked') return;
1865
+ const [baseAttrName] = attrName.split(":");
1866
+ if (baseAttrName !== "checked") return;
2045
1867
 
2046
1868
  const inputType = getInputType(node);
2047
- if (inputType !== 'checkbox' && inputType !== 'radio') return;
1869
+ if (inputType !== "checkbox" && inputType !== "radio") return;
2048
1870
 
2049
1871
  const propName = getPropertyNameInAttribute(attrValue);
2050
1872
  if (!propName) return;
@@ -2052,19 +1874,17 @@ function validateCheckedBinding(
2052
1874
  const propInfo = supportedProps.get(propName);
2053
1875
  if (!propInfo) return;
2054
1876
 
2055
- const expectedType = inputType === 'checkbox' ? 'boolean' : 'string';
1877
+ const expectedType = inputType === "checkbox" ? "boolean" : "string";
2056
1878
  if (propInfo.typeText === expectedType) return;
2057
1879
 
2058
1880
  const expectedTypeName = getPropertyConfigTypeName(expectedType);
2059
1881
 
2060
- findings.invalidCheckedBindings.push(
2061
- {
2062
- location: getHtmlAttributeLocation(node, attrName, resolveLocation),
2063
- message:
2064
- `input type="${inputType}" attribute "${attrName}" refers to ` +
2065
- `property "${propName}" whose type is not ${expectedTypeName}`
2066
- }
2067
- );
1882
+ findings.invalidCheckedBindings.push({
1883
+ location: getHtmlAttributeLocation(node, attrName, resolveLocation),
1884
+ message:
1885
+ `input type="${inputType}" attribute "${attrName}" refers to ` +
1886
+ `property "${propName}" whose type is not ${expectedTypeName}`,
1887
+ });
2068
1888
  }
2069
1889
 
2070
1890
  // Validates value bindings on native form controls.
@@ -2074,10 +1894,10 @@ function validateValueBinding(
2074
1894
  attrValue,
2075
1895
  findings,
2076
1896
  supportedProps,
2077
- resolveLocation
1897
+ resolveLocation,
2078
1898
  ) {
2079
- const [baseAttrName] = attrName.split(':');
2080
- if (baseAttrName !== 'value') return;
1899
+ const [baseAttrName] = attrName.split(":");
1900
+ if (baseAttrName !== "value") return;
2081
1901
  if (!isNativeFormControl(node)) return;
2082
1902
 
2083
1903
  const propName = getPropertyNameInAttribute(attrValue);
@@ -2086,18 +1906,16 @@ function validateValueBinding(
2086
1906
  const propInfo = supportedProps.get(propName);
2087
1907
  if (!propInfo) return;
2088
1908
 
2089
- if (propInfo.typeText === 'number' || propInfo.typeText === 'string') {
1909
+ if (propInfo.typeText === "number" || propInfo.typeText === "string") {
2090
1910
  return;
2091
1911
  }
2092
1912
 
2093
- findings.invalidValueBindings.push(
2094
- {
2095
- location: getHtmlAttributeLocation(node, attrName, resolveLocation),
2096
- message:
2097
- `${getHtmlTagName(node)} attribute "${attrName}" refers to property ` +
2098
- `"${propName}" whose type is not String or Number`
2099
- }
2100
- );
1913
+ findings.invalidValueBindings.push({
1914
+ location: getHtmlAttributeLocation(node, attrName, resolveLocation),
1915
+ message:
1916
+ `${getHtmlTagName(node)} attribute "${attrName}" refers to property ` +
1917
+ `"${propName}" whose type is not String or Number`,
1918
+ });
2101
1919
  }
2102
1920
 
2103
1921
  // Validates computed property references and method calls.
@@ -2107,46 +1925,33 @@ function validateComputedProperty(
2107
1925
  supportedProps,
2108
1926
  classMethods,
2109
1927
  findings,
2110
- location
1928
+ location,
2111
1929
  ) {
2112
1930
  for (const match of computedText.matchAll(THIS_REF_RE)) {
2113
1931
  const referencedName = match[1];
2114
- if (
2115
- !supportedProps.has(referencedName) &&
2116
- !classMethods.has(referencedName)
2117
- ) {
2118
- findings.invalidComputedProperties.push(
2119
- {
2120
- location,
2121
- message:
2122
- `property "${propName}" computed references ` +
2123
- `missing property "${referencedName}"`
2124
- }
2125
- );
1932
+ if (!supportedProps.has(referencedName) && !classMethods.has(referencedName)) {
1933
+ findings.invalidComputedProperties.push({
1934
+ location,
1935
+ message:
1936
+ `property "${propName}" computed references ` + `missing property "${referencedName}"`,
1937
+ });
2126
1938
  }
2127
1939
  }
2128
1940
 
2129
1941
  for (const match of computedText.matchAll(THIS_CALL_RE)) {
2130
1942
  const methodName = match[1];
2131
1943
  if (!classMethods.has(methodName)) {
2132
- findings.invalidComputedProperties.push(
2133
- {
2134
- location,
2135
- message:
2136
- `property "${propName}" computed calls ` +
2137
- `non-method instance member "${methodName}"`
2138
- }
2139
- );
1944
+ findings.invalidComputedProperties.push({
1945
+ location,
1946
+ message:
1947
+ `property "${propName}" computed calls ` + `non-method instance member "${methodName}"`,
1948
+ });
2140
1949
  }
2141
1950
  }
2142
1951
  }
2143
1952
 
2144
1953
  // Validates that computed properties do not form dependency cycles.
2145
- function validateComputedPropertyCycles(
2146
- computedDependencies,
2147
- computedLocations,
2148
- findings
2149
- ) {
1954
+ function validateComputedPropertyCycles(computedDependencies, computedLocations, findings) {
2150
1955
  const computedNames = [...computedDependencies.keys()].sort();
2151
1956
  const dependencyCountMap = new Map();
2152
1957
  const dependentsMap = new Map();
@@ -2183,20 +1988,16 @@ function validateComputedPropertyCycles(
2183
1988
  if (orderedNames.length === computedNames.length) return;
2184
1989
 
2185
1990
  const cycleNames = computedNames.filter(
2186
- computedName => dependencyCountMap.get(computedName) > 0
1991
+ (computedName) => dependencyCountMap.get(computedName) > 0,
2187
1992
  );
2188
1993
  const cycleLocation = cycleNames
2189
- .map(name => computedLocations.get(name))
1994
+ .map((name) => computedLocations.get(name))
2190
1995
  .filter(Boolean)
2191
- .sort(
2192
- (a, b) => a.line - b.line || a.character - b.character
2193
- )[0];
2194
- findings.invalidComputedProperties.push(
2195
- {
2196
- location: cycleLocation ?? null,
2197
- message: `computed properties form a cycle: ${cycleNames.join(', ')}`
2198
- }
2199
- );
1996
+ .sort((a, b) => a.line - b.line || a.character - b.character)[0];
1997
+ findings.invalidComputedProperties.push({
1998
+ location: cycleLocation ?? null,
1999
+ message: `computed properties form a cycle: ${cycleNames.join(", ")}`,
2000
+ });
2200
2001
  }
2201
2002
 
2202
2003
  // Validates that a default value matches the declared property type.
@@ -2207,50 +2008,44 @@ function validateDefaultValue(checker, typeExpression, valueExpression) {
2207
2008
  const valueType = checker.getTypeAtLocation(valueExpression);
2208
2009
  const valueTypeName = checker.typeToString(valueType);
2209
2010
 
2210
- if (typeKind === 'String') {
2211
- if (
2212
- !(valueType.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral))
2213
- ) {
2214
- return {typeName: 'String', valueTypeName};
2011
+ if (typeKind === "String") {
2012
+ if (!(valueType.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral))) {
2013
+ return { typeName: "String", valueTypeName };
2215
2014
  }
2216
2015
  return undefined;
2217
2016
  }
2218
2017
 
2219
- if (typeKind === 'Number') {
2220
- if (
2221
- !(valueType.flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral))
2222
- ) {
2223
- return {typeName: 'Number', valueTypeName};
2018
+ if (typeKind === "Number") {
2019
+ if (!(valueType.flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral))) {
2020
+ return { typeName: "Number", valueTypeName };
2224
2021
  }
2225
2022
  return undefined;
2226
2023
  }
2227
2024
 
2228
- if (typeKind === 'Boolean') {
2229
- if (
2230
- !(valueType.flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral))
2231
- ) {
2232
- return {typeName: 'Boolean', valueTypeName};
2025
+ if (typeKind === "Boolean") {
2026
+ if (!(valueType.flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral))) {
2027
+ return { typeName: "Boolean", valueTypeName };
2233
2028
  }
2234
2029
  return undefined;
2235
2030
  }
2236
2031
 
2237
- if (typeKind === 'Array') {
2032
+ if (typeKind === "Array") {
2238
2033
  if (!checker.isArrayType(valueType) && !checker.isTupleType(valueType)) {
2239
- return {typeName: 'Array', valueTypeName};
2034
+ return { typeName: "Array", valueTypeName };
2240
2035
  }
2241
2036
  return undefined;
2242
2037
  }
2243
2038
 
2244
- if (typeKind === 'Object') {
2039
+ if (typeKind === "Object") {
2245
2040
  if (!isNonArrayObjectLikeType(checker, valueType)) {
2246
- return {typeName: 'Object', valueTypeName};
2041
+ return { typeName: "Object", valueTypeName };
2247
2042
  }
2248
2043
  }
2249
2044
 
2250
- if (typeKind === 'HTMLElement') {
2045
+ if (typeKind === "HTMLElement") {
2251
2046
  const elementType = getConstructedType(checker, typeExpression);
2252
2047
  if (!elementType || !checker.isTypeAssignableTo(valueType, elementType)) {
2253
- return {typeName: 'HTMLElement', valueTypeName};
2048
+ return { typeName: "HTMLElement", valueTypeName };
2254
2049
  }
2255
2050
  }
2256
2051
 
@@ -2261,8 +2056,8 @@ function validateDefaultValue(checker, typeExpression, valueExpression) {
2261
2056
  function validateFilePath(filePath) {
2262
2057
  const resolved = path.resolve(filePath);
2263
2058
  const ext = path.extname(resolved);
2264
- if (ext !== '.js' && ext !== '.ts') {
2265
- throw new Error('argument must be a path to a .js or .ts file');
2059
+ if (ext !== ".js" && ext !== ".ts") {
2060
+ throw new Error("argument must be a path to a .js or .ts file");
2266
2061
  }
2267
2062
  if (!fs.existsSync(resolved)) {
2268
2063
  throw new Error(`file not found: ${resolved}`);
@@ -2271,30 +2066,20 @@ function validateFilePath(filePath) {
2271
2066
  }
2272
2067
 
2273
2068
  // Validates the syntax of a form-assoc attribute value.
2274
- function validateFormAssocAttribute(
2275
- node,
2276
- attrName,
2277
- attrValue,
2278
- findings,
2279
- resolveLocation
2280
- ) {
2281
- if (attrName !== 'form-assoc') return;
2069
+ function validateFormAssocAttribute(node, attrName, attrValue, findings, resolveLocation) {
2070
+ if (attrName !== "form-assoc") return;
2282
2071
 
2283
- const pairs = attrValue.split(',');
2072
+ const pairs = attrValue.split(",");
2284
2073
  for (const pair of pairs) {
2285
2074
  const trimmed = pair.trim();
2286
- const [propName, fieldName, ...rest] = trimmed
2287
- .split(':')
2288
- .map(part => part.trim());
2075
+ const [propName, fieldName, ...rest] = trimmed.split(":").map((part) => part.trim());
2289
2076
  if (!trimmed || rest.length > 0 || !propName || !fieldName) {
2290
- findings.invalidFormAssocValues.push(
2291
- {
2292
- location: getHtmlAttributeLocation(node, attrName, resolveLocation),
2293
- message:
2294
- `form-assoc="${attrValue}" is invalid; expected ` +
2295
- '"property:field" or a comma-separated list of them'
2296
- }
2297
- );
2077
+ findings.invalidFormAssocValues.push({
2078
+ location: getHtmlAttributeLocation(node, attrName, resolveLocation),
2079
+ message:
2080
+ `form-assoc="${attrValue}" is invalid; expected ` +
2081
+ '"property:field" or a comma-separated list of them',
2082
+ });
2298
2083
  return;
2299
2084
  }
2300
2085
  }
@@ -2307,79 +2092,66 @@ function validateFormAssocPropertyMappings(
2307
2092
  attrValue,
2308
2093
  findings,
2309
2094
  componentPropertyMaps,
2310
- resolveLocation
2095
+ resolveLocation,
2311
2096
  ) {
2312
- if (attrName !== 'form-assoc') return;
2313
- const tagName = (node.rawTagName || node.tagName || '').toLowerCase();
2097
+ if (attrName !== "form-assoc") return;
2098
+ const tagName = (node.rawTagName || node.tagName || "").toLowerCase();
2314
2099
  const supportedProps = componentPropertyMaps.get(tagName);
2315
2100
  if (!supportedProps) return;
2316
2101
 
2317
- const pairs = attrValue.split(',');
2102
+ const pairs = attrValue.split(",");
2318
2103
  for (const pair of pairs) {
2319
- const [propName] = pair.split(':').map(part => part.trim());
2104
+ const [propName] = pair.split(":").map((part) => part.trim());
2320
2105
  if (!propName) continue;
2321
2106
  if (!supportedProps.has(propName)) {
2322
- findings.invalidFormAssocValues.push(
2323
- {
2324
- location: getHtmlAttributeLocation(node, attrName, resolveLocation),
2325
- message:
2326
- `form-assoc="${attrValue}" refers to ` +
2327
- `missing component property "${propName}"`
2328
- }
2329
- );
2107
+ findings.invalidFormAssocValues.push({
2108
+ location: getHtmlAttributeLocation(node, attrName, resolveLocation),
2109
+ message:
2110
+ `form-assoc="${attrValue}" refers to ` + `missing component property "${propName}"`,
2111
+ });
2330
2112
  }
2331
2113
  }
2332
2114
  }
2333
2115
 
2334
2116
  // Validates that an HTML attribute is supported for the current element.
2335
2117
  function validateHtmlAttribute(node, attrName, findings, resolveLocation) {
2336
- if (attrName.startsWith('aria-') || attrName.startsWith('data-')) return;
2337
- if (attrName.startsWith('on')) return;
2338
- if (attrName === 'form-assoc') return;
2339
- if (attrName === 'ref') return;
2118
+ if (attrName.startsWith("aria-") || attrName.startsWith("data-")) return;
2119
+ if (attrName.startsWith("on")) return;
2120
+ if (attrName === "form-assoc") return;
2121
+ if (attrName === "ref") return;
2340
2122
 
2341
- const [baseAttrName] = attrName.split(':');
2123
+ const [baseAttrName] = attrName.split(":");
2342
2124
  if (HTML_GLOBAL_ATTRIBUTES.has(baseAttrName)) return;
2343
2125
 
2344
- const tagName = (node.rawTagName || node.tagName || '').toLowerCase();
2345
- if (!tagName || tagName.includes('-')) return;
2126
+ const tagName = (node.rawTagName || node.tagName || "").toLowerCase();
2127
+ if (!tagName || tagName.includes("-")) return;
2346
2128
 
2347
2129
  const supported = getSupportedHtmlAttributes(tagName);
2348
2130
  if (!supported) return;
2349
2131
  if (supported.has(baseAttrName)) return;
2350
2132
 
2351
- findings.unsupportedHtmlAttributes.push(
2352
- {
2353
- location: getHtmlAttributeLocation(node, attrName, resolveLocation),
2354
- message: `${tagName} attribute "${attrName}" is not supported`
2355
- }
2356
- );
2133
+ findings.unsupportedHtmlAttributes.push({
2134
+ location: getHtmlAttributeLocation(node, attrName, resolveLocation),
2135
+ message: `${tagName} attribute "${attrName}" is not supported`,
2136
+ });
2357
2137
  }
2358
2138
 
2359
2139
  // Validates required parent-child relationships for supported HTML tags.
2360
2140
  function validateHtmlNesting(node, findings, resolveLocation) {
2361
2141
  const tagName = getHtmlTagName(node);
2362
- if (!tagName || tagName.includes('-')) return;
2142
+ if (!tagName || tagName.includes("-")) return;
2363
2143
 
2364
2144
  const parentNode = node.parentNode?.nodeType === 1 ? node.parentNode : null;
2365
- const parentTagName = parentNode ? getHtmlTagName(parentNode) : '';
2145
+ const parentTagName = parentNode ? getHtmlTagName(parentNode) : "";
2366
2146
  const allowedParents = HTML_ALLOWED_PARENTS.get(tagName);
2367
- if (
2368
- allowedParents &&
2369
- (!parentTagName || !allowedParents.has(parentTagName))
2370
- ) {
2371
- const parentDescription = parentTagName
2372
- ? `<${parentTagName}>`
2373
- : 'the document root';
2374
- findings.invalidHtmlNesting.push(
2375
- {
2376
- location: getHtmlNodeLocation(node, resolveLocation),
2377
- message:
2378
- `<${tagName}> must be nested inside ${[...allowedParents]
2379
- .map(name => `<${name}>`)
2380
- .join(' or ')}, but parent is ${parentDescription}`
2381
- }
2382
- );
2147
+ if (allowedParents && (!parentTagName || !allowedParents.has(parentTagName))) {
2148
+ const parentDescription = parentTagName ? `<${parentTagName}>` : "the document root";
2149
+ findings.invalidHtmlNesting.push({
2150
+ location: getHtmlNodeLocation(node, resolveLocation),
2151
+ message: `<${tagName}> must be nested inside ${[...allowedParents]
2152
+ .map((name) => `<${name}>`)
2153
+ .join(" or ")}, but parent is ${parentDescription}`,
2154
+ });
2383
2155
  }
2384
2156
 
2385
2157
  const allowedChildren = HTML_ALLOWED_CHILDREN.get(tagName);
@@ -2388,15 +2160,13 @@ function validateHtmlNesting(node, findings, resolveLocation) {
2388
2160
  for (const child of node.childNodes ?? []) {
2389
2161
  if (child.nodeType !== 1) continue;
2390
2162
  const childTagName = getHtmlTagName(child);
2391
- if (!childTagName || childTagName.includes('-')) continue;
2163
+ if (!childTagName || childTagName.includes("-")) continue;
2392
2164
  if (allowedChildren.has(childTagName)) continue;
2393
2165
 
2394
- findings.invalidHtmlNesting.push(
2395
- {
2396
- location: getHtmlNodeLocation(child, resolveLocation),
2397
- message: `<${childTagName}> is not allowed directly inside <${tagName}>`
2398
- }
2399
- );
2166
+ findings.invalidHtmlNesting.push({
2167
+ location: getHtmlNodeLocation(child, resolveLocation),
2168
+ message: `<${childTagName}> is not allowed directly inside <${tagName}>`,
2169
+ });
2400
2170
  }
2401
2171
  }
2402
2172
 
@@ -2409,14 +2179,14 @@ function validatePropertyConfigs(
2409
2179
  propertyEntries,
2410
2180
  getterNames,
2411
2181
  classMethods,
2412
- findings
2182
+ findings,
2413
2183
  ) {
2414
2184
  const computedDependencies = new Map();
2415
2185
  const computedLocations = new Map();
2416
2186
  const computedPropNames = new Set();
2417
2187
 
2418
- for (const {config, propName} of propertyEntries) {
2419
- const computedProp = getObjectProperty(config, 'computed');
2188
+ for (const { config, propName } of propertyEntries) {
2189
+ const computedProp = getObjectProperty(config, "computed");
2420
2190
  if (
2421
2191
  computedProp &&
2422
2192
  ts.isPropertyAssignment(computedProp) &&
@@ -2425,81 +2195,59 @@ function validatePropertyConfigs(
2425
2195
  ) {
2426
2196
  computedLocations.set(
2427
2197
  propName,
2428
- sourceFile.getLineAndCharacterOfPosition(
2429
- computedProp.initializer.getStart(sourceFile)
2430
- )
2198
+ sourceFile.getLineAndCharacterOfPosition(computedProp.initializer.getStart(sourceFile)),
2431
2199
  );
2432
2200
  computedPropNames.add(propName);
2433
2201
  }
2434
2202
  }
2435
2203
 
2436
- for (const {config, propName, property} of propertyEntries) {
2437
- const computedProp = getObjectProperty(config, 'computed');
2204
+ for (const { config, propName, property } of propertyEntries) {
2205
+ const computedProp = getObjectProperty(config, "computed");
2438
2206
  const declaredTypeNode = declaredPropertyTypes.get(propName);
2439
- const typeProp = getObjectProperty(config, 'type');
2440
- const usedByProp = getObjectProperty(config, 'usedBy');
2441
- const valueProp = getObjectProperty(config, 'value');
2442
- const valuesProp = getObjectProperty(config, 'values');
2207
+ const typeProp = getObjectProperty(config, "type");
2208
+ const usedByProp = getObjectProperty(config, "usedBy");
2209
+ const valueProp = getObjectProperty(config, "value");
2210
+ const valuesProp = getObjectProperty(config, "values");
2443
2211
  const valuesConfigError = getValuesConfigurationError(valuesProp);
2444
2212
  const propertyLocation = sourceFile.getLineAndCharacterOfPosition(
2445
- property.name.getStart(sourceFile)
2213
+ property.name.getStart(sourceFile),
2446
2214
  );
2447
2215
 
2448
2216
  const typeExpression =
2449
- typeProp && ts.isPropertyAssignment(typeProp)
2450
- ? typeProp.initializer
2451
- : undefined;
2217
+ typeProp && ts.isPropertyAssignment(typeProp) ? typeProp.initializer : undefined;
2452
2218
 
2453
2219
  if (!typeExpression) {
2454
- findings.missingTypeProperties.push(
2455
- {
2456
- location: propertyLocation,
2457
- message: `property "${propName}" does not specify a type`
2458
- }
2459
- );
2460
- } else if (
2461
- SUPPORTED_PROPERTY_TYPE_NAMES.has(
2462
- getPropertyTypeGenericBaseName(sourceFile, typeExpression)
2463
- )
2464
- ) {
2465
- findings.invalidTypeProperties.push(
2466
- {
2467
- location: propertyLocation,
2468
- message:
2469
- `property "${propName}" type cannot use generic syntax like ` +
2470
- `"${typeExpression.getText(sourceFile).trim()}"; use ` +
2471
- `"${getPropertyTypeGenericBaseName(sourceFile, typeExpression)}" instead`
2472
- }
2473
- );
2220
+ findings.missingTypeProperties.push({
2221
+ location: propertyLocation,
2222
+ message: `property "${propName}" does not specify a type`,
2223
+ });
2474
2224
  } else if (
2475
- !SUPPORTED_PROPERTY_TYPE_NAMES.has(typeExpressionKind(typeExpression))
2225
+ SUPPORTED_PROPERTY_TYPE_NAMES.has(getPropertyTypeGenericBaseName(sourceFile, typeExpression))
2476
2226
  ) {
2477
- findings.invalidTypeProperties.push(
2478
- {
2227
+ findings.invalidTypeProperties.push({
2228
+ location: propertyLocation,
2229
+ message:
2230
+ `property "${propName}" type cannot use generic syntax like ` +
2231
+ `"${typeExpression.getText(sourceFile).trim()}"; use ` +
2232
+ `"${getPropertyTypeGenericBaseName(sourceFile, typeExpression)}" instead`,
2233
+ });
2234
+ } else if (!SUPPORTED_PROPERTY_TYPE_NAMES.has(typeExpressionKind(typeExpression))) {
2235
+ findings.invalidTypeProperties.push({
2236
+ location: propertyLocation,
2237
+ message:
2238
+ `property "${propName}" type must be one of ` +
2239
+ "Boolean, Number, String, Object, Array, or HTMLElement",
2240
+ });
2241
+ } else if (declaredTypeNode) {
2242
+ if (!typeExpressionMatchesDeclaredType(checker, typeExpression, declaredTypeNode)) {
2243
+ findings.incompatibleDeclareTypes.push({
2479
2244
  location: propertyLocation,
2480
2245
  message:
2481
- `property "${propName}" type must be one of ` +
2482
- 'Boolean, Number, String, Object, Array, or HTMLElement'
2483
- }
2484
- );
2485
- } else if (declaredTypeNode) {
2486
- if (
2487
- !typeExpressionMatchesDeclaredType(
2488
- checker,
2489
- typeExpression,
2490
- declaredTypeNode
2491
- )
2492
- ) {
2493
- findings.incompatibleDeclareTypes.push(
2494
- {
2495
- location: propertyLocation,
2496
- message:
2497
- `property "${propName}" declare type ` +
2498
- `"${getPropertyTypeTextFromNode(sourceFile, declaredTypeNode)}" ` +
2499
- `is not compatible with static properties type ` +
2500
- `"${getPropertyConfigTypeName(typeExpressionKind(typeExpression))}"`
2501
- }
2502
- );
2246
+ `property "${propName}" declare type ` +
2247
+ `"${getPropertyTypeTextFromNode(sourceFile, declaredTypeNode)}" ` +
2248
+ `is not compatible with static properties type ` +
2249
+ `"${getPropertyConfigTypeName(typeExpressionKind(typeExpression))}"`,
2250
+ });
2503
2251
  }
2504
2252
  }
2505
2253
 
@@ -2511,26 +2259,20 @@ function validatePropertyConfigs(
2511
2259
  if (isGetterReference(methodName)) {
2512
2260
  const getterName = getGetterName(methodName);
2513
2261
  if (getterNames.has(getterName)) continue;
2514
- findings.invalidUsedByReferences.push(
2515
- {
2516
- location: propertyLocation,
2517
- message:
2518
- `property "${propName}" usedBy references ` +
2519
- `missing getter "${getterName}"`
2520
- }
2521
- );
2262
+ findings.invalidUsedByReferences.push({
2263
+ location: propertyLocation,
2264
+ message:
2265
+ `property "${propName}" usedBy references ` + `missing getter "${getterName}"`,
2266
+ });
2522
2267
  continue;
2523
2268
  }
2524
2269
 
2525
2270
  if (!classMethods.has(methodName)) {
2526
- findings.invalidUsedByReferences.push(
2527
- {
2528
- location: propertyLocation,
2529
- message:
2530
- `property "${propName}" usedBy references ` +
2531
- `missing method "${methodName}"`
2532
- }
2533
- );
2271
+ findings.invalidUsedByReferences.push({
2272
+ location: propertyLocation,
2273
+ message:
2274
+ `property "${propName}" usedBy references ` + `missing method "${methodName}"`,
2275
+ });
2534
2276
  }
2535
2277
  }
2536
2278
  }
@@ -2544,10 +2286,7 @@ function validatePropertyConfigs(
2544
2286
  ) {
2545
2287
  computedDependencies.set(
2546
2288
  propName,
2547
- collectComputedDependencies(
2548
- computedProp.initializer.text,
2549
- computedPropNames
2550
- )
2289
+ collectComputedDependencies(computedProp.initializer.text, computedPropNames),
2551
2290
  );
2552
2291
  validateComputedProperty(
2553
2292
  propName,
@@ -2555,28 +2294,24 @@ function validatePropertyConfigs(
2555
2294
  supportedProps,
2556
2295
  classMethods,
2557
2296
  findings,
2558
- computedLocations.get(propName) ?? propertyLocation
2297
+ computedLocations.get(propName) ?? propertyLocation,
2559
2298
  );
2560
2299
  }
2561
2300
 
2562
2301
  if (valuesConfigError) {
2563
- findings.invalidValuesConfigurations.push(
2564
- {
2565
- location: propertyLocation,
2566
- message: `property "${propName}" ${valuesConfigError}`
2567
- }
2568
- );
2302
+ findings.invalidValuesConfigurations.push({
2303
+ location: propertyLocation,
2304
+ message: `property "${propName}" ${valuesConfigError}`,
2305
+ });
2569
2306
  }
2570
2307
 
2571
2308
  const values = getStringArrayLiteral(valuesProp);
2572
2309
  if (values) {
2573
- if (typeExpressionKind(typeExpression) !== 'String') {
2574
- findings.invalidValuesConfigurations.push(
2575
- {
2576
- location: propertyLocation,
2577
- message: `property "${propName}" uses values, but its type is not String`
2578
- }
2579
- );
2310
+ if (typeExpressionKind(typeExpression) !== "String") {
2311
+ findings.invalidValuesConfigurations.push({
2312
+ location: propertyLocation,
2313
+ message: `property "${propName}" uses values, but its type is not String`,
2314
+ });
2580
2315
  }
2581
2316
 
2582
2317
  if (
@@ -2586,52 +2321,34 @@ function validatePropertyConfigs(
2586
2321
  ts.isNoSubstitutionTemplateLiteral(valueProp.initializer)) &&
2587
2322
  !values.includes(valueProp.initializer.text)
2588
2323
  ) {
2589
- findings.invalidDefaultValues.push(
2590
- {
2591
- location: propertyLocation,
2592
- message:
2593
- `property "${propName}" default value ` +
2594
- `"${valueProp.initializer.text}" is not in values`
2595
- }
2596
- );
2324
+ findings.invalidDefaultValues.push({
2325
+ location: propertyLocation,
2326
+ message:
2327
+ `property "${propName}" default value ` +
2328
+ `"${valueProp.initializer.text}" is not in values`,
2329
+ });
2597
2330
  }
2598
2331
  }
2599
2332
 
2600
2333
  if (valueProp && ts.isPropertyAssignment(valueProp)) {
2601
- const mismatch = validateDefaultValue(
2602
- checker,
2603
- typeExpression,
2604
- valueProp.initializer
2605
- );
2334
+ const mismatch = validateDefaultValue(checker, typeExpression, valueProp.initializer);
2606
2335
  if (mismatch) {
2607
- findings.invalidDefaultValues.push(
2608
- {
2609
- location: propertyLocation,
2610
- message:
2611
- `property "${propName}" default value ` +
2612
- `has type ${mismatch.valueTypeName}, ` +
2613
- `but declared type is ${mismatch.typeName}`
2614
- }
2615
- );
2336
+ findings.invalidDefaultValues.push({
2337
+ location: propertyLocation,
2338
+ message:
2339
+ `property "${propName}" default value ` +
2340
+ `has type ${mismatch.valueTypeName}, ` +
2341
+ `but declared type is ${mismatch.typeName}`,
2342
+ });
2616
2343
  }
2617
2344
  }
2618
2345
  }
2619
2346
 
2620
- validateComputedPropertyCycles(
2621
- computedDependencies,
2622
- computedLocations,
2623
- findings
2624
- );
2347
+ validateComputedPropertyCycles(computedDependencies, computedLocations, findings);
2625
2348
  }
2626
2349
 
2627
2350
  // Validates that a ref attribute targets a unique HTMLElement property.
2628
- function validateRefAttribute(
2629
- attrValue,
2630
- supportedProps,
2631
- findings,
2632
- seenRefProps,
2633
- location
2634
- ) {
2351
+ function validateRefAttribute(attrValue, supportedProps, findings, seenRefProps, location) {
2635
2352
  if (!attrValue) return;
2636
2353
 
2637
2354
  const propName = attrValue.trim();
@@ -2639,36 +2356,27 @@ function validateRefAttribute(
2639
2356
 
2640
2357
  const propInfo = supportedProps.get(propName);
2641
2358
  if (!propInfo) {
2642
- findings.invalidRefAttributes.push(
2643
- {
2644
- location,
2645
- message: `ref="${attrValue}" refers to missing property "${propName}"`
2646
- }
2647
- );
2359
+ findings.invalidRefAttributes.push({
2360
+ location,
2361
+ message: `ref="${attrValue}" refers to missing property "${propName}"`,
2362
+ });
2648
2363
  return;
2649
2364
  }
2650
2365
 
2651
- if (propInfo.typeText !== 'HTMLElement') {
2652
- findings.invalidRefAttributes.push(
2653
- {
2654
- location,
2655
- message:
2656
- `ref="${attrValue}" refers to property "${propName}" ` +
2657
- 'whose type is not HTMLElement'
2658
- }
2659
- );
2366
+ if (propInfo.typeText !== "HTMLElement") {
2367
+ findings.invalidRefAttributes.push({
2368
+ location,
2369
+ message:
2370
+ `ref="${attrValue}" refers to property "${propName}" ` + "whose type is not HTMLElement",
2371
+ });
2660
2372
  return;
2661
2373
  }
2662
2374
 
2663
2375
  if (seenRefProps.has(propName)) {
2664
- findings.invalidRefAttributes.push(
2665
- {
2666
- location,
2667
- message:
2668
- `ref="${attrValue}" is a duplicate reference ` +
2669
- `to the property "${propName}"`
2670
- }
2671
- );
2376
+ findings.invalidRefAttributes.push({
2377
+ location,
2378
+ message: `ref="${attrValue}" is a duplicate reference ` + `to the property "${propName}"`,
2379
+ });
2672
2380
  return;
2673
2381
  }
2674
2382
 
@@ -2677,19 +2385,16 @@ function validateRefAttribute(
2677
2385
 
2678
2386
  // Validates event names used in value-binding attributes.
2679
2387
  function validateValueBindingEvent(node, attrName, findings, resolveLocation) {
2680
- const [realAttrName, eventName] = attrName.split(':');
2681
- if (realAttrName !== 'value' || !eventName) return;
2388
+ const [realAttrName, eventName] = attrName.split(":");
2389
+ if (realAttrName !== "value" || !eventName) return;
2682
2390
  if (SUPPORTED_EVENT_NAMES.has(eventName)) return;
2683
2391
 
2684
- const tagName = node.rawTagName || node.tagName || 'element';
2685
- findings.unsupportedEventNames.push(
2686
- {
2687
- location: getHtmlAttributeLocation(node, attrName, resolveLocation),
2688
- message:
2689
- `${tagName} attribute "${attrName}" refers to ` +
2690
- `an unsupported event name "${eventName}"`
2691
- }
2692
- );
2392
+ const tagName = node.rawTagName || node.tagName || "element";
2393
+ findings.unsupportedEventNames.push({
2394
+ location: getHtmlAttributeLocation(node, attrName, resolveLocation),
2395
+ message:
2396
+ `${tagName} attribute "${attrName}" refers to ` + `an unsupported event name "${eventName}"`,
2397
+ });
2693
2398
  }
2694
2399
 
2695
2400
  // Walks parsed HTML nodes to extract expressions and apply HTML validations.
@@ -2700,82 +2405,59 @@ function walkHtmlNode(
2700
2405
  componentPropertyMaps,
2701
2406
  supportedProps,
2702
2407
  seenRefProps,
2703
- resolveLocation
2408
+ resolveLocation,
2704
2409
  ) {
2705
2410
  if (node.nodeType === 1) {
2706
2411
  validateHtmlNesting(node, findings, resolveLocation);
2707
2412
 
2708
2413
  for (const [attrName, attrValue] of Object.entries(node.attributes)) {
2709
2414
  if (!attrValue) continue;
2710
- validateFormAssocAttribute(
2711
- node,
2712
- attrName,
2713
- attrValue,
2714
- findings,
2715
- resolveLocation
2716
- );
2415
+ validateFormAssocAttribute(node, attrName, attrValue, findings, resolveLocation);
2717
2416
  validateFormAssocPropertyMappings(
2718
2417
  node,
2719
2418
  attrName,
2720
2419
  attrValue,
2721
2420
  findings,
2722
2421
  componentPropertyMaps,
2723
- resolveLocation
2724
- );
2725
- validateCheckedBinding(
2726
- node,
2727
- attrName,
2728
- attrValue,
2729
- findings,
2730
- supportedProps,
2731
- resolveLocation
2422
+ resolveLocation,
2732
2423
  );
2424
+ validateCheckedBinding(node, attrName, attrValue, findings, supportedProps, resolveLocation);
2733
2425
  validateHtmlAttribute(node, attrName, findings, resolveLocation);
2734
- validateValueBinding(
2735
- node,
2736
- attrName,
2737
- attrValue,
2738
- findings,
2739
- supportedProps,
2740
- resolveLocation
2741
- );
2426
+ validateValueBinding(node, attrName, attrValue, findings, supportedProps, resolveLocation);
2742
2427
  validateValueBindingEvent(node, attrName, findings, resolveLocation);
2743
- if (attrName === 'ref') {
2428
+ if (attrName === "ref") {
2744
2429
  validateRefAttribute(
2745
2430
  attrValue,
2746
2431
  supportedProps,
2747
2432
  findings,
2748
2433
  seenRefProps,
2749
- getHtmlAttributeLocation(node, attrName, resolveLocation)
2434
+ getHtmlAttributeLocation(node, attrName, resolveLocation),
2750
2435
  );
2751
2436
  }
2752
2437
  if (
2753
2438
  REFS_TEST_RE.test(attrValue) ||
2754
- (attrName.startsWith('on') && IDENTIFIER_RE.test(attrValue))
2439
+ (attrName.startsWith("on") && IDENTIFIER_RE.test(attrValue))
2755
2440
  ) {
2756
2441
  expressions.push({
2757
- context: 'instance',
2758
- eventHandler: attrName.startsWith('on'),
2759
- kind: 'html',
2442
+ context: "instance",
2443
+ eventHandler: attrName.startsWith("on"),
2444
+ kind: "html",
2760
2445
  text: attrValue.trim(),
2761
- location: getHtmlAttributeLocation(node, attrName, resolveLocation)
2446
+ location: getHtmlAttributeLocation(node, attrName, resolveLocation),
2762
2447
  });
2763
2448
  }
2764
2449
  }
2765
2450
  }
2766
2451
 
2767
- if (
2768
- (node.nodeType === 3 || node.nodeType === 8) &&
2769
- typeof node.rawText === 'string'
2770
- ) {
2452
+ if ((node.nodeType === 3 || node.nodeType === 8) && typeof node.rawText === "string") {
2771
2453
  const text = node.rawText.trim();
2772
2454
  if (text && REFS_TEST_RE.test(text)) {
2773
2455
  expressions.push({
2774
- context: 'instance',
2456
+ context: "instance",
2775
2457
  eventHandler: false,
2776
- kind: 'html',
2458
+ kind: "html",
2777
2459
  text,
2778
- location: getHtmlNodeLocation(node, resolveLocation)
2460
+ location: getHtmlNodeLocation(node, resolveLocation),
2779
2461
  });
2780
2462
  }
2781
2463
  }
@@ -2788,7 +2470,7 @@ function walkHtmlNode(
2788
2470
  componentPropertyMaps,
2789
2471
  supportedProps,
2790
2472
  seenRefProps,
2791
- resolveLocation
2473
+ resolveLocation,
2792
2474
  );
2793
2475
  }
2794
2476
  }