wrec 0.34.0 → 0.35.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/package.json +3 -1
- package/scripts/declare.js +274 -0
- package/scripts/lint.js +169 -87
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "wrec",
|
|
3
3
|
"description": "a library that greatly simplifies building web components",
|
|
4
4
|
"author": "R. Mark Volkmann",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.35.1",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"bin": {
|
|
32
|
+
"wrec-declare": "./scripts/declare.js",
|
|
32
33
|
"wrec-lint": "./scripts/lint.js",
|
|
33
34
|
"wrec-scaffold": "./scripts/scaffold.js",
|
|
34
35
|
"wrec-usedby": "./scripts/used-by.js"
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
"files": [
|
|
37
38
|
"dist",
|
|
38
39
|
"scripts/ast-utils.js",
|
|
40
|
+
"scripts/declare.js",
|
|
39
41
|
"scripts/lint.js",
|
|
40
42
|
"scripts/scaffold.js",
|
|
41
43
|
"scripts/template.ts",
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// This script inspects a given Wrec component source file and
|
|
4
|
+
// generates TypeScript `declare` statements for the properties
|
|
5
|
+
// found in the component's `static properties` declaration.
|
|
6
|
+
//
|
|
7
|
+
// To run this, enter `npx wrec-declare [--dry] {file-path}`
|
|
8
|
+
//
|
|
9
|
+
// Include the --dry flag to report whether changes are needed
|
|
10
|
+
// without writing the modified source back to the file.
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import ts from 'typescript';
|
|
15
|
+
import {
|
|
16
|
+
collectWrecClasses,
|
|
17
|
+
getMemberName,
|
|
18
|
+
hasStaticModifier
|
|
19
|
+
} from './ast-utils.js';
|
|
20
|
+
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
const DECLARE_TYPE_MAP = new Map([
|
|
23
|
+
['Array', 'unknown[]'],
|
|
24
|
+
['Boolean', 'boolean'],
|
|
25
|
+
['Number', 'number'],
|
|
26
|
+
['Object', 'object'],
|
|
27
|
+
['String', 'string']
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
// Analyzes a parsed source file and returns any proposed `declare` edits.
|
|
31
|
+
function analyzeSourceFile(sourceFile) {
|
|
32
|
+
const edits = [];
|
|
33
|
+
const classNodes = collectWrecClasses(sourceFile);
|
|
34
|
+
|
|
35
|
+
for (const classNode of classNodes) {
|
|
36
|
+
const propertiesMember = getLastPropertiesDeclaration(classNode);
|
|
37
|
+
if (!propertiesMember) continue;
|
|
38
|
+
|
|
39
|
+
const declareLines = [];
|
|
40
|
+
for (const property of propertiesMember.initializer.properties) {
|
|
41
|
+
if (!ts.isPropertyAssignment(property)) continue;
|
|
42
|
+
|
|
43
|
+
const propName = getMemberName(property);
|
|
44
|
+
if (!propName || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(propName)) continue;
|
|
45
|
+
|
|
46
|
+
const declareType = getDeclareType(property.initializer);
|
|
47
|
+
if (!declareType) continue;
|
|
48
|
+
declareLines.push(createDeclareLine(propName, declareType));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const start = propertiesMember.end;
|
|
52
|
+
const end = findDeclareBlockEnd(classNode, propertiesMember, sourceFile);
|
|
53
|
+
const nextText = buildDeclareBlock(
|
|
54
|
+
sourceFile,
|
|
55
|
+
classNode,
|
|
56
|
+
propertiesMember,
|
|
57
|
+
declareLines
|
|
58
|
+
);
|
|
59
|
+
const currentText = sourceFile.text.slice(start, end);
|
|
60
|
+
|
|
61
|
+
if (nextText !== currentText) {
|
|
62
|
+
edits.push({end, start, text: nextText});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (classNodes.length === 0) {
|
|
67
|
+
return {changed: false, foundWrecSubclass: false, text: sourceFile.text};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (edits.length === 0) {
|
|
71
|
+
return {changed: false, foundWrecSubclass: true, text: sourceFile.text};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let nextSource = sourceFile.text;
|
|
75
|
+
edits.sort((a, b) => b.start - a.start);
|
|
76
|
+
|
|
77
|
+
for (const edit of edits) {
|
|
78
|
+
nextSource =
|
|
79
|
+
nextSource.slice(0, edit.start) + edit.text + nextSource.slice(edit.end);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {changed: true, foundWrecSubclass: true, text: nextSource};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Builds the `declare` block that should appear after `static properties`.
|
|
86
|
+
function buildDeclareBlock(sourceFile, classNode, propertiesMember, declareLines) {
|
|
87
|
+
const {text} = sourceFile;
|
|
88
|
+
const memberIndent = getIndent(text, propertiesMember.getStart(sourceFile));
|
|
89
|
+
const startIndex = classNode.members.indexOf(propertiesMember) + 1;
|
|
90
|
+
let nextMember = null;
|
|
91
|
+
|
|
92
|
+
for (let index = startIndex; index < classNode.members.length; index += 1) {
|
|
93
|
+
const member = classNode.members[index];
|
|
94
|
+
if (isDeclarePropertyDeclaration(member)) continue;
|
|
95
|
+
|
|
96
|
+
nextMember = member;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const nextIndent = nextMember
|
|
101
|
+
? getIndent(text, nextMember.getStart(sourceFile))
|
|
102
|
+
: '';
|
|
103
|
+
|
|
104
|
+
if (declareLines.length === 0) {
|
|
105
|
+
return nextMember ? `\n\n${nextIndent}` : '\n';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const content = declareLines
|
|
109
|
+
.map(line => `${memberIndent}${line}`)
|
|
110
|
+
.join('\n');
|
|
111
|
+
return nextMember ? `\n${content}\n\n${nextIndent}` : `\n${content}\n`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Creates a single `declare` statement for a component property.
|
|
115
|
+
function createDeclareLine(propName, declareType) {
|
|
116
|
+
return `declare ${propName}: ${declareType};`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Determines what changes, if any, should be made in a source file.
|
|
120
|
+
export function evaluateSourceFile(filePath, options = {}) {
|
|
121
|
+
const {dry = false} = options;
|
|
122
|
+
const absFilePath = path.resolve(cwd, filePath);
|
|
123
|
+
validateFile(absFilePath);
|
|
124
|
+
|
|
125
|
+
const text = fs.readFileSync(absFilePath, 'utf8');
|
|
126
|
+
const result = evaluateSourceText(absFilePath, text);
|
|
127
|
+
|
|
128
|
+
if (!result.foundWrecSubclass) {
|
|
129
|
+
throw new Error('No class extending Wrec was found.');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!dry && result.changed) {
|
|
133
|
+
fs.writeFileSync(absFilePath, result.text);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Determines what changes, if any, should be made in source file text.
|
|
140
|
+
export function evaluateSourceText(filePath, text) {
|
|
141
|
+
const scriptKind = filePath.endsWith('.ts')
|
|
142
|
+
? ts.ScriptKind.TS
|
|
143
|
+
: ts.ScriptKind.JS;
|
|
144
|
+
const sourceFile = ts.createSourceFile(
|
|
145
|
+
filePath,
|
|
146
|
+
text,
|
|
147
|
+
ts.ScriptTarget.Latest,
|
|
148
|
+
true,
|
|
149
|
+
scriptKind
|
|
150
|
+
);
|
|
151
|
+
return analyzeSourceFile(sourceFile);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Finds the end of the contiguous `declare` block after `static properties`.
|
|
155
|
+
function findDeclareBlockEnd(classNode, propertiesMember, sourceFile) {
|
|
156
|
+
const startIndex = classNode.members.indexOf(propertiesMember) + 1;
|
|
157
|
+
|
|
158
|
+
for (let index = startIndex; index < classNode.members.length; index += 1) {
|
|
159
|
+
const member = classNode.members[index];
|
|
160
|
+
if (isDeclarePropertyDeclaration(member)) continue;
|
|
161
|
+
return member.getStart(sourceFile);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return classNode.end - 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Gets the TypeScript type string for a property configuration object.
|
|
168
|
+
function getDeclareType(initializer) {
|
|
169
|
+
if (!ts.isObjectLiteralExpression(initializer)) return null;
|
|
170
|
+
|
|
171
|
+
for (const property of initializer.properties) {
|
|
172
|
+
if (
|
|
173
|
+
!ts.isPropertyAssignment(property) ||
|
|
174
|
+
getMemberName(property) !== 'type' ||
|
|
175
|
+
!ts.isIdentifier(property.initializer)
|
|
176
|
+
) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return DECLARE_TYPE_MAP.get(property.initializer.text) ?? null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Gets the leading indentation in the line that contains a given position.
|
|
187
|
+
function getIndent(text, pos) {
|
|
188
|
+
const lineStart = text.lastIndexOf('\n', pos - 1) + 1;
|
|
189
|
+
const match = /^[ \t]*/.exec(text.slice(lineStart));
|
|
190
|
+
return match ? match[0] : '';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Gets the last `static properties = { ... }` declaration in a class.
|
|
194
|
+
function getLastPropertiesDeclaration(classNode) {
|
|
195
|
+
let propertiesMember = null;
|
|
196
|
+
|
|
197
|
+
for (const member of classNode.members) {
|
|
198
|
+
if (
|
|
199
|
+
ts.isPropertyDeclaration(member) &&
|
|
200
|
+
hasStaticModifier(member) &&
|
|
201
|
+
getMemberName(member) === 'properties' &&
|
|
202
|
+
member.initializer &&
|
|
203
|
+
ts.isObjectLiteralExpression(member.initializer)
|
|
204
|
+
) {
|
|
205
|
+
propertiesMember = member;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return propertiesMember;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Determines whether a class member is a `declare` property declaration.
|
|
213
|
+
function isDeclarePropertyDeclaration(member) {
|
|
214
|
+
return (
|
|
215
|
+
ts.isPropertyDeclaration(member) &&
|
|
216
|
+
ts.canHaveModifiers(member) &&
|
|
217
|
+
ts
|
|
218
|
+
.getModifiers(member)
|
|
219
|
+
?.some(mod => mod.kind === ts.SyntaxKind.DeclareKeyword) === true
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Handles CLI arguments and runs the script.
|
|
224
|
+
function main() {
|
|
225
|
+
const args = process.argv.slice(2);
|
|
226
|
+
const unknownFlags = args.filter(
|
|
227
|
+
arg => arg.startsWith('--') && arg !== '--dry'
|
|
228
|
+
);
|
|
229
|
+
if (unknownFlags.length > 0) {
|
|
230
|
+
throw new Error(`unknown option: ${unknownFlags[0]}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const inputPaths = args.filter(arg => !arg.startsWith('--'));
|
|
234
|
+
if (inputPaths.length !== 1) {
|
|
235
|
+
throw new Error('Specify a single source file');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const dry = args.includes('--dry');
|
|
239
|
+
const result = evaluateSourceFile(inputPaths[0], {dry});
|
|
240
|
+
|
|
241
|
+
if (dry) {
|
|
242
|
+
if (result.changed) process.exit(1);
|
|
243
|
+
console.info('no changes needed');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (result.changed) {
|
|
248
|
+
console.info('updated source file');
|
|
249
|
+
} else {
|
|
250
|
+
console.info('no changes needed');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Validates that a source file exists and has a supported extension.
|
|
255
|
+
function validateFile(absFilePath) {
|
|
256
|
+
if (!fs.existsSync(absFilePath)) throw new Error('File not found');
|
|
257
|
+
|
|
258
|
+
const stat = fs.statSync(absFilePath);
|
|
259
|
+
if (!stat.isFile()) throw new Error('Not a file');
|
|
260
|
+
|
|
261
|
+
if (!/\.(js|ts)$/.test(absFilePath) || /\.d\.ts$/.test(absFilePath)) {
|
|
262
|
+
throw new Error('Unsupported file type');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Runs the CLI when this module is executed directly.
|
|
267
|
+
if (import.meta.main) {
|
|
268
|
+
try {
|
|
269
|
+
main();
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
}
|
package/scripts/lint.js
CHANGED
|
@@ -119,6 +119,13 @@ const THIS_CALL_RE = /this\.([A-Za-z_$][\w$]*)\s*\(/g;
|
|
|
119
119
|
const THIS_REF_RE = /this\.([A-Za-z_$][\w$]*)(\.[A-Za-z_$][\w$]*)*/g;
|
|
120
120
|
const PLACEHOLDER_PREFIX = '__WREC_PLACEHOLDER__';
|
|
121
121
|
const RESERVED_PROPERTY_NAMES = new Set(['class', 'style']);
|
|
122
|
+
const SUPPORTED_PROPERTY_TYPE_NAMES = new Set([
|
|
123
|
+
'Array',
|
|
124
|
+
'Boolean',
|
|
125
|
+
'Number',
|
|
126
|
+
'Object',
|
|
127
|
+
'String'
|
|
128
|
+
]);
|
|
122
129
|
const GETTER_PREFIX = 'get ';
|
|
123
130
|
const SUPPORTED_EVENT_NAMES = new Set([
|
|
124
131
|
'blur',
|
|
@@ -444,6 +451,21 @@ function collectSupportedPropertyNames(classNode) {
|
|
|
444
451
|
return supportedProps;
|
|
445
452
|
}
|
|
446
453
|
|
|
454
|
+
// Collects declared TypeScript property types from a component class.
|
|
455
|
+
function collectDeclaredPropertyTypes(classNode) {
|
|
456
|
+
const declaredProps = new Map();
|
|
457
|
+
|
|
458
|
+
for (const member of classNode.members) {
|
|
459
|
+
if (!isDeclarePropertyDeclaration(member)) continue;
|
|
460
|
+
|
|
461
|
+
const propName = getMemberName(member);
|
|
462
|
+
if (!propName || !member.type) continue;
|
|
463
|
+
declaredProps.set(propName, member.type);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return declaredProps;
|
|
467
|
+
}
|
|
468
|
+
|
|
447
469
|
// Validates that useState mappings point at existing component properties.
|
|
448
470
|
function collectUseStateMapErrors(classNode, supportedProps, findings) {
|
|
449
471
|
// Walks the class body looking for useState calls with mapping objects.
|
|
@@ -806,27 +828,29 @@ function formatReport(
|
|
|
806
828
|
|
|
807
829
|
const hasIssues =
|
|
808
830
|
findings.duplicateProperties.length > 0 ||
|
|
809
|
-
findings.
|
|
810
|
-
findings.
|
|
831
|
+
findings.extraArguments.length > 0 ||
|
|
832
|
+
findings.incompatibleArguments.length > 0 ||
|
|
833
|
+
findings.incompatibleDeclareTypes.length > 0 ||
|
|
811
834
|
findings.invalidCheckedBindings.length > 0 ||
|
|
812
835
|
findings.invalidComputedProperties.length > 0 ||
|
|
813
|
-
findings.invalidRefAttributes.length > 0 ||
|
|
814
|
-
findings.invalidValuesConfigurations.length > 0 ||
|
|
815
836
|
findings.invalidDefaultValues.length > 0 ||
|
|
837
|
+
findings.invalidEventHandlers.length > 0 ||
|
|
816
838
|
findings.invalidFormAssocValues.length > 0 ||
|
|
839
|
+
findings.invalidHtmlNesting.length > 0 ||
|
|
840
|
+
findings.invalidRefAttributes.length > 0 ||
|
|
841
|
+
findings.invalidTypeProperties.length > 0 ||
|
|
842
|
+
findings.invalidUsedByReferences.length > 0 ||
|
|
817
843
|
findings.invalidUseStateMaps.length > 0 ||
|
|
844
|
+
findings.invalidValuesConfigurations.length > 0 ||
|
|
818
845
|
findings.missingFormAssociatedProperty.length > 0 ||
|
|
819
846
|
findings.missingTypeProperties.length > 0 ||
|
|
820
|
-
findings.
|
|
847
|
+
findings.reservedProperties.length > 0 ||
|
|
848
|
+
findings.typeErrors.length > 0 ||
|
|
821
849
|
findings.undefinedContextFunctions.length > 0 ||
|
|
822
850
|
findings.undefinedMethods.length > 0 ||
|
|
823
|
-
findings.
|
|
824
|
-
findings.incompatibleArguments.length > 0 ||
|
|
825
|
-
findings.invalidEventHandlers.length > 0 ||
|
|
826
|
-
findings.invalidHtmlNesting.length > 0 ||
|
|
827
|
-
findings.unsupportedHtmlAttributes.length > 0 ||
|
|
851
|
+
findings.undefinedProperties.length > 0 ||
|
|
828
852
|
findings.unsupportedEventNames.length > 0 ||
|
|
829
|
-
findings.
|
|
853
|
+
findings.unsupportedHtmlAttributes.length > 0;
|
|
830
854
|
|
|
831
855
|
if (showFileHeader) lines.push(`file: ${fileLabel}`);
|
|
832
856
|
|
|
@@ -859,21 +883,31 @@ function formatReport(
|
|
|
859
883
|
findings.duplicateProperties.forEach(name => lines.push(` ${name}`));
|
|
860
884
|
}
|
|
861
885
|
|
|
862
|
-
if (findings.
|
|
863
|
-
lines.push('
|
|
864
|
-
findings.
|
|
886
|
+
if (findings.extraArguments.length > 0) {
|
|
887
|
+
lines.push('extra arguments:');
|
|
888
|
+
findings.extraArguments.forEach(finding => {
|
|
889
|
+
lines.push(
|
|
890
|
+
` ${finding.methodName}: argument ${finding.argumentIndex} ` +
|
|
891
|
+
`"${finding.argument}" exceeds the ` +
|
|
892
|
+
`${finding.parameterCount}-parameter signature`
|
|
893
|
+
);
|
|
894
|
+
});
|
|
865
895
|
}
|
|
866
896
|
|
|
867
|
-
if (findings.
|
|
868
|
-
lines.push('
|
|
869
|
-
findings.
|
|
870
|
-
lines.push(
|
|
871
|
-
|
|
897
|
+
if (findings.incompatibleArguments.length > 0) {
|
|
898
|
+
lines.push('incompatible arguments:');
|
|
899
|
+
findings.incompatibleArguments.forEach(finding => {
|
|
900
|
+
lines.push(
|
|
901
|
+
` ${finding.methodName}: argument "${finding.argument}" ` +
|
|
902
|
+
`has type ${finding.argumentType}, but parameter ` +
|
|
903
|
+
`"${finding.parameterName}" expects ${finding.parameterType}`
|
|
904
|
+
);
|
|
905
|
+
});
|
|
872
906
|
}
|
|
873
907
|
|
|
874
|
-
if (findings.
|
|
875
|
-
lines.push('
|
|
876
|
-
findings.
|
|
908
|
+
if (findings.incompatibleDeclareTypes.length > 0) {
|
|
909
|
+
lines.push('incompatible declare types:');
|
|
910
|
+
findings.incompatibleDeclareTypes.forEach(message =>
|
|
877
911
|
lines.push(` ${message}`)
|
|
878
912
|
);
|
|
879
913
|
}
|
|
@@ -885,23 +919,23 @@ function formatReport(
|
|
|
885
919
|
);
|
|
886
920
|
}
|
|
887
921
|
|
|
888
|
-
if (findings.
|
|
889
|
-
lines.push('invalid
|
|
890
|
-
findings.
|
|
922
|
+
if (findings.invalidComputedProperties.length > 0) {
|
|
923
|
+
lines.push('invalid computed properties:');
|
|
924
|
+
findings.invalidComputedProperties.forEach(message =>
|
|
891
925
|
lines.push(` ${message}`)
|
|
892
926
|
);
|
|
893
927
|
}
|
|
894
928
|
|
|
895
|
-
if (findings.
|
|
896
|
-
lines.push('invalid values
|
|
897
|
-
findings.
|
|
929
|
+
if (findings.invalidDefaultValues.length > 0) {
|
|
930
|
+
lines.push('invalid default values:');
|
|
931
|
+
findings.invalidDefaultValues.forEach(message =>
|
|
898
932
|
lines.push(` ${message}`)
|
|
899
933
|
);
|
|
900
934
|
}
|
|
901
935
|
|
|
902
|
-
if (findings.
|
|
903
|
-
lines.push('invalid
|
|
904
|
-
findings.
|
|
936
|
+
if (findings.invalidEventHandlers.length > 0) {
|
|
937
|
+
lines.push('invalid event handler references:');
|
|
938
|
+
findings.invalidEventHandlers.forEach(message =>
|
|
905
939
|
lines.push(` ${message}`)
|
|
906
940
|
);
|
|
907
941
|
}
|
|
@@ -913,38 +947,28 @@ function formatReport(
|
|
|
913
947
|
);
|
|
914
948
|
}
|
|
915
949
|
|
|
916
|
-
if (findings.
|
|
917
|
-
lines.push('
|
|
918
|
-
findings.
|
|
919
|
-
lines.push(` ${message}`)
|
|
920
|
-
);
|
|
950
|
+
if (findings.invalidHtmlNesting.length > 0) {
|
|
951
|
+
lines.push('invalid html nesting:');
|
|
952
|
+
findings.invalidHtmlNesting.forEach(message => lines.push(` ${message}`));
|
|
921
953
|
}
|
|
922
954
|
|
|
923
|
-
if (findings.
|
|
924
|
-
lines.push('
|
|
925
|
-
findings.
|
|
955
|
+
if (findings.invalidRefAttributes.length > 0) {
|
|
956
|
+
lines.push('invalid ref attributes:');
|
|
957
|
+
findings.invalidRefAttributes.forEach(message =>
|
|
926
958
|
lines.push(` ${message}`)
|
|
927
959
|
);
|
|
928
960
|
}
|
|
929
961
|
|
|
930
|
-
if (findings.
|
|
931
|
-
lines.push('
|
|
932
|
-
findings.
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
if (findings.undefinedContextFunctions.length > 0) {
|
|
936
|
-
lines.push('undefined context functions:');
|
|
937
|
-
findings.undefinedContextFunctions.forEach(name => lines.push(` ${name}`));
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
if (findings.undefinedMethods.length > 0) {
|
|
941
|
-
lines.push('undefined methods:');
|
|
942
|
-
findings.undefinedMethods.forEach(name => lines.push(` ${name}`));
|
|
962
|
+
if (findings.invalidTypeProperties.length > 0) {
|
|
963
|
+
lines.push('invalid type properties:');
|
|
964
|
+
findings.invalidTypeProperties.forEach(message =>
|
|
965
|
+
lines.push(` ${message}`)
|
|
966
|
+
);
|
|
943
967
|
}
|
|
944
968
|
|
|
945
|
-
if (findings.
|
|
946
|
-
lines.push('invalid
|
|
947
|
-
findings.
|
|
969
|
+
if (findings.invalidUsedByReferences.length > 0) {
|
|
970
|
+
lines.push('invalid usedBy references:');
|
|
971
|
+
findings.invalidUsedByReferences.forEach(message =>
|
|
948
972
|
lines.push(` ${message}`)
|
|
949
973
|
);
|
|
950
974
|
}
|
|
@@ -954,31 +978,30 @@ function formatReport(
|
|
|
954
978
|
findings.invalidUseStateMaps.forEach(message => lines.push(` ${message}`));
|
|
955
979
|
}
|
|
956
980
|
|
|
957
|
-
if (findings.
|
|
958
|
-
lines.push('invalid
|
|
959
|
-
findings.
|
|
981
|
+
if (findings.invalidValuesConfigurations.length > 0) {
|
|
982
|
+
lines.push('invalid values configurations:');
|
|
983
|
+
findings.invalidValuesConfigurations.forEach(message =>
|
|
984
|
+
lines.push(` ${message}`)
|
|
985
|
+
);
|
|
960
986
|
}
|
|
961
987
|
|
|
962
|
-
if (findings.
|
|
963
|
-
lines.push('
|
|
964
|
-
findings.
|
|
965
|
-
lines.push(
|
|
966
|
-
|
|
967
|
-
`"${finding.argument}" exceeds the ` +
|
|
968
|
-
`${finding.parameterCount}-parameter signature`
|
|
969
|
-
);
|
|
970
|
-
});
|
|
988
|
+
if (findings.missingFormAssociatedProperty.length > 0) {
|
|
989
|
+
lines.push('missing formAssociated property:');
|
|
990
|
+
findings.missingFormAssociatedProperty.forEach(message =>
|
|
991
|
+
lines.push(` ${message}`)
|
|
992
|
+
);
|
|
971
993
|
}
|
|
972
994
|
|
|
973
|
-
if (findings.
|
|
974
|
-
lines.push('
|
|
975
|
-
findings.
|
|
976
|
-
lines.push(
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
995
|
+
if (findings.missingTypeProperties.length > 0) {
|
|
996
|
+
lines.push('missing type properties:');
|
|
997
|
+
findings.missingTypeProperties.forEach(message =>
|
|
998
|
+
lines.push(` ${message}`)
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (findings.reservedProperties.length > 0) {
|
|
1003
|
+
lines.push('reserved property names:');
|
|
1004
|
+
findings.reservedProperties.forEach(name => lines.push(` ${name}`));
|
|
982
1005
|
}
|
|
983
1006
|
|
|
984
1007
|
if (findings.typeErrors.length > 0) {
|
|
@@ -988,11 +1011,19 @@ function formatReport(
|
|
|
988
1011
|
});
|
|
989
1012
|
}
|
|
990
1013
|
|
|
991
|
-
if (findings.
|
|
992
|
-
lines.push('
|
|
993
|
-
findings.
|
|
994
|
-
|
|
995
|
-
|
|
1014
|
+
if (findings.undefinedContextFunctions.length > 0) {
|
|
1015
|
+
lines.push('undefined context functions:');
|
|
1016
|
+
findings.undefinedContextFunctions.forEach(name => lines.push(` ${name}`));
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (findings.undefinedMethods.length > 0) {
|
|
1020
|
+
lines.push('undefined methods:');
|
|
1021
|
+
findings.undefinedMethods.forEach(name => lines.push(` ${name}`));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (findings.undefinedProperties.length > 0) {
|
|
1025
|
+
lines.push('undefined properties:');
|
|
1026
|
+
findings.undefinedProperties.forEach(name => lines.push(` ${name}`));
|
|
996
1027
|
}
|
|
997
1028
|
|
|
998
1029
|
if (findings.unsupportedEventNames.length > 0) {
|
|
@@ -1002,6 +1033,13 @@ function formatReport(
|
|
|
1002
1033
|
);
|
|
1003
1034
|
}
|
|
1004
1035
|
|
|
1036
|
+
if (findings.unsupportedHtmlAttributes.length > 0) {
|
|
1037
|
+
lines.push('unsupported html attributes:');
|
|
1038
|
+
findings.unsupportedHtmlAttributes.forEach(message =>
|
|
1039
|
+
lines.push(` ${message}`)
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1005
1043
|
if (!hasIssues && showNoIssuesMessage) lines.push('no issues found');
|
|
1006
1044
|
|
|
1007
1045
|
return `${lines.join('\n')}\n`;
|
|
@@ -1151,6 +1189,11 @@ function getPropertyTypeText(checker, sourceFile, expression) {
|
|
|
1151
1189
|
return checker.typeToString(type);
|
|
1152
1190
|
}
|
|
1153
1191
|
|
|
1192
|
+
// Gets the TypeScript type text for a declared property member.
|
|
1193
|
+
function getPropertyTypeTextFromNode(sourceFile, typeNode) {
|
|
1194
|
+
return typeNode.getText(sourceFile).trim();
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1154
1197
|
// Returns an array of string literal values from a property when possible.
|
|
1155
1198
|
function getStringArrayLiteral(property) {
|
|
1156
1199
|
if (!property || !ts.isPropertyAssignment(property)) return undefined;
|
|
@@ -1260,6 +1303,17 @@ function isCallCallee(node) {
|
|
|
1260
1303
|
return ts.isCallExpression(node.parent) && node.parent.expression === node;
|
|
1261
1304
|
}
|
|
1262
1305
|
|
|
1306
|
+
// Returns whether a class member is a declared property declaration.
|
|
1307
|
+
function isDeclarePropertyDeclaration(member) {
|
|
1308
|
+
return (
|
|
1309
|
+
ts.isPropertyDeclaration(member) &&
|
|
1310
|
+
ts.canHaveModifiers(member) &&
|
|
1311
|
+
ts
|
|
1312
|
+
.getModifiers(member)
|
|
1313
|
+
?.some(mod => mod.kind === ts.SyntaxKind.DeclareKeyword)
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1263
1317
|
// Returns whether a reference refers to a getter method.
|
|
1264
1318
|
function isGetterReference(reference) {
|
|
1265
1319
|
return reference.startsWith(GETTER_PREFIX);
|
|
@@ -1347,12 +1401,14 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1347
1401
|
propertyEntries,
|
|
1348
1402
|
reservedProperties
|
|
1349
1403
|
} = extractProperties(sourceFile, checker, classNode);
|
|
1404
|
+
const declaredPropertyTypes = collectDeclaredPropertyTypes(classNode);
|
|
1350
1405
|
const getterNames = collectGetterNames(classNode);
|
|
1351
1406
|
const allMethods = collectClassMethods(classNode);
|
|
1352
1407
|
const findings = {
|
|
1353
1408
|
duplicateProperties,
|
|
1354
1409
|
extraArguments: [],
|
|
1355
1410
|
incompatibleArguments: [],
|
|
1411
|
+
incompatibleDeclareTypes: [],
|
|
1356
1412
|
invalidCheckedBindings: [],
|
|
1357
1413
|
invalidComputedProperties: [],
|
|
1358
1414
|
invalidDefaultValues: [],
|
|
@@ -1360,8 +1416,9 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1360
1416
|
invalidFormAssocValues: [],
|
|
1361
1417
|
invalidHtmlNesting: [],
|
|
1362
1418
|
invalidRefAttributes: [],
|
|
1363
|
-
|
|
1419
|
+
invalidTypeProperties: [],
|
|
1364
1420
|
invalidUsedByReferences: [],
|
|
1421
|
+
invalidUseStateMaps: [],
|
|
1365
1422
|
invalidValuesConfigurations: [],
|
|
1366
1423
|
missingFormAssociatedProperty: [],
|
|
1367
1424
|
missingTypeProperties: [],
|
|
@@ -1370,8 +1427,8 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1370
1427
|
undefinedContextFunctions: [],
|
|
1371
1428
|
undefinedMethods: [],
|
|
1372
1429
|
undefinedProperties: [],
|
|
1373
|
-
|
|
1374
|
-
|
|
1430
|
+
unsupportedEventNames: [],
|
|
1431
|
+
unsupportedHtmlAttributes: []
|
|
1375
1432
|
};
|
|
1376
1433
|
const templateExprs = extractTemplateExpressions(
|
|
1377
1434
|
classNode,
|
|
@@ -1414,6 +1471,8 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1414
1471
|
|
|
1415
1472
|
validatePropertyConfigs(
|
|
1416
1473
|
checker,
|
|
1474
|
+
sourceFile,
|
|
1475
|
+
declaredPropertyTypes,
|
|
1417
1476
|
supportedProps,
|
|
1418
1477
|
propertyEntries,
|
|
1419
1478
|
getterNames,
|
|
@@ -1457,6 +1516,7 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1457
1516
|
a.methodName.localeCompare(b.methodName) ||
|
|
1458
1517
|
a.parameterName.localeCompare(b.parameterName)
|
|
1459
1518
|
);
|
|
1519
|
+
findings.incompatibleDeclareTypes.sort();
|
|
1460
1520
|
findings.invalidCheckedBindings.sort();
|
|
1461
1521
|
findings.invalidComputedProperties.sort();
|
|
1462
1522
|
findings.invalidDefaultValues.sort();
|
|
@@ -1464,8 +1524,9 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1464
1524
|
findings.invalidFormAssocValues.sort();
|
|
1465
1525
|
findings.invalidHtmlNesting.sort();
|
|
1466
1526
|
findings.invalidRefAttributes.sort();
|
|
1467
|
-
findings.
|
|
1527
|
+
findings.invalidTypeProperties.sort();
|
|
1468
1528
|
findings.invalidUsedByReferences.sort();
|
|
1529
|
+
findings.invalidUseStateMaps.sort();
|
|
1469
1530
|
findings.invalidValuesConfigurations.sort();
|
|
1470
1531
|
findings.missingFormAssociatedProperty.sort();
|
|
1471
1532
|
findings.missingTypeProperties.sort();
|
|
@@ -1474,8 +1535,8 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1474
1535
|
findings.undefinedContextFunctions.sort();
|
|
1475
1536
|
findings.undefinedMethods.sort();
|
|
1476
1537
|
findings.undefinedProperties.sort();
|
|
1477
|
-
findings.unsupportedHtmlAttributes.sort();
|
|
1478
1538
|
findings.unsupportedEventNames.sort();
|
|
1539
|
+
findings.unsupportedHtmlAttributes.sort();
|
|
1479
1540
|
|
|
1480
1541
|
return formatReport(
|
|
1481
1542
|
filePath,
|
|
@@ -1848,6 +1909,8 @@ function validateHtmlNesting(node, findings) {
|
|
|
1848
1909
|
// Validates all configured component property metadata entries.
|
|
1849
1910
|
function validatePropertyConfigs(
|
|
1850
1911
|
checker,
|
|
1912
|
+
sourceFile,
|
|
1913
|
+
declaredPropertyTypes,
|
|
1851
1914
|
supportedProps,
|
|
1852
1915
|
propertyEntries,
|
|
1853
1916
|
getterNames,
|
|
@@ -1855,9 +1918,10 @@ function validatePropertyConfigs(
|
|
|
1855
1918
|
findings
|
|
1856
1919
|
) {
|
|
1857
1920
|
for (const {config, propName} of propertyEntries) {
|
|
1921
|
+
const computedProp = getObjectProperty(config, 'computed');
|
|
1922
|
+
const declaredTypeNode = declaredPropertyTypes.get(propName);
|
|
1858
1923
|
const typeProp = getObjectProperty(config, 'type');
|
|
1859
1924
|
const usedByProp = getObjectProperty(config, 'usedBy');
|
|
1860
|
-
const computedProp = getObjectProperty(config, 'computed');
|
|
1861
1925
|
const valueProp = getObjectProperty(config, 'value');
|
|
1862
1926
|
const valuesProp = getObjectProperty(config, 'values');
|
|
1863
1927
|
|
|
@@ -1870,6 +1934,24 @@ function validatePropertyConfigs(
|
|
|
1870
1934
|
findings.missingTypeProperties.push(
|
|
1871
1935
|
`property "${propName}" does not specify a type`
|
|
1872
1936
|
);
|
|
1937
|
+
} else if (
|
|
1938
|
+
!SUPPORTED_PROPERTY_TYPE_NAMES.has(typeExpressionKind(typeExpression))
|
|
1939
|
+
) {
|
|
1940
|
+
findings.invalidTypeProperties.push(
|
|
1941
|
+
`property "${propName}" type must be one of ` +
|
|
1942
|
+
'Boolean, Number, String, Object, or Array'
|
|
1943
|
+
);
|
|
1944
|
+
} else if (declaredTypeNode) {
|
|
1945
|
+
const runtimeType = checker.getTypeAtLocation(typeExpression);
|
|
1946
|
+
const declaredType = checker.getTypeFromTypeNode(declaredTypeNode);
|
|
1947
|
+
if (!checker.isTypeAssignableTo(runtimeType, declaredType)) {
|
|
1948
|
+
findings.incompatibleDeclareTypes.push(
|
|
1949
|
+
`property "${propName}" declare type ` +
|
|
1950
|
+
`"${getPropertyTypeTextFromNode(sourceFile, declaredTypeNode)}" ` +
|
|
1951
|
+
`is not compatible with static properties type ` +
|
|
1952
|
+
`"${getPropertyConfigTypeName(typeExpressionKind(typeExpression))}"`
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1873
1955
|
}
|
|
1874
1956
|
|
|
1875
1957
|
if (usedByProp && ts.isPropertyAssignment(usedByProp)) {
|