wrec 0.40.0 → 0.40.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 +1 -1
- package/scripts/lint.js +489 -152
package/package.json
CHANGED
package/scripts/lint.js
CHANGED
|
@@ -167,7 +167,12 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
|
|
|
167
167
|
if (!metadata.classMethods.has(codeNode.text)) {
|
|
168
168
|
uniquePush(
|
|
169
169
|
findings.invalidEventHandlers,
|
|
170
|
-
|
|
170
|
+
metadata.location
|
|
171
|
+
? {
|
|
172
|
+
location: metadata.location,
|
|
173
|
+
message: `"${codeNode.text}" is not a defined instance method`
|
|
174
|
+
}
|
|
175
|
+
: `"${codeNode.text}" is not a defined instance method`
|
|
171
176
|
);
|
|
172
177
|
}
|
|
173
178
|
}
|
|
@@ -180,9 +185,15 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
|
|
|
180
185
|
if (!symbol) {
|
|
181
186
|
const name = node.name.text;
|
|
182
187
|
if (isCallCallee(node)) {
|
|
183
|
-
uniquePush(
|
|
188
|
+
uniquePush(
|
|
189
|
+
findings.undefinedMethods,
|
|
190
|
+
metadata.location ? {location: metadata.location, message: name} : name
|
|
191
|
+
);
|
|
184
192
|
} else {
|
|
185
|
-
uniquePush(
|
|
193
|
+
uniquePush(
|
|
194
|
+
findings.undefinedProperties,
|
|
195
|
+
metadata.location ? {location: metadata.location, message: name} : name
|
|
196
|
+
);
|
|
186
197
|
}
|
|
187
198
|
}
|
|
188
199
|
}
|
|
@@ -193,7 +204,12 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
|
|
|
193
204
|
if (!metadata.contextKeys.has(callee.text)) {
|
|
194
205
|
const symbol = checker.getSymbolAtLocation(callee);
|
|
195
206
|
if (!symbol || requiresContextFunction(symbol, metadata.sourceFile)) {
|
|
196
|
-
uniquePush(
|
|
207
|
+
uniquePush(
|
|
208
|
+
findings.undefinedContextFunctions,
|
|
209
|
+
metadata.location
|
|
210
|
+
? {location: metadata.location, message: callee.text}
|
|
211
|
+
: callee.text
|
|
212
|
+
);
|
|
197
213
|
}
|
|
198
214
|
}
|
|
199
215
|
} else if (
|
|
@@ -202,7 +218,14 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
|
|
|
202
218
|
) {
|
|
203
219
|
const ownerType = checker.getTypeAtLocation(callee.expression);
|
|
204
220
|
const symbol = ownerType.getProperty(callee.name.text);
|
|
205
|
-
if (!symbol)
|
|
221
|
+
if (!symbol) {
|
|
222
|
+
uniquePush(
|
|
223
|
+
findings.undefinedMethods,
|
|
224
|
+
metadata.location
|
|
225
|
+
? {location: metadata.location, message: callee.name.text}
|
|
226
|
+
: callee.name.text
|
|
227
|
+
);
|
|
228
|
+
}
|
|
206
229
|
}
|
|
207
230
|
|
|
208
231
|
const signature =
|
|
@@ -229,6 +252,7 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
|
|
|
229
252
|
findings.extraArguments.push({
|
|
230
253
|
argument: toUserFacingExpression(argument.getText()),
|
|
231
254
|
argumentIndex: parameters.length + index + 1,
|
|
255
|
+
location: metadata.location ?? null,
|
|
232
256
|
methodName: toUserFacingExpression(callee.getText()),
|
|
233
257
|
parameterCount: parameters.length
|
|
234
258
|
});
|
|
@@ -257,6 +281,7 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
|
|
|
257
281
|
findings.incompatibleArguments.push({
|
|
258
282
|
argument: toUserFacingExpression(argument.getText()),
|
|
259
283
|
argumentType: checker.typeToString(argumentType),
|
|
284
|
+
location: metadata.location ?? null,
|
|
260
285
|
methodName: toUserFacingExpression(callee.getText()),
|
|
261
286
|
parameterName: parameterSymbol.getName(),
|
|
262
287
|
parameterType: checker.typeToString(parameterType)
|
|
@@ -276,6 +301,7 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
|
|
|
276
301
|
if (!isNumericLikeType(leftType)) {
|
|
277
302
|
findings.typeErrors.push({
|
|
278
303
|
expression: toUserFacingExpression(node.getText()),
|
|
304
|
+
location: metadata.location ?? null,
|
|
279
305
|
message:
|
|
280
306
|
`left operand "${toUserFacingExpression(node.left.getText())}" ` +
|
|
281
307
|
`has type ${checker.typeToString(leftType)}, ` +
|
|
@@ -286,6 +312,7 @@ function analyzeCodeNode(codeNode, checker, classNode, findings, metadata) {
|
|
|
286
312
|
if (!isNumericLikeType(rightType)) {
|
|
287
313
|
findings.typeErrors.push({
|
|
288
314
|
expression: toUserFacingExpression(node.getText()),
|
|
315
|
+
location: metadata.location ?? null,
|
|
289
316
|
message:
|
|
290
317
|
`right operand "${toUserFacingExpression(node.right.getText())}" ` +
|
|
291
318
|
`has type ${checker.typeToString(rightType)}, ` +
|
|
@@ -515,8 +542,14 @@ function collectUseStateMapErrors(classNode, supportedProps, findings) {
|
|
|
515
542
|
const componentProp = property.initializer.text;
|
|
516
543
|
if (!supportedProps.has(componentProp)) {
|
|
517
544
|
findings.invalidUseStateMaps.push(
|
|
518
|
-
|
|
519
|
-
|
|
545
|
+
{
|
|
546
|
+
location: node
|
|
547
|
+
.getSourceFile()
|
|
548
|
+
.getLineAndCharacterOfPosition(property.getStart()),
|
|
549
|
+
message:
|
|
550
|
+
`useState maps state property "${statePath}" to ` +
|
|
551
|
+
`missing component property "${componentProp}"`
|
|
552
|
+
}
|
|
520
553
|
);
|
|
521
554
|
}
|
|
522
555
|
}
|
|
@@ -591,6 +624,7 @@ function createProgram(filePath, sourceText) {
|
|
|
591
624
|
function extractProperties(sourceFile, checker, classNode) {
|
|
592
625
|
const duplicateProperties = [];
|
|
593
626
|
let formAssociated = false;
|
|
627
|
+
const propertyLocations = new Map();
|
|
594
628
|
const propertyEntries = [];
|
|
595
629
|
const reservedProperties = [];
|
|
596
630
|
const supportedProps = new Map();
|
|
@@ -635,22 +669,41 @@ function extractProperties(sourceFile, checker, classNode) {
|
|
|
635
669
|
if (!propName || !ts.isObjectLiteralExpression(property.initializer)) {
|
|
636
670
|
continue;
|
|
637
671
|
}
|
|
672
|
+
const propertyLocation = sourceFile.getLineAndCharacterOfPosition(
|
|
673
|
+
property.name.getStart(sourceFile)
|
|
674
|
+
);
|
|
638
675
|
|
|
639
676
|
if (
|
|
640
677
|
supportedProps.has(propName) &&
|
|
641
|
-
!duplicateProperties.
|
|
678
|
+
!duplicateProperties.some(
|
|
679
|
+
finding => getLocatedFindingMessage(finding).startsWith(`"${propName}" `)
|
|
680
|
+
)
|
|
642
681
|
) {
|
|
643
|
-
duplicateProperties.push(
|
|
682
|
+
duplicateProperties.push({
|
|
683
|
+
location: propertyLocation,
|
|
684
|
+
message:
|
|
685
|
+
`"${propName}" first declared at ` +
|
|
686
|
+
`${formatLocation(propertyLocations.get(propName))}, ` +
|
|
687
|
+
`duplicated at ${formatLocation(propertyLocation)}`
|
|
688
|
+
});
|
|
644
689
|
}
|
|
645
690
|
if (
|
|
646
691
|
RESERVED_PROPERTY_NAMES.has(propName) &&
|
|
647
|
-
!reservedProperties.
|
|
692
|
+
!reservedProperties.some(
|
|
693
|
+
finding => getLocatedFindingMessage(finding) === propName
|
|
694
|
+
)
|
|
648
695
|
) {
|
|
649
|
-
reservedProperties.push(
|
|
696
|
+
reservedProperties.push({
|
|
697
|
+
location: propertyLocation,
|
|
698
|
+
message: propName
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
if (!propertyLocations.has(propName)) {
|
|
702
|
+
propertyLocations.set(propName, propertyLocation);
|
|
650
703
|
}
|
|
651
704
|
|
|
652
705
|
const config = property.initializer;
|
|
653
|
-
propertyEntries.push({config, propName});
|
|
706
|
+
propertyEntries.push({config, propName, property});
|
|
654
707
|
const typeProp = getObjectProperty(config, 'type');
|
|
655
708
|
const computedProp = getObjectProperty(config, 'computed');
|
|
656
709
|
|
|
@@ -738,6 +791,10 @@ function extractTemplateExpressions(
|
|
|
738
791
|
}
|
|
739
792
|
|
|
740
793
|
const rendered = getTemplateLiteralText(template);
|
|
794
|
+
const resolveLocation = createTemplateLocationResolver(
|
|
795
|
+
member.getSourceFile(),
|
|
796
|
+
template
|
|
797
|
+
);
|
|
741
798
|
|
|
742
799
|
if (tag === 'css') {
|
|
743
800
|
CSS_PROPERTY_RE.lastIndex = 0;
|
|
@@ -765,7 +822,8 @@ function extractTemplateExpressions(
|
|
|
765
822
|
findings,
|
|
766
823
|
componentPropertyMaps,
|
|
767
824
|
supportedProps,
|
|
768
|
-
new Set()
|
|
825
|
+
new Set(),
|
|
826
|
+
resolveLocation
|
|
769
827
|
);
|
|
770
828
|
}
|
|
771
829
|
|
|
@@ -826,12 +884,91 @@ function findWrecFiles(rootDir, onMatch) {
|
|
|
826
884
|
walk(rootDir);
|
|
827
885
|
}
|
|
828
886
|
|
|
887
|
+
// Converts an offset within rendered template text to a source location.
|
|
888
|
+
function createTemplateLocationResolver(sourceFile, template) {
|
|
889
|
+
const segments = [];
|
|
890
|
+
|
|
891
|
+
if (ts.isNoSubstitutionTemplateLiteral(template)) {
|
|
892
|
+
segments.push({
|
|
893
|
+
renderedEnd: template.text.length,
|
|
894
|
+
renderedStart: 0,
|
|
895
|
+
sourceStart: template.getStart(sourceFile) + 1
|
|
896
|
+
});
|
|
897
|
+
} else {
|
|
898
|
+
let renderedStart = 0;
|
|
899
|
+
const headText = template.head.text;
|
|
900
|
+
segments.push({
|
|
901
|
+
renderedEnd: headText.length,
|
|
902
|
+
renderedStart,
|
|
903
|
+
sourceStart: template.head.getStart(sourceFile) + 1
|
|
904
|
+
});
|
|
905
|
+
renderedStart += headText.length;
|
|
906
|
+
|
|
907
|
+
template.templateSpans.forEach((span, index) => {
|
|
908
|
+
renderedStart += `${PLACEHOLDER_PREFIX}${index}`.length;
|
|
909
|
+
segments.push({
|
|
910
|
+
renderedEnd: renderedStart + span.literal.text.length,
|
|
911
|
+
renderedStart,
|
|
912
|
+
sourceStart: span.literal.getStart(sourceFile) + 1
|
|
913
|
+
});
|
|
914
|
+
renderedStart += span.literal.text.length;
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return offset => {
|
|
919
|
+
const segment =
|
|
920
|
+
segments.find(
|
|
921
|
+
candidate =>
|
|
922
|
+
offset >= candidate.renderedStart && offset <= candidate.renderedEnd
|
|
923
|
+
) ?? segments[segments.length - 1];
|
|
924
|
+
if (!segment) return null;
|
|
925
|
+
|
|
926
|
+
const sourceOffset =
|
|
927
|
+
segment.sourceStart + Math.max(0, offset - segment.renderedStart);
|
|
928
|
+
return sourceFile.getLineAndCharacterOfPosition(sourceOffset);
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
829
932
|
// Formats an optional source location as line and column text.
|
|
830
933
|
function formatLocation(location) {
|
|
831
934
|
if (!location) return '';
|
|
832
935
|
return `:${location.line + 1}:${location.character + 1}`;
|
|
833
936
|
}
|
|
834
937
|
|
|
938
|
+
// Gets the source location for the start of a parsed HTML node.
|
|
939
|
+
function getHtmlNodeLocation(node, resolveLocation) {
|
|
940
|
+
const [start] = node.range ?? [];
|
|
941
|
+
return Number.isInteger(start) && start >= 0 ? resolveLocation(start) : null;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Gets the source location for a specific HTML attribute within a parsed node.
|
|
945
|
+
function getHtmlAttributeLocation(node, attrName, resolveLocation) {
|
|
946
|
+
const [start] = node.range ?? [];
|
|
947
|
+
const tagName = getHtmlTagName(node);
|
|
948
|
+
if (!Number.isInteger(start) || start < 0 || !tagName) return null;
|
|
949
|
+
|
|
950
|
+
const attrsOffset = node.rawAttrs.indexOf(attrName);
|
|
951
|
+
if (attrsOffset < 0) return getHtmlNodeLocation(node, resolveLocation);
|
|
952
|
+
|
|
953
|
+
return resolveLocation(start + 1 + tagName.length + 1 + attrsOffset);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Formats a lint finding that may optionally include a source location.
|
|
957
|
+
function formatMaybeLocatedFinding(finding) {
|
|
958
|
+
if (typeof finding === 'string') return finding;
|
|
959
|
+
return `${formatLocation(finding.location)} ${finding.message}`.trim();
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Gets the message text from a lint finding that may include a location.
|
|
963
|
+
function getLocatedFindingMessage(finding) {
|
|
964
|
+
return typeof finding === 'string' ? finding : finding.message;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Compares lint findings that may optionally include source locations.
|
|
968
|
+
function compareLocatedFindings(a, b) {
|
|
969
|
+
return getLocatedFindingMessage(a).localeCompare(getLocatedFindingMessage(b));
|
|
970
|
+
}
|
|
971
|
+
|
|
835
972
|
// Formats the collected lint findings into the command-line report output.
|
|
836
973
|
function formatReport(
|
|
837
974
|
filePath,
|
|
@@ -904,14 +1041,19 @@ function formatReport(
|
|
|
904
1041
|
|
|
905
1042
|
if (findings.duplicateProperties.length > 0) {
|
|
906
1043
|
lines.push('duplicate properties:');
|
|
907
|
-
findings.duplicateProperties.forEach(
|
|
1044
|
+
findings.duplicateProperties.forEach(finding =>
|
|
1045
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1046
|
+
);
|
|
908
1047
|
}
|
|
909
1048
|
|
|
910
1049
|
if (findings.extraArguments.length > 0) {
|
|
911
1050
|
lines.push('extra arguments:');
|
|
912
1051
|
findings.extraArguments.forEach(finding => {
|
|
1052
|
+
const locationPrefix = finding.location
|
|
1053
|
+
? `${formatLocation(finding.location)} `
|
|
1054
|
+
: '';
|
|
913
1055
|
lines.push(
|
|
914
|
-
` ${finding.methodName}: argument ${finding.argumentIndex} ` +
|
|
1056
|
+
` ${locationPrefix}${finding.methodName}: argument ${finding.argumentIndex} ` +
|
|
915
1057
|
`"${finding.argument}" exceeds the ` +
|
|
916
1058
|
`${finding.parameterCount}-parameter signature`
|
|
917
1059
|
);
|
|
@@ -921,8 +1063,11 @@ function formatReport(
|
|
|
921
1063
|
if (findings.incompatibleArguments.length > 0) {
|
|
922
1064
|
lines.push('incompatible arguments:');
|
|
923
1065
|
findings.incompatibleArguments.forEach(finding => {
|
|
1066
|
+
const locationPrefix = finding.location
|
|
1067
|
+
? `${formatLocation(finding.location)} `
|
|
1068
|
+
: '';
|
|
924
1069
|
lines.push(
|
|
925
|
-
` ${finding.methodName}: argument "${finding.argument}" ` +
|
|
1070
|
+
` ${locationPrefix}${finding.methodName}: argument "${finding.argument}" ` +
|
|
926
1071
|
`has type ${finding.argumentType}, but parameter ` +
|
|
927
1072
|
`"${finding.parameterName}" expects ${finding.parameterType}`
|
|
928
1073
|
);
|
|
@@ -931,143 +1076,158 @@ function formatReport(
|
|
|
931
1076
|
|
|
932
1077
|
if (findings.incompatibleDeclareTypes.length > 0) {
|
|
933
1078
|
lines.push('incompatible declare types:');
|
|
934
|
-
findings.incompatibleDeclareTypes.forEach(
|
|
935
|
-
lines.push(` ${
|
|
1079
|
+
findings.incompatibleDeclareTypes.forEach(finding =>
|
|
1080
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
936
1081
|
);
|
|
937
1082
|
}
|
|
938
1083
|
|
|
939
1084
|
if (findings.invalidCheckedBindings.length > 0) {
|
|
940
1085
|
lines.push('invalid checked bindings:');
|
|
941
|
-
findings.invalidCheckedBindings.forEach(
|
|
942
|
-
lines.push(` ${
|
|
1086
|
+
findings.invalidCheckedBindings.forEach(finding =>
|
|
1087
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
943
1088
|
);
|
|
944
1089
|
}
|
|
945
1090
|
|
|
946
1091
|
if (findings.invalidComputedProperties.length > 0) {
|
|
947
1092
|
lines.push('invalid computed properties:');
|
|
948
|
-
findings.invalidComputedProperties.forEach(
|
|
949
|
-
lines.push(` ${
|
|
1093
|
+
findings.invalidComputedProperties.forEach(finding =>
|
|
1094
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
950
1095
|
);
|
|
951
1096
|
}
|
|
952
1097
|
|
|
953
1098
|
if (findings.invalidDefaultValues.length > 0) {
|
|
954
1099
|
lines.push('invalid default values:');
|
|
955
|
-
findings.invalidDefaultValues.forEach(
|
|
956
|
-
lines.push(` ${
|
|
1100
|
+
findings.invalidDefaultValues.forEach(finding =>
|
|
1101
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
957
1102
|
);
|
|
958
1103
|
}
|
|
959
1104
|
|
|
960
1105
|
if (findings.invalidEventHandlers.length > 0) {
|
|
961
1106
|
lines.push('invalid event handler references:');
|
|
962
|
-
findings.invalidEventHandlers.forEach(
|
|
963
|
-
lines.push(` ${
|
|
1107
|
+
findings.invalidEventHandlers.forEach(finding =>
|
|
1108
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
964
1109
|
);
|
|
965
1110
|
}
|
|
966
1111
|
|
|
967
1112
|
if (findings.invalidFormAssocValues.length > 0) {
|
|
968
1113
|
lines.push('invalid form-assoc values:');
|
|
969
|
-
findings.invalidFormAssocValues.forEach(
|
|
970
|
-
lines.push(` ${
|
|
1114
|
+
findings.invalidFormAssocValues.forEach(finding =>
|
|
1115
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
971
1116
|
);
|
|
972
1117
|
}
|
|
973
1118
|
|
|
974
1119
|
if (findings.invalidHtmlNesting.length > 0) {
|
|
975
1120
|
lines.push('invalid html nesting:');
|
|
976
|
-
findings.invalidHtmlNesting.forEach(
|
|
1121
|
+
findings.invalidHtmlNesting.forEach(finding =>
|
|
1122
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1123
|
+
);
|
|
977
1124
|
}
|
|
978
1125
|
|
|
979
1126
|
if (findings.invalidRefAttributes.length > 0) {
|
|
980
1127
|
lines.push('invalid ref attributes:');
|
|
981
|
-
findings.invalidRefAttributes.forEach(
|
|
982
|
-
lines.push(` ${
|
|
1128
|
+
findings.invalidRefAttributes.forEach(finding =>
|
|
1129
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
983
1130
|
);
|
|
984
1131
|
}
|
|
985
1132
|
|
|
986
1133
|
if (findings.invalidTypeProperties.length > 0) {
|
|
987
1134
|
lines.push('invalid type properties:');
|
|
988
|
-
findings.invalidTypeProperties.forEach(
|
|
989
|
-
lines.push(` ${
|
|
1135
|
+
findings.invalidTypeProperties.forEach(finding =>
|
|
1136
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
990
1137
|
);
|
|
991
1138
|
}
|
|
992
1139
|
|
|
993
1140
|
if (findings.invalidUsedByReferences.length > 0) {
|
|
994
1141
|
lines.push('invalid usedBy references:');
|
|
995
|
-
findings.invalidUsedByReferences.forEach(
|
|
996
|
-
lines.push(` ${
|
|
1142
|
+
findings.invalidUsedByReferences.forEach(finding =>
|
|
1143
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
997
1144
|
);
|
|
998
1145
|
}
|
|
999
1146
|
|
|
1000
1147
|
if (findings.invalidUseStateMaps.length > 0) {
|
|
1001
1148
|
lines.push('invalid useState map entries:');
|
|
1002
|
-
findings.invalidUseStateMaps.forEach(
|
|
1149
|
+
findings.invalidUseStateMaps.forEach(finding =>
|
|
1150
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1151
|
+
);
|
|
1003
1152
|
}
|
|
1004
1153
|
|
|
1005
1154
|
if (findings.invalidValueBindings.length > 0) {
|
|
1006
1155
|
lines.push('invalid value bindings:');
|
|
1007
|
-
findings.invalidValueBindings.forEach(
|
|
1008
|
-
lines.push(` ${
|
|
1156
|
+
findings.invalidValueBindings.forEach(finding =>
|
|
1157
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1009
1158
|
);
|
|
1010
1159
|
}
|
|
1011
1160
|
|
|
1012
1161
|
if (findings.invalidValuesConfigurations.length > 0) {
|
|
1013
1162
|
lines.push('invalid values configurations:');
|
|
1014
|
-
findings.invalidValuesConfigurations.forEach(
|
|
1015
|
-
lines.push(` ${
|
|
1163
|
+
findings.invalidValuesConfigurations.forEach(finding =>
|
|
1164
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1016
1165
|
);
|
|
1017
1166
|
}
|
|
1018
1167
|
|
|
1019
1168
|
if (findings.missingRequiredMembers.length > 0) {
|
|
1020
1169
|
lines.push('missing required members:');
|
|
1021
|
-
findings.missingRequiredMembers.forEach(
|
|
1022
|
-
lines.push(` ${
|
|
1170
|
+
findings.missingRequiredMembers.forEach(finding =>
|
|
1171
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1023
1172
|
);
|
|
1024
1173
|
}
|
|
1025
1174
|
|
|
1026
1175
|
if (findings.missingTypeProperties.length > 0) {
|
|
1027
1176
|
lines.push('missing type properties:');
|
|
1028
|
-
findings.missingTypeProperties.forEach(
|
|
1029
|
-
lines.push(` ${
|
|
1177
|
+
findings.missingTypeProperties.forEach(finding =>
|
|
1178
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1030
1179
|
);
|
|
1031
1180
|
}
|
|
1032
1181
|
|
|
1033
1182
|
if (findings.reservedProperties.length > 0) {
|
|
1034
1183
|
lines.push('reserved property names:');
|
|
1035
|
-
findings.reservedProperties.forEach(
|
|
1184
|
+
findings.reservedProperties.forEach(finding =>
|
|
1185
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1186
|
+
);
|
|
1036
1187
|
}
|
|
1037
1188
|
|
|
1038
1189
|
if (findings.typeErrors.length > 0) {
|
|
1039
1190
|
lines.push('type errors:');
|
|
1040
1191
|
findings.typeErrors.forEach(finding => {
|
|
1041
|
-
|
|
1192
|
+
const locationPrefix = finding.location
|
|
1193
|
+
? `${formatLocation(finding.location)} `
|
|
1194
|
+
: '';
|
|
1195
|
+
lines.push(` ${locationPrefix}${finding.expression}: ${finding.message}`);
|
|
1042
1196
|
});
|
|
1043
1197
|
}
|
|
1044
1198
|
|
|
1045
1199
|
if (findings.undefinedContextFunctions.length > 0) {
|
|
1046
1200
|
lines.push('undefined context functions:');
|
|
1047
|
-
findings.undefinedContextFunctions.forEach(
|
|
1201
|
+
findings.undefinedContextFunctions.forEach(finding =>
|
|
1202
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1203
|
+
);
|
|
1048
1204
|
}
|
|
1049
1205
|
|
|
1050
1206
|
if (findings.undefinedMethods.length > 0) {
|
|
1051
1207
|
lines.push('undefined methods:');
|
|
1052
|
-
findings.undefinedMethods.forEach(
|
|
1208
|
+
findings.undefinedMethods.forEach(finding =>
|
|
1209
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1210
|
+
);
|
|
1053
1211
|
}
|
|
1054
1212
|
|
|
1055
1213
|
if (findings.undefinedProperties.length > 0) {
|
|
1056
1214
|
lines.push('undefined properties:');
|
|
1057
|
-
findings.undefinedProperties.forEach(
|
|
1215
|
+
findings.undefinedProperties.forEach(finding =>
|
|
1216
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1217
|
+
);
|
|
1058
1218
|
}
|
|
1059
1219
|
|
|
1060
1220
|
if (findings.unsupportedEventNames.length > 0) {
|
|
1061
1221
|
lines.push('unsupported event names:');
|
|
1062
|
-
findings.unsupportedEventNames.forEach(
|
|
1063
|
-
lines.push(` ${
|
|
1222
|
+
findings.unsupportedEventNames.forEach(finding =>
|
|
1223
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1064
1224
|
);
|
|
1065
1225
|
}
|
|
1066
1226
|
|
|
1067
1227
|
if (findings.unsupportedHtmlAttributes.length > 0) {
|
|
1068
1228
|
lines.push('unsupported html attributes:');
|
|
1069
|
-
findings.unsupportedHtmlAttributes.forEach(
|
|
1070
|
-
lines.push(` ${
|
|
1229
|
+
findings.unsupportedHtmlAttributes.forEach(finding =>
|
|
1230
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1071
1231
|
);
|
|
1072
1232
|
}
|
|
1073
1233
|
|
|
@@ -1557,14 +1717,32 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1557
1717
|
}));
|
|
1558
1718
|
|
|
1559
1719
|
if (allMethods.has('formAssociatedCallback') && !formAssociated) {
|
|
1720
|
+
const callbackMember = classNode.members.find(
|
|
1721
|
+
member =>
|
|
1722
|
+
ts.isMethodDeclaration(member) &&
|
|
1723
|
+
getMemberName(member) === 'formAssociatedCallback'
|
|
1724
|
+
);
|
|
1560
1725
|
findings.missingRequiredMembers.push(
|
|
1561
|
-
|
|
1726
|
+
{
|
|
1727
|
+
location: callbackMember
|
|
1728
|
+
? callbackMember
|
|
1729
|
+
.getSourceFile()
|
|
1730
|
+
.getLineAndCharacterOfPosition(
|
|
1731
|
+
callbackMember.name.getStart(callbackMember.getSourceFile())
|
|
1732
|
+
)
|
|
1733
|
+
: null,
|
|
1734
|
+
message:
|
|
1735
|
+
'formAssociatedCallback is defined, but static formAssociated is not true'
|
|
1736
|
+
}
|
|
1562
1737
|
);
|
|
1563
1738
|
}
|
|
1564
1739
|
if (!hasStaticHtmlDefinition(classNode)) {
|
|
1565
|
-
findings.missingRequiredMembers.push(
|
|
1566
|
-
|
|
1567
|
-
|
|
1740
|
+
findings.missingRequiredMembers.push({
|
|
1741
|
+
location: sourceFile.getLineAndCharacterOfPosition(
|
|
1742
|
+
classNode.name?.getStart(sourceFile) ?? classNode.getStart(sourceFile)
|
|
1743
|
+
),
|
|
1744
|
+
message: 'static html property must be defined'
|
|
1745
|
+
});
|
|
1568
1746
|
}
|
|
1569
1747
|
|
|
1570
1748
|
const augmentedSource = buildAugmentedSource(
|
|
@@ -1606,7 +1784,12 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1606
1784
|
) {
|
|
1607
1785
|
uniquePush(
|
|
1608
1786
|
findings.invalidEventHandlers,
|
|
1609
|
-
|
|
1787
|
+
expr.location
|
|
1788
|
+
? {
|
|
1789
|
+
location: expr.location,
|
|
1790
|
+
message: `"${expr.text}" is not a defined instance method`
|
|
1791
|
+
}
|
|
1792
|
+
: `"${expr.text}" is not a defined instance method`
|
|
1610
1793
|
);
|
|
1611
1794
|
}
|
|
1612
1795
|
});
|
|
@@ -1618,11 +1801,12 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1618
1801
|
contextKeys: new Set(contextKeys),
|
|
1619
1802
|
checkContextCalls: allCodeItems[index]?.checkContextCalls ?? true,
|
|
1620
1803
|
eventHandler: allCodeItems[index]?.eventHandler ?? false,
|
|
1804
|
+
location: allCodeItems[index]?.location ?? null,
|
|
1621
1805
|
sourceFile: augmentedSourceFile
|
|
1622
1806
|
});
|
|
1623
1807
|
});
|
|
1624
1808
|
|
|
1625
|
-
findings.duplicateProperties.sort();
|
|
1809
|
+
findings.duplicateProperties.sort(compareLocatedFindings);
|
|
1626
1810
|
findings.extraArguments.sort(
|
|
1627
1811
|
(a, b) =>
|
|
1628
1812
|
a.methodName.localeCompare(b.methodName) ||
|
|
@@ -1633,28 +1817,28 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1633
1817
|
a.methodName.localeCompare(b.methodName) ||
|
|
1634
1818
|
a.parameterName.localeCompare(b.parameterName)
|
|
1635
1819
|
);
|
|
1636
|
-
findings.incompatibleDeclareTypes.sort();
|
|
1637
|
-
findings.invalidCheckedBindings.sort();
|
|
1638
|
-
findings.invalidComputedProperties.sort();
|
|
1639
|
-
findings.invalidDefaultValues.sort();
|
|
1640
|
-
findings.invalidEventHandlers.sort();
|
|
1641
|
-
findings.invalidFormAssocValues.sort();
|
|
1642
|
-
findings.invalidHtmlNesting.sort();
|
|
1643
|
-
findings.invalidRefAttributes.sort();
|
|
1644
|
-
findings.invalidTypeProperties.sort();
|
|
1645
|
-
findings.invalidUsedByReferences.sort();
|
|
1646
|
-
findings.invalidUseStateMaps.sort();
|
|
1647
|
-
findings.invalidValueBindings.sort();
|
|
1648
|
-
findings.invalidValuesConfigurations.sort();
|
|
1649
|
-
findings.missingRequiredMembers.sort();
|
|
1650
|
-
findings.missingTypeProperties.sort();
|
|
1651
|
-
findings.reservedProperties.sort();
|
|
1820
|
+
findings.incompatibleDeclareTypes.sort(compareLocatedFindings);
|
|
1821
|
+
findings.invalidCheckedBindings.sort(compareLocatedFindings);
|
|
1822
|
+
findings.invalidComputedProperties.sort(compareLocatedFindings);
|
|
1823
|
+
findings.invalidDefaultValues.sort(compareLocatedFindings);
|
|
1824
|
+
findings.invalidEventHandlers.sort(compareLocatedFindings);
|
|
1825
|
+
findings.invalidFormAssocValues.sort(compareLocatedFindings);
|
|
1826
|
+
findings.invalidHtmlNesting.sort(compareLocatedFindings);
|
|
1827
|
+
findings.invalidRefAttributes.sort(compareLocatedFindings);
|
|
1828
|
+
findings.invalidTypeProperties.sort(compareLocatedFindings);
|
|
1829
|
+
findings.invalidUsedByReferences.sort(compareLocatedFindings);
|
|
1830
|
+
findings.invalidUseStateMaps.sort(compareLocatedFindings);
|
|
1831
|
+
findings.invalidValueBindings.sort(compareLocatedFindings);
|
|
1832
|
+
findings.invalidValuesConfigurations.sort(compareLocatedFindings);
|
|
1833
|
+
findings.missingRequiredMembers.sort(compareLocatedFindings);
|
|
1834
|
+
findings.missingTypeProperties.sort(compareLocatedFindings);
|
|
1835
|
+
findings.reservedProperties.sort(compareLocatedFindings);
|
|
1652
1836
|
findings.typeErrors.sort((a, b) => a.expression.localeCompare(b.expression));
|
|
1653
|
-
findings.undefinedContextFunctions.sort();
|
|
1654
|
-
findings.undefinedMethods.sort();
|
|
1655
|
-
findings.undefinedProperties.sort();
|
|
1656
|
-
findings.unsupportedEventNames.sort();
|
|
1657
|
-
findings.unsupportedHtmlAttributes.sort();
|
|
1837
|
+
findings.undefinedContextFunctions.sort(compareLocatedFindings);
|
|
1838
|
+
findings.undefinedMethods.sort(compareLocatedFindings);
|
|
1839
|
+
findings.undefinedProperties.sort(compareLocatedFindings);
|
|
1840
|
+
findings.unsupportedEventNames.sort(compareLocatedFindings);
|
|
1841
|
+
findings.unsupportedHtmlAttributes.sort(compareLocatedFindings);
|
|
1658
1842
|
|
|
1659
1843
|
return formatReport(
|
|
1660
1844
|
filePath,
|
|
@@ -1831,7 +2015,12 @@ function typeNodeFromConstructorExpression(expression) {
|
|
|
1831
2015
|
|
|
1832
2016
|
// Pushes a value into an array only if it is not already present.
|
|
1833
2017
|
function uniquePush(array, value) {
|
|
1834
|
-
|
|
2018
|
+
const valueText = getLocatedFindingMessage(value);
|
|
2019
|
+
if (
|
|
2020
|
+
!array.some(existing => getLocatedFindingMessage(existing) === valueText)
|
|
2021
|
+
) {
|
|
2022
|
+
array.push(value);
|
|
2023
|
+
}
|
|
1835
2024
|
}
|
|
1836
2025
|
|
|
1837
2026
|
// Validates checked bindings for checkbox and radio input elements.
|
|
@@ -1840,7 +2029,8 @@ function validateCheckedBinding(
|
|
|
1840
2029
|
attrName,
|
|
1841
2030
|
attrValue,
|
|
1842
2031
|
findings,
|
|
1843
|
-
supportedProps
|
|
2032
|
+
supportedProps,
|
|
2033
|
+
resolveLocation
|
|
1844
2034
|
) {
|
|
1845
2035
|
if (getHtmlTagName(node) !== 'input') return;
|
|
1846
2036
|
|
|
@@ -1862,8 +2052,12 @@ function validateCheckedBinding(
|
|
|
1862
2052
|
const expectedTypeName = getPropertyConfigTypeName(expectedType);
|
|
1863
2053
|
|
|
1864
2054
|
findings.invalidCheckedBindings.push(
|
|
1865
|
-
|
|
1866
|
-
|
|
2055
|
+
{
|
|
2056
|
+
location: getHtmlAttributeLocation(node, attrName, resolveLocation),
|
|
2057
|
+
message:
|
|
2058
|
+
`input type="${inputType}" attribute "${attrName}" refers to ` +
|
|
2059
|
+
`property "${propName}" whose type is not ${expectedTypeName}`
|
|
2060
|
+
}
|
|
1867
2061
|
);
|
|
1868
2062
|
}
|
|
1869
2063
|
|
|
@@ -1873,7 +2067,8 @@ function validateValueBinding(
|
|
|
1873
2067
|
attrName,
|
|
1874
2068
|
attrValue,
|
|
1875
2069
|
findings,
|
|
1876
|
-
supportedProps
|
|
2070
|
+
supportedProps,
|
|
2071
|
+
resolveLocation
|
|
1877
2072
|
) {
|
|
1878
2073
|
const [baseAttrName] = attrName.split(':');
|
|
1879
2074
|
if (baseAttrName !== 'value') return;
|
|
@@ -1890,8 +2085,12 @@ function validateValueBinding(
|
|
|
1890
2085
|
}
|
|
1891
2086
|
|
|
1892
2087
|
findings.invalidValueBindings.push(
|
|
1893
|
-
|
|
1894
|
-
|
|
2088
|
+
{
|
|
2089
|
+
location: getHtmlAttributeLocation(node, attrName, resolveLocation),
|
|
2090
|
+
message:
|
|
2091
|
+
`${getHtmlTagName(node)} attribute "${attrName}" refers to property ` +
|
|
2092
|
+
`"${propName}" whose type is not String or Number`
|
|
2093
|
+
}
|
|
1895
2094
|
);
|
|
1896
2095
|
}
|
|
1897
2096
|
|
|
@@ -1901,7 +2100,8 @@ function validateComputedProperty(
|
|
|
1901
2100
|
computedText,
|
|
1902
2101
|
supportedProps,
|
|
1903
2102
|
classMethods,
|
|
1904
|
-
findings
|
|
2103
|
+
findings,
|
|
2104
|
+
location
|
|
1905
2105
|
) {
|
|
1906
2106
|
for (const match of computedText.matchAll(THIS_REF_RE)) {
|
|
1907
2107
|
const referencedName = match[1];
|
|
@@ -1910,8 +2110,12 @@ function validateComputedProperty(
|
|
|
1910
2110
|
!classMethods.has(referencedName)
|
|
1911
2111
|
) {
|
|
1912
2112
|
findings.invalidComputedProperties.push(
|
|
1913
|
-
|
|
1914
|
-
|
|
2113
|
+
{
|
|
2114
|
+
location,
|
|
2115
|
+
message:
|
|
2116
|
+
`property "${propName}" computed references ` +
|
|
2117
|
+
`missing property "${referencedName}"`
|
|
2118
|
+
}
|
|
1915
2119
|
);
|
|
1916
2120
|
}
|
|
1917
2121
|
}
|
|
@@ -1920,15 +2124,23 @@ function validateComputedProperty(
|
|
|
1920
2124
|
const methodName = match[1];
|
|
1921
2125
|
if (!classMethods.has(methodName)) {
|
|
1922
2126
|
findings.invalidComputedProperties.push(
|
|
1923
|
-
|
|
1924
|
-
|
|
2127
|
+
{
|
|
2128
|
+
location,
|
|
2129
|
+
message:
|
|
2130
|
+
`property "${propName}" computed calls ` +
|
|
2131
|
+
`non-method instance member "${methodName}"`
|
|
2132
|
+
}
|
|
1925
2133
|
);
|
|
1926
2134
|
}
|
|
1927
2135
|
}
|
|
1928
2136
|
}
|
|
1929
2137
|
|
|
1930
2138
|
// Validates that computed properties do not form dependency cycles.
|
|
1931
|
-
function validateComputedPropertyCycles(
|
|
2139
|
+
function validateComputedPropertyCycles(
|
|
2140
|
+
computedDependencies,
|
|
2141
|
+
computedLocations,
|
|
2142
|
+
findings
|
|
2143
|
+
) {
|
|
1932
2144
|
const computedNames = [...computedDependencies.keys()].sort();
|
|
1933
2145
|
const dependencyCountMap = new Map();
|
|
1934
2146
|
const dependentsMap = new Map();
|
|
@@ -1967,8 +2179,17 @@ function validateComputedPropertyCycles(computedDependencies, findings) {
|
|
|
1967
2179
|
const cycleNames = computedNames.filter(
|
|
1968
2180
|
computedName => dependencyCountMap.get(computedName) > 0
|
|
1969
2181
|
);
|
|
2182
|
+
const cycleLocation = cycleNames
|
|
2183
|
+
.map(name => computedLocations.get(name))
|
|
2184
|
+
.filter(Boolean)
|
|
2185
|
+
.sort(
|
|
2186
|
+
(a, b) => a.line - b.line || a.character - b.character
|
|
2187
|
+
)[0];
|
|
1970
2188
|
findings.invalidComputedProperties.push(
|
|
1971
|
-
|
|
2189
|
+
{
|
|
2190
|
+
location: cycleLocation ?? null,
|
|
2191
|
+
message: `computed properties form a cycle: ${cycleNames.join(', ')}`
|
|
2192
|
+
}
|
|
1972
2193
|
);
|
|
1973
2194
|
}
|
|
1974
2195
|
|
|
@@ -2044,7 +2265,13 @@ function validateFilePath(filePath) {
|
|
|
2044
2265
|
}
|
|
2045
2266
|
|
|
2046
2267
|
// Validates the syntax of a form-assoc attribute value.
|
|
2047
|
-
function validateFormAssocAttribute(
|
|
2268
|
+
function validateFormAssocAttribute(
|
|
2269
|
+
node,
|
|
2270
|
+
attrName,
|
|
2271
|
+
attrValue,
|
|
2272
|
+
findings,
|
|
2273
|
+
resolveLocation
|
|
2274
|
+
) {
|
|
2048
2275
|
if (attrName !== 'form-assoc') return;
|
|
2049
2276
|
|
|
2050
2277
|
const pairs = attrValue.split(',');
|
|
@@ -2055,8 +2282,12 @@ function validateFormAssocAttribute(attrName, attrValue, findings) {
|
|
|
2055
2282
|
.map(part => part.trim());
|
|
2056
2283
|
if (!trimmed || rest.length > 0 || !propName || !fieldName) {
|
|
2057
2284
|
findings.invalidFormAssocValues.push(
|
|
2058
|
-
|
|
2059
|
-
|
|
2285
|
+
{
|
|
2286
|
+
location: getHtmlAttributeLocation(node, attrName, resolveLocation),
|
|
2287
|
+
message:
|
|
2288
|
+
`form-assoc="${attrValue}" is invalid; expected ` +
|
|
2289
|
+
'"property:field" or a comma-separated list of them'
|
|
2290
|
+
}
|
|
2060
2291
|
);
|
|
2061
2292
|
return;
|
|
2062
2293
|
}
|
|
@@ -2069,7 +2300,8 @@ function validateFormAssocPropertyMappings(
|
|
|
2069
2300
|
attrName,
|
|
2070
2301
|
attrValue,
|
|
2071
2302
|
findings,
|
|
2072
|
-
componentPropertyMaps
|
|
2303
|
+
componentPropertyMaps,
|
|
2304
|
+
resolveLocation
|
|
2073
2305
|
) {
|
|
2074
2306
|
if (attrName !== 'form-assoc') return;
|
|
2075
2307
|
const tagName = (node.rawTagName || node.tagName || '').toLowerCase();
|
|
@@ -2082,15 +2314,19 @@ function validateFormAssocPropertyMappings(
|
|
|
2082
2314
|
if (!propName) continue;
|
|
2083
2315
|
if (!supportedProps.has(propName)) {
|
|
2084
2316
|
findings.invalidFormAssocValues.push(
|
|
2085
|
-
|
|
2086
|
-
|
|
2317
|
+
{
|
|
2318
|
+
location: getHtmlAttributeLocation(node, attrName, resolveLocation),
|
|
2319
|
+
message:
|
|
2320
|
+
`form-assoc="${attrValue}" refers to ` +
|
|
2321
|
+
`missing component property "${propName}"`
|
|
2322
|
+
}
|
|
2087
2323
|
);
|
|
2088
2324
|
}
|
|
2089
2325
|
}
|
|
2090
2326
|
}
|
|
2091
2327
|
|
|
2092
2328
|
// Validates that an HTML attribute is supported for the current element.
|
|
2093
|
-
function validateHtmlAttribute(node, attrName, findings) {
|
|
2329
|
+
function validateHtmlAttribute(node, attrName, findings, resolveLocation) {
|
|
2094
2330
|
if (attrName.startsWith('aria-') || attrName.startsWith('data-')) return;
|
|
2095
2331
|
if (attrName.startsWith('on')) return;
|
|
2096
2332
|
if (attrName === 'form-assoc') return;
|
|
@@ -2107,12 +2343,15 @@ function validateHtmlAttribute(node, attrName, findings) {
|
|
|
2107
2343
|
if (supported.has(baseAttrName)) return;
|
|
2108
2344
|
|
|
2109
2345
|
findings.unsupportedHtmlAttributes.push(
|
|
2110
|
-
|
|
2346
|
+
{
|
|
2347
|
+
location: getHtmlAttributeLocation(node, attrName, resolveLocation),
|
|
2348
|
+
message: `${tagName} attribute "${attrName}" is not supported`
|
|
2349
|
+
}
|
|
2111
2350
|
);
|
|
2112
2351
|
}
|
|
2113
2352
|
|
|
2114
2353
|
// Validates required parent-child relationships for supported HTML tags.
|
|
2115
|
-
function validateHtmlNesting(node, findings) {
|
|
2354
|
+
function validateHtmlNesting(node, findings, resolveLocation) {
|
|
2116
2355
|
const tagName = getHtmlTagName(node);
|
|
2117
2356
|
if (!tagName || tagName.includes('-')) return;
|
|
2118
2357
|
|
|
@@ -2127,9 +2366,13 @@ function validateHtmlNesting(node, findings) {
|
|
|
2127
2366
|
? `<${parentTagName}>`
|
|
2128
2367
|
: 'the document root';
|
|
2129
2368
|
findings.invalidHtmlNesting.push(
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2369
|
+
{
|
|
2370
|
+
location: getHtmlNodeLocation(node, resolveLocation),
|
|
2371
|
+
message:
|
|
2372
|
+
`<${tagName}> must be nested inside ${[...allowedParents]
|
|
2373
|
+
.map(name => `<${name}>`)
|
|
2374
|
+
.join(' or ')}, but parent is ${parentDescription}`
|
|
2375
|
+
}
|
|
2133
2376
|
);
|
|
2134
2377
|
}
|
|
2135
2378
|
|
|
@@ -2143,7 +2386,10 @@ function validateHtmlNesting(node, findings) {
|
|
|
2143
2386
|
if (allowedChildren.has(childTagName)) continue;
|
|
2144
2387
|
|
|
2145
2388
|
findings.invalidHtmlNesting.push(
|
|
2146
|
-
|
|
2389
|
+
{
|
|
2390
|
+
location: getHtmlNodeLocation(child, resolveLocation),
|
|
2391
|
+
message: `<${childTagName}> is not allowed directly inside <${tagName}>`
|
|
2392
|
+
}
|
|
2147
2393
|
);
|
|
2148
2394
|
}
|
|
2149
2395
|
}
|
|
@@ -2160,6 +2406,7 @@ function validatePropertyConfigs(
|
|
|
2160
2406
|
findings
|
|
2161
2407
|
) {
|
|
2162
2408
|
const computedDependencies = new Map();
|
|
2409
|
+
const computedLocations = new Map();
|
|
2163
2410
|
const computedPropNames = new Set();
|
|
2164
2411
|
|
|
2165
2412
|
for (const {config, propName} of propertyEntries) {
|
|
@@ -2170,11 +2417,17 @@ function validatePropertyConfigs(
|
|
|
2170
2417
|
(ts.isStringLiteral(computedProp.initializer) ||
|
|
2171
2418
|
ts.isNoSubstitutionTemplateLiteral(computedProp.initializer))
|
|
2172
2419
|
) {
|
|
2420
|
+
computedLocations.set(
|
|
2421
|
+
propName,
|
|
2422
|
+
sourceFile.getLineAndCharacterOfPosition(
|
|
2423
|
+
computedProp.initializer.getStart(sourceFile)
|
|
2424
|
+
)
|
|
2425
|
+
);
|
|
2173
2426
|
computedPropNames.add(propName);
|
|
2174
2427
|
}
|
|
2175
2428
|
}
|
|
2176
2429
|
|
|
2177
|
-
for (const {config, propName} of propertyEntries) {
|
|
2430
|
+
for (const {config, propName, property} of propertyEntries) {
|
|
2178
2431
|
const computedProp = getObjectProperty(config, 'computed');
|
|
2179
2432
|
const declaredTypeNode = declaredPropertyTypes.get(propName);
|
|
2180
2433
|
const typeProp = getObjectProperty(config, 'type');
|
|
@@ -2182,6 +2435,9 @@ function validatePropertyConfigs(
|
|
|
2182
2435
|
const valueProp = getObjectProperty(config, 'value');
|
|
2183
2436
|
const valuesProp = getObjectProperty(config, 'values');
|
|
2184
2437
|
const valuesConfigError = getValuesConfigurationError(valuesProp);
|
|
2438
|
+
const propertyLocation = sourceFile.getLineAndCharacterOfPosition(
|
|
2439
|
+
property.name.getStart(sourceFile)
|
|
2440
|
+
);
|
|
2185
2441
|
|
|
2186
2442
|
const typeExpression =
|
|
2187
2443
|
typeProp && ts.isPropertyAssignment(typeProp)
|
|
@@ -2190,7 +2446,10 @@ function validatePropertyConfigs(
|
|
|
2190
2446
|
|
|
2191
2447
|
if (!typeExpression) {
|
|
2192
2448
|
findings.missingTypeProperties.push(
|
|
2193
|
-
|
|
2449
|
+
{
|
|
2450
|
+
location: propertyLocation,
|
|
2451
|
+
message: `property "${propName}" does not specify a type`
|
|
2452
|
+
}
|
|
2194
2453
|
);
|
|
2195
2454
|
} else if (
|
|
2196
2455
|
SUPPORTED_PROPERTY_TYPE_NAMES.has(
|
|
@@ -2198,16 +2457,24 @@ function validatePropertyConfigs(
|
|
|
2198
2457
|
)
|
|
2199
2458
|
) {
|
|
2200
2459
|
findings.invalidTypeProperties.push(
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2460
|
+
{
|
|
2461
|
+
location: propertyLocation,
|
|
2462
|
+
message:
|
|
2463
|
+
`property "${propName}" type cannot use generic syntax like ` +
|
|
2464
|
+
`"${typeExpression.getText(sourceFile).trim()}"; use ` +
|
|
2465
|
+
`"${getPropertyTypeGenericBaseName(sourceFile, typeExpression)}" instead`
|
|
2466
|
+
}
|
|
2204
2467
|
);
|
|
2205
2468
|
} else if (
|
|
2206
2469
|
!SUPPORTED_PROPERTY_TYPE_NAMES.has(typeExpressionKind(typeExpression))
|
|
2207
2470
|
) {
|
|
2208
2471
|
findings.invalidTypeProperties.push(
|
|
2209
|
-
|
|
2210
|
-
|
|
2472
|
+
{
|
|
2473
|
+
location: propertyLocation,
|
|
2474
|
+
message:
|
|
2475
|
+
`property "${propName}" type must be one of ` +
|
|
2476
|
+
'Boolean, Number, String, Object, Array, or HTMLElement'
|
|
2477
|
+
}
|
|
2211
2478
|
);
|
|
2212
2479
|
} else if (declaredTypeNode) {
|
|
2213
2480
|
if (
|
|
@@ -2218,10 +2485,14 @@ function validatePropertyConfigs(
|
|
|
2218
2485
|
)
|
|
2219
2486
|
) {
|
|
2220
2487
|
findings.incompatibleDeclareTypes.push(
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2488
|
+
{
|
|
2489
|
+
location: propertyLocation,
|
|
2490
|
+
message:
|
|
2491
|
+
`property "${propName}" declare type ` +
|
|
2492
|
+
`"${getPropertyTypeTextFromNode(sourceFile, declaredTypeNode)}" ` +
|
|
2493
|
+
`is not compatible with static properties type ` +
|
|
2494
|
+
`"${getPropertyConfigTypeName(typeExpressionKind(typeExpression))}"`
|
|
2495
|
+
}
|
|
2225
2496
|
);
|
|
2226
2497
|
}
|
|
2227
2498
|
}
|
|
@@ -2235,16 +2506,24 @@ function validatePropertyConfigs(
|
|
|
2235
2506
|
const getterName = getGetterName(methodName);
|
|
2236
2507
|
if (getterNames.has(getterName)) continue;
|
|
2237
2508
|
findings.invalidUsedByReferences.push(
|
|
2238
|
-
|
|
2239
|
-
|
|
2509
|
+
{
|
|
2510
|
+
location: propertyLocation,
|
|
2511
|
+
message:
|
|
2512
|
+
`property "${propName}" usedBy references ` +
|
|
2513
|
+
`missing getter "${getterName}"`
|
|
2514
|
+
}
|
|
2240
2515
|
);
|
|
2241
2516
|
continue;
|
|
2242
2517
|
}
|
|
2243
2518
|
|
|
2244
2519
|
if (!classMethods.has(methodName)) {
|
|
2245
2520
|
findings.invalidUsedByReferences.push(
|
|
2246
|
-
|
|
2247
|
-
|
|
2521
|
+
{
|
|
2522
|
+
location: propertyLocation,
|
|
2523
|
+
message:
|
|
2524
|
+
`property "${propName}" usedBy references ` +
|
|
2525
|
+
`missing method "${methodName}"`
|
|
2526
|
+
}
|
|
2248
2527
|
);
|
|
2249
2528
|
}
|
|
2250
2529
|
}
|
|
@@ -2269,13 +2548,17 @@ function validatePropertyConfigs(
|
|
|
2269
2548
|
computedProp.initializer.text,
|
|
2270
2549
|
supportedProps,
|
|
2271
2550
|
classMethods,
|
|
2272
|
-
findings
|
|
2551
|
+
findings,
|
|
2552
|
+
computedLocations.get(propName) ?? propertyLocation
|
|
2273
2553
|
);
|
|
2274
2554
|
}
|
|
2275
2555
|
|
|
2276
2556
|
if (valuesConfigError) {
|
|
2277
2557
|
findings.invalidValuesConfigurations.push(
|
|
2278
|
-
|
|
2558
|
+
{
|
|
2559
|
+
location: propertyLocation,
|
|
2560
|
+
message: `property "${propName}" ${valuesConfigError}`
|
|
2561
|
+
}
|
|
2279
2562
|
);
|
|
2280
2563
|
}
|
|
2281
2564
|
|
|
@@ -2283,7 +2566,10 @@ function validatePropertyConfigs(
|
|
|
2283
2566
|
if (values) {
|
|
2284
2567
|
if (typeExpressionKind(typeExpression) !== 'String') {
|
|
2285
2568
|
findings.invalidValuesConfigurations.push(
|
|
2286
|
-
|
|
2569
|
+
{
|
|
2570
|
+
location: propertyLocation,
|
|
2571
|
+
message: `property "${propName}" uses values, but its type is not String`
|
|
2572
|
+
}
|
|
2287
2573
|
);
|
|
2288
2574
|
}
|
|
2289
2575
|
|
|
@@ -2295,8 +2581,12 @@ function validatePropertyConfigs(
|
|
|
2295
2581
|
!values.includes(valueProp.initializer.text)
|
|
2296
2582
|
) {
|
|
2297
2583
|
findings.invalidDefaultValues.push(
|
|
2298
|
-
|
|
2299
|
-
|
|
2584
|
+
{
|
|
2585
|
+
location: propertyLocation,
|
|
2586
|
+
message:
|
|
2587
|
+
`property "${propName}" default value ` +
|
|
2588
|
+
`"${valueProp.initializer.text}" is not in values`
|
|
2589
|
+
}
|
|
2300
2590
|
);
|
|
2301
2591
|
}
|
|
2302
2592
|
}
|
|
@@ -2309,15 +2599,23 @@ function validatePropertyConfigs(
|
|
|
2309
2599
|
);
|
|
2310
2600
|
if (mismatch) {
|
|
2311
2601
|
findings.invalidDefaultValues.push(
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2602
|
+
{
|
|
2603
|
+
location: propertyLocation,
|
|
2604
|
+
message:
|
|
2605
|
+
`property "${propName}" default value ` +
|
|
2606
|
+
`has type ${mismatch.valueTypeName}, ` +
|
|
2607
|
+
`but declared type is ${mismatch.typeName}`
|
|
2608
|
+
}
|
|
2315
2609
|
);
|
|
2316
2610
|
}
|
|
2317
2611
|
}
|
|
2318
2612
|
}
|
|
2319
2613
|
|
|
2320
|
-
validateComputedPropertyCycles(
|
|
2614
|
+
validateComputedPropertyCycles(
|
|
2615
|
+
computedDependencies,
|
|
2616
|
+
computedLocations,
|
|
2617
|
+
findings
|
|
2618
|
+
);
|
|
2321
2619
|
}
|
|
2322
2620
|
|
|
2323
2621
|
// Validates that a ref attribute targets a unique HTMLElement property.
|
|
@@ -2325,7 +2623,8 @@ function validateRefAttribute(
|
|
|
2325
2623
|
attrValue,
|
|
2326
2624
|
supportedProps,
|
|
2327
2625
|
findings,
|
|
2328
|
-
seenRefProps
|
|
2626
|
+
seenRefProps,
|
|
2627
|
+
location
|
|
2329
2628
|
) {
|
|
2330
2629
|
if (!attrValue) return;
|
|
2331
2630
|
|
|
@@ -2335,23 +2634,34 @@ function validateRefAttribute(
|
|
|
2335
2634
|
const propInfo = supportedProps.get(propName);
|
|
2336
2635
|
if (!propInfo) {
|
|
2337
2636
|
findings.invalidRefAttributes.push(
|
|
2338
|
-
|
|
2637
|
+
{
|
|
2638
|
+
location,
|
|
2639
|
+
message: `ref="${attrValue}" refers to missing property "${propName}"`
|
|
2640
|
+
}
|
|
2339
2641
|
);
|
|
2340
2642
|
return;
|
|
2341
2643
|
}
|
|
2342
2644
|
|
|
2343
2645
|
if (propInfo.typeText !== 'HTMLElement') {
|
|
2344
2646
|
findings.invalidRefAttributes.push(
|
|
2345
|
-
|
|
2346
|
-
|
|
2647
|
+
{
|
|
2648
|
+
location,
|
|
2649
|
+
message:
|
|
2650
|
+
`ref="${attrValue}" refers to property "${propName}" ` +
|
|
2651
|
+
'whose type is not HTMLElement'
|
|
2652
|
+
}
|
|
2347
2653
|
);
|
|
2348
2654
|
return;
|
|
2349
2655
|
}
|
|
2350
2656
|
|
|
2351
2657
|
if (seenRefProps.has(propName)) {
|
|
2352
2658
|
findings.invalidRefAttributes.push(
|
|
2353
|
-
|
|
2354
|
-
|
|
2659
|
+
{
|
|
2660
|
+
location,
|
|
2661
|
+
message:
|
|
2662
|
+
`ref="${attrValue}" is a duplicate reference ` +
|
|
2663
|
+
`to the property "${propName}"`
|
|
2664
|
+
}
|
|
2355
2665
|
);
|
|
2356
2666
|
return;
|
|
2357
2667
|
}
|
|
@@ -2360,15 +2670,19 @@ function validateRefAttribute(
|
|
|
2360
2670
|
}
|
|
2361
2671
|
|
|
2362
2672
|
// Validates event names used in value-binding attributes.
|
|
2363
|
-
function validateValueBindingEvent(node, attrName, findings) {
|
|
2673
|
+
function validateValueBindingEvent(node, attrName, findings, resolveLocation) {
|
|
2364
2674
|
const [realAttrName, eventName] = attrName.split(':');
|
|
2365
2675
|
if (realAttrName !== 'value' || !eventName) return;
|
|
2366
2676
|
if (SUPPORTED_EVENT_NAMES.has(eventName)) return;
|
|
2367
2677
|
|
|
2368
2678
|
const tagName = node.rawTagName || node.tagName || 'element';
|
|
2369
2679
|
findings.unsupportedEventNames.push(
|
|
2370
|
-
|
|
2371
|
-
|
|
2680
|
+
{
|
|
2681
|
+
location: getHtmlAttributeLocation(node, attrName, resolveLocation),
|
|
2682
|
+
message:
|
|
2683
|
+
`${tagName} attribute "${attrName}" refers to ` +
|
|
2684
|
+
`an unsupported event name "${eventName}"`
|
|
2685
|
+
}
|
|
2372
2686
|
);
|
|
2373
2687
|
}
|
|
2374
2688
|
|
|
@@ -2379,33 +2693,55 @@ function walkHtmlNode(
|
|
|
2379
2693
|
findings,
|
|
2380
2694
|
componentPropertyMaps,
|
|
2381
2695
|
supportedProps,
|
|
2382
|
-
seenRefProps
|
|
2696
|
+
seenRefProps,
|
|
2697
|
+
resolveLocation
|
|
2383
2698
|
) {
|
|
2384
2699
|
if (node.nodeType === 1) {
|
|
2385
|
-
validateHtmlNesting(node, findings);
|
|
2700
|
+
validateHtmlNesting(node, findings, resolveLocation);
|
|
2386
2701
|
|
|
2387
2702
|
for (const [attrName, attrValue] of Object.entries(node.attributes)) {
|
|
2388
2703
|
if (!attrValue) continue;
|
|
2389
|
-
validateFormAssocAttribute(
|
|
2704
|
+
validateFormAssocAttribute(
|
|
2705
|
+
node,
|
|
2706
|
+
attrName,
|
|
2707
|
+
attrValue,
|
|
2708
|
+
findings,
|
|
2709
|
+
resolveLocation
|
|
2710
|
+
);
|
|
2390
2711
|
validateFormAssocPropertyMappings(
|
|
2391
2712
|
node,
|
|
2392
2713
|
attrName,
|
|
2393
2714
|
attrValue,
|
|
2394
2715
|
findings,
|
|
2395
|
-
componentPropertyMaps
|
|
2716
|
+
componentPropertyMaps,
|
|
2717
|
+
resolveLocation
|
|
2396
2718
|
);
|
|
2397
2719
|
validateCheckedBinding(
|
|
2398
2720
|
node,
|
|
2399
2721
|
attrName,
|
|
2400
2722
|
attrValue,
|
|
2401
2723
|
findings,
|
|
2402
|
-
supportedProps
|
|
2724
|
+
supportedProps,
|
|
2725
|
+
resolveLocation
|
|
2726
|
+
);
|
|
2727
|
+
validateHtmlAttribute(node, attrName, findings, resolveLocation);
|
|
2728
|
+
validateValueBinding(
|
|
2729
|
+
node,
|
|
2730
|
+
attrName,
|
|
2731
|
+
attrValue,
|
|
2732
|
+
findings,
|
|
2733
|
+
supportedProps,
|
|
2734
|
+
resolveLocation
|
|
2403
2735
|
);
|
|
2404
|
-
|
|
2405
|
-
validateValueBinding(node, attrName, attrValue, findings, supportedProps);
|
|
2406
|
-
validateValueBindingEvent(node, attrName, findings);
|
|
2736
|
+
validateValueBindingEvent(node, attrName, findings, resolveLocation);
|
|
2407
2737
|
if (attrName === 'ref') {
|
|
2408
|
-
validateRefAttribute(
|
|
2738
|
+
validateRefAttribute(
|
|
2739
|
+
attrValue,
|
|
2740
|
+
supportedProps,
|
|
2741
|
+
findings,
|
|
2742
|
+
seenRefProps,
|
|
2743
|
+
getHtmlAttributeLocation(node, attrName, resolveLocation)
|
|
2744
|
+
);
|
|
2409
2745
|
}
|
|
2410
2746
|
if (
|
|
2411
2747
|
REFS_TEST_RE.test(attrValue) ||
|
|
@@ -2416,7 +2752,7 @@ function walkHtmlNode(
|
|
|
2416
2752
|
eventHandler: attrName.startsWith('on'),
|
|
2417
2753
|
kind: 'html',
|
|
2418
2754
|
text: attrValue.trim(),
|
|
2419
|
-
location:
|
|
2755
|
+
location: getHtmlAttributeLocation(node, attrName, resolveLocation)
|
|
2420
2756
|
});
|
|
2421
2757
|
}
|
|
2422
2758
|
}
|
|
@@ -2433,7 +2769,7 @@ function walkHtmlNode(
|
|
|
2433
2769
|
eventHandler: false,
|
|
2434
2770
|
kind: 'html',
|
|
2435
2771
|
text,
|
|
2436
|
-
location:
|
|
2772
|
+
location: getHtmlNodeLocation(node, resolveLocation)
|
|
2437
2773
|
});
|
|
2438
2774
|
}
|
|
2439
2775
|
}
|
|
@@ -2445,7 +2781,8 @@ function walkHtmlNode(
|
|
|
2445
2781
|
findings,
|
|
2446
2782
|
componentPropertyMaps,
|
|
2447
2783
|
supportedProps,
|
|
2448
|
-
seenRefProps
|
|
2784
|
+
seenRefProps,
|
|
2785
|
+
resolveLocation
|
|
2449
2786
|
);
|
|
2450
2787
|
}
|
|
2451
2788
|
}
|