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