wrec 0.40.0 → 0.40.2
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 +497 -154
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,20 +791,30 @@ 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;
|
|
744
801
|
while (true) {
|
|
745
802
|
const match = CSS_PROPERTY_RE.exec(rendered);
|
|
746
803
|
if (!match) break;
|
|
747
|
-
const
|
|
804
|
+
const rawValue = match[2] ?? '';
|
|
805
|
+
const value = rawValue.trim();
|
|
748
806
|
if (value && REFS_TEST_RE.test(value)) {
|
|
807
|
+
const valueOffsetInMatch = match[0].lastIndexOf(rawValue);
|
|
808
|
+
const leadingWhitespace = rawValue.length - rawValue.trimStart().length;
|
|
809
|
+
const valueLocation = resolveLocation(
|
|
810
|
+
match.index + valueOffsetInMatch + leadingWhitespace
|
|
811
|
+
);
|
|
749
812
|
expressions.push({
|
|
750
813
|
context: 'instance',
|
|
751
814
|
eventHandler: false,
|
|
752
815
|
kind: 'css',
|
|
753
816
|
text: value,
|
|
754
|
-
location:
|
|
817
|
+
location: valueLocation
|
|
755
818
|
});
|
|
756
819
|
}
|
|
757
820
|
}
|
|
@@ -765,7 +828,8 @@ function extractTemplateExpressions(
|
|
|
765
828
|
findings,
|
|
766
829
|
componentPropertyMaps,
|
|
767
830
|
supportedProps,
|
|
768
|
-
new Set()
|
|
831
|
+
new Set(),
|
|
832
|
+
resolveLocation
|
|
769
833
|
);
|
|
770
834
|
}
|
|
771
835
|
|
|
@@ -826,12 +890,91 @@ function findWrecFiles(rootDir, onMatch) {
|
|
|
826
890
|
walk(rootDir);
|
|
827
891
|
}
|
|
828
892
|
|
|
893
|
+
// Converts an offset within rendered template text to a source location.
|
|
894
|
+
function createTemplateLocationResolver(sourceFile, template) {
|
|
895
|
+
const segments = [];
|
|
896
|
+
|
|
897
|
+
if (ts.isNoSubstitutionTemplateLiteral(template)) {
|
|
898
|
+
segments.push({
|
|
899
|
+
renderedEnd: template.text.length,
|
|
900
|
+
renderedStart: 0,
|
|
901
|
+
sourceStart: template.getStart(sourceFile) + 1
|
|
902
|
+
});
|
|
903
|
+
} else {
|
|
904
|
+
let renderedStart = 0;
|
|
905
|
+
const headText = template.head.text;
|
|
906
|
+
segments.push({
|
|
907
|
+
renderedEnd: headText.length,
|
|
908
|
+
renderedStart,
|
|
909
|
+
sourceStart: template.head.getStart(sourceFile) + 1
|
|
910
|
+
});
|
|
911
|
+
renderedStart += headText.length;
|
|
912
|
+
|
|
913
|
+
template.templateSpans.forEach((span, index) => {
|
|
914
|
+
renderedStart += `${PLACEHOLDER_PREFIX}${index}`.length;
|
|
915
|
+
segments.push({
|
|
916
|
+
renderedEnd: renderedStart + span.literal.text.length,
|
|
917
|
+
renderedStart,
|
|
918
|
+
sourceStart: span.literal.getStart(sourceFile) + 1
|
|
919
|
+
});
|
|
920
|
+
renderedStart += span.literal.text.length;
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return offset => {
|
|
925
|
+
const segment =
|
|
926
|
+
segments.find(
|
|
927
|
+
candidate =>
|
|
928
|
+
offset >= candidate.renderedStart && offset <= candidate.renderedEnd
|
|
929
|
+
) ?? segments[segments.length - 1];
|
|
930
|
+
if (!segment) return null;
|
|
931
|
+
|
|
932
|
+
const sourceOffset =
|
|
933
|
+
segment.sourceStart + Math.max(0, offset - segment.renderedStart);
|
|
934
|
+
return sourceFile.getLineAndCharacterOfPosition(sourceOffset);
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
829
938
|
// Formats an optional source location as line and column text.
|
|
830
939
|
function formatLocation(location) {
|
|
831
940
|
if (!location) return '';
|
|
832
941
|
return `:${location.line + 1}:${location.character + 1}`;
|
|
833
942
|
}
|
|
834
943
|
|
|
944
|
+
// Gets the source location for the start of a parsed HTML node.
|
|
945
|
+
function getHtmlNodeLocation(node, resolveLocation) {
|
|
946
|
+
const [start] = node.range ?? [];
|
|
947
|
+
return Number.isInteger(start) && start >= 0 ? resolveLocation(start) : null;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Gets the source location for a specific HTML attribute within a parsed node.
|
|
951
|
+
function getHtmlAttributeLocation(node, attrName, resolveLocation) {
|
|
952
|
+
const [start] = node.range ?? [];
|
|
953
|
+
const tagName = getHtmlTagName(node);
|
|
954
|
+
if (!Number.isInteger(start) || start < 0 || !tagName) return null;
|
|
955
|
+
|
|
956
|
+
const attrsOffset = node.rawAttrs.indexOf(attrName);
|
|
957
|
+
if (attrsOffset < 0) return getHtmlNodeLocation(node, resolveLocation);
|
|
958
|
+
|
|
959
|
+
return resolveLocation(start + 1 + tagName.length + 1 + attrsOffset);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Formats a lint finding that may optionally include a source location.
|
|
963
|
+
function formatMaybeLocatedFinding(finding) {
|
|
964
|
+
if (typeof finding === 'string') return finding;
|
|
965
|
+
return `${formatLocation(finding.location)} ${finding.message}`.trim();
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Gets the message text from a lint finding that may include a location.
|
|
969
|
+
function getLocatedFindingMessage(finding) {
|
|
970
|
+
return typeof finding === 'string' ? finding : finding.message;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Compares lint findings that may optionally include source locations.
|
|
974
|
+
function compareLocatedFindings(a, b) {
|
|
975
|
+
return getLocatedFindingMessage(a).localeCompare(getLocatedFindingMessage(b));
|
|
976
|
+
}
|
|
977
|
+
|
|
835
978
|
// Formats the collected lint findings into the command-line report output.
|
|
836
979
|
function formatReport(
|
|
837
980
|
filePath,
|
|
@@ -904,14 +1047,19 @@ function formatReport(
|
|
|
904
1047
|
|
|
905
1048
|
if (findings.duplicateProperties.length > 0) {
|
|
906
1049
|
lines.push('duplicate properties:');
|
|
907
|
-
findings.duplicateProperties.forEach(
|
|
1050
|
+
findings.duplicateProperties.forEach(finding =>
|
|
1051
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1052
|
+
);
|
|
908
1053
|
}
|
|
909
1054
|
|
|
910
1055
|
if (findings.extraArguments.length > 0) {
|
|
911
1056
|
lines.push('extra arguments:');
|
|
912
1057
|
findings.extraArguments.forEach(finding => {
|
|
1058
|
+
const locationPrefix = finding.location
|
|
1059
|
+
? `${formatLocation(finding.location)} `
|
|
1060
|
+
: '';
|
|
913
1061
|
lines.push(
|
|
914
|
-
` ${finding.methodName}: argument ${finding.argumentIndex} ` +
|
|
1062
|
+
` ${locationPrefix}${finding.methodName}: argument ${finding.argumentIndex} ` +
|
|
915
1063
|
`"${finding.argument}" exceeds the ` +
|
|
916
1064
|
`${finding.parameterCount}-parameter signature`
|
|
917
1065
|
);
|
|
@@ -921,8 +1069,11 @@ function formatReport(
|
|
|
921
1069
|
if (findings.incompatibleArguments.length > 0) {
|
|
922
1070
|
lines.push('incompatible arguments:');
|
|
923
1071
|
findings.incompatibleArguments.forEach(finding => {
|
|
1072
|
+
const locationPrefix = finding.location
|
|
1073
|
+
? `${formatLocation(finding.location)} `
|
|
1074
|
+
: '';
|
|
924
1075
|
lines.push(
|
|
925
|
-
` ${finding.methodName}: argument "${finding.argument}" ` +
|
|
1076
|
+
` ${locationPrefix}${finding.methodName}: argument "${finding.argument}" ` +
|
|
926
1077
|
`has type ${finding.argumentType}, but parameter ` +
|
|
927
1078
|
`"${finding.parameterName}" expects ${finding.parameterType}`
|
|
928
1079
|
);
|
|
@@ -931,143 +1082,158 @@ function formatReport(
|
|
|
931
1082
|
|
|
932
1083
|
if (findings.incompatibleDeclareTypes.length > 0) {
|
|
933
1084
|
lines.push('incompatible declare types:');
|
|
934
|
-
findings.incompatibleDeclareTypes.forEach(
|
|
935
|
-
lines.push(` ${
|
|
1085
|
+
findings.incompatibleDeclareTypes.forEach(finding =>
|
|
1086
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
936
1087
|
);
|
|
937
1088
|
}
|
|
938
1089
|
|
|
939
1090
|
if (findings.invalidCheckedBindings.length > 0) {
|
|
940
1091
|
lines.push('invalid checked bindings:');
|
|
941
|
-
findings.invalidCheckedBindings.forEach(
|
|
942
|
-
lines.push(` ${
|
|
1092
|
+
findings.invalidCheckedBindings.forEach(finding =>
|
|
1093
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
943
1094
|
);
|
|
944
1095
|
}
|
|
945
1096
|
|
|
946
1097
|
if (findings.invalidComputedProperties.length > 0) {
|
|
947
1098
|
lines.push('invalid computed properties:');
|
|
948
|
-
findings.invalidComputedProperties.forEach(
|
|
949
|
-
lines.push(` ${
|
|
1099
|
+
findings.invalidComputedProperties.forEach(finding =>
|
|
1100
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
950
1101
|
);
|
|
951
1102
|
}
|
|
952
1103
|
|
|
953
1104
|
if (findings.invalidDefaultValues.length > 0) {
|
|
954
1105
|
lines.push('invalid default values:');
|
|
955
|
-
findings.invalidDefaultValues.forEach(
|
|
956
|
-
lines.push(` ${
|
|
1106
|
+
findings.invalidDefaultValues.forEach(finding =>
|
|
1107
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
957
1108
|
);
|
|
958
1109
|
}
|
|
959
1110
|
|
|
960
1111
|
if (findings.invalidEventHandlers.length > 0) {
|
|
961
1112
|
lines.push('invalid event handler references:');
|
|
962
|
-
findings.invalidEventHandlers.forEach(
|
|
963
|
-
lines.push(` ${
|
|
1113
|
+
findings.invalidEventHandlers.forEach(finding =>
|
|
1114
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
964
1115
|
);
|
|
965
1116
|
}
|
|
966
1117
|
|
|
967
1118
|
if (findings.invalidFormAssocValues.length > 0) {
|
|
968
1119
|
lines.push('invalid form-assoc values:');
|
|
969
|
-
findings.invalidFormAssocValues.forEach(
|
|
970
|
-
lines.push(` ${
|
|
1120
|
+
findings.invalidFormAssocValues.forEach(finding =>
|
|
1121
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
971
1122
|
);
|
|
972
1123
|
}
|
|
973
1124
|
|
|
974
1125
|
if (findings.invalidHtmlNesting.length > 0) {
|
|
975
1126
|
lines.push('invalid html nesting:');
|
|
976
|
-
findings.invalidHtmlNesting.forEach(
|
|
1127
|
+
findings.invalidHtmlNesting.forEach(finding =>
|
|
1128
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1129
|
+
);
|
|
977
1130
|
}
|
|
978
1131
|
|
|
979
1132
|
if (findings.invalidRefAttributes.length > 0) {
|
|
980
1133
|
lines.push('invalid ref attributes:');
|
|
981
|
-
findings.invalidRefAttributes.forEach(
|
|
982
|
-
lines.push(` ${
|
|
1134
|
+
findings.invalidRefAttributes.forEach(finding =>
|
|
1135
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
983
1136
|
);
|
|
984
1137
|
}
|
|
985
1138
|
|
|
986
1139
|
if (findings.invalidTypeProperties.length > 0) {
|
|
987
1140
|
lines.push('invalid type properties:');
|
|
988
|
-
findings.invalidTypeProperties.forEach(
|
|
989
|
-
lines.push(` ${
|
|
1141
|
+
findings.invalidTypeProperties.forEach(finding =>
|
|
1142
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
990
1143
|
);
|
|
991
1144
|
}
|
|
992
1145
|
|
|
993
1146
|
if (findings.invalidUsedByReferences.length > 0) {
|
|
994
1147
|
lines.push('invalid usedBy references:');
|
|
995
|
-
findings.invalidUsedByReferences.forEach(
|
|
996
|
-
lines.push(` ${
|
|
1148
|
+
findings.invalidUsedByReferences.forEach(finding =>
|
|
1149
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
997
1150
|
);
|
|
998
1151
|
}
|
|
999
1152
|
|
|
1000
1153
|
if (findings.invalidUseStateMaps.length > 0) {
|
|
1001
1154
|
lines.push('invalid useState map entries:');
|
|
1002
|
-
findings.invalidUseStateMaps.forEach(
|
|
1155
|
+
findings.invalidUseStateMaps.forEach(finding =>
|
|
1156
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1157
|
+
);
|
|
1003
1158
|
}
|
|
1004
1159
|
|
|
1005
1160
|
if (findings.invalidValueBindings.length > 0) {
|
|
1006
1161
|
lines.push('invalid value bindings:');
|
|
1007
|
-
findings.invalidValueBindings.forEach(
|
|
1008
|
-
lines.push(` ${
|
|
1162
|
+
findings.invalidValueBindings.forEach(finding =>
|
|
1163
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1009
1164
|
);
|
|
1010
1165
|
}
|
|
1011
1166
|
|
|
1012
1167
|
if (findings.invalidValuesConfigurations.length > 0) {
|
|
1013
1168
|
lines.push('invalid values configurations:');
|
|
1014
|
-
findings.invalidValuesConfigurations.forEach(
|
|
1015
|
-
lines.push(` ${
|
|
1169
|
+
findings.invalidValuesConfigurations.forEach(finding =>
|
|
1170
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1016
1171
|
);
|
|
1017
1172
|
}
|
|
1018
1173
|
|
|
1019
1174
|
if (findings.missingRequiredMembers.length > 0) {
|
|
1020
1175
|
lines.push('missing required members:');
|
|
1021
|
-
findings.missingRequiredMembers.forEach(
|
|
1022
|
-
lines.push(` ${
|
|
1176
|
+
findings.missingRequiredMembers.forEach(finding =>
|
|
1177
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1023
1178
|
);
|
|
1024
1179
|
}
|
|
1025
1180
|
|
|
1026
1181
|
if (findings.missingTypeProperties.length > 0) {
|
|
1027
1182
|
lines.push('missing type properties:');
|
|
1028
|
-
findings.missingTypeProperties.forEach(
|
|
1029
|
-
lines.push(` ${
|
|
1183
|
+
findings.missingTypeProperties.forEach(finding =>
|
|
1184
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1030
1185
|
);
|
|
1031
1186
|
}
|
|
1032
1187
|
|
|
1033
1188
|
if (findings.reservedProperties.length > 0) {
|
|
1034
1189
|
lines.push('reserved property names:');
|
|
1035
|
-
findings.reservedProperties.forEach(
|
|
1190
|
+
findings.reservedProperties.forEach(finding =>
|
|
1191
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1192
|
+
);
|
|
1036
1193
|
}
|
|
1037
1194
|
|
|
1038
1195
|
if (findings.typeErrors.length > 0) {
|
|
1039
1196
|
lines.push('type errors:');
|
|
1040
1197
|
findings.typeErrors.forEach(finding => {
|
|
1041
|
-
|
|
1198
|
+
const locationPrefix = finding.location
|
|
1199
|
+
? `${formatLocation(finding.location)} `
|
|
1200
|
+
: '';
|
|
1201
|
+
lines.push(` ${locationPrefix}${finding.expression}: ${finding.message}`);
|
|
1042
1202
|
});
|
|
1043
1203
|
}
|
|
1044
1204
|
|
|
1045
1205
|
if (findings.undefinedContextFunctions.length > 0) {
|
|
1046
1206
|
lines.push('undefined context functions:');
|
|
1047
|
-
findings.undefinedContextFunctions.forEach(
|
|
1207
|
+
findings.undefinedContextFunctions.forEach(finding =>
|
|
1208
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1209
|
+
);
|
|
1048
1210
|
}
|
|
1049
1211
|
|
|
1050
1212
|
if (findings.undefinedMethods.length > 0) {
|
|
1051
1213
|
lines.push('undefined methods:');
|
|
1052
|
-
findings.undefinedMethods.forEach(
|
|
1214
|
+
findings.undefinedMethods.forEach(finding =>
|
|
1215
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1216
|
+
);
|
|
1053
1217
|
}
|
|
1054
1218
|
|
|
1055
1219
|
if (findings.undefinedProperties.length > 0) {
|
|
1056
1220
|
lines.push('undefined properties:');
|
|
1057
|
-
findings.undefinedProperties.forEach(
|
|
1221
|
+
findings.undefinedProperties.forEach(finding =>
|
|
1222
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1223
|
+
);
|
|
1058
1224
|
}
|
|
1059
1225
|
|
|
1060
1226
|
if (findings.unsupportedEventNames.length > 0) {
|
|
1061
1227
|
lines.push('unsupported event names:');
|
|
1062
|
-
findings.unsupportedEventNames.forEach(
|
|
1063
|
-
lines.push(` ${
|
|
1228
|
+
findings.unsupportedEventNames.forEach(finding =>
|
|
1229
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1064
1230
|
);
|
|
1065
1231
|
}
|
|
1066
1232
|
|
|
1067
1233
|
if (findings.unsupportedHtmlAttributes.length > 0) {
|
|
1068
1234
|
lines.push('unsupported html attributes:');
|
|
1069
|
-
findings.unsupportedHtmlAttributes.forEach(
|
|
1070
|
-
lines.push(` ${
|
|
1235
|
+
findings.unsupportedHtmlAttributes.forEach(finding =>
|
|
1236
|
+
lines.push(` ${formatMaybeLocatedFinding(finding)}`)
|
|
1071
1237
|
);
|
|
1072
1238
|
}
|
|
1073
1239
|
|
|
@@ -1557,14 +1723,32 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1557
1723
|
}));
|
|
1558
1724
|
|
|
1559
1725
|
if (allMethods.has('formAssociatedCallback') && !formAssociated) {
|
|
1726
|
+
const callbackMember = classNode.members.find(
|
|
1727
|
+
member =>
|
|
1728
|
+
ts.isMethodDeclaration(member) &&
|
|
1729
|
+
getMemberName(member) === 'formAssociatedCallback'
|
|
1730
|
+
);
|
|
1560
1731
|
findings.missingRequiredMembers.push(
|
|
1561
|
-
|
|
1732
|
+
{
|
|
1733
|
+
location: callbackMember
|
|
1734
|
+
? callbackMember
|
|
1735
|
+
.getSourceFile()
|
|
1736
|
+
.getLineAndCharacterOfPosition(
|
|
1737
|
+
callbackMember.name.getStart(callbackMember.getSourceFile())
|
|
1738
|
+
)
|
|
1739
|
+
: null,
|
|
1740
|
+
message:
|
|
1741
|
+
'formAssociatedCallback is defined, but static formAssociated is not true'
|
|
1742
|
+
}
|
|
1562
1743
|
);
|
|
1563
1744
|
}
|
|
1564
1745
|
if (!hasStaticHtmlDefinition(classNode)) {
|
|
1565
|
-
findings.missingRequiredMembers.push(
|
|
1566
|
-
|
|
1567
|
-
|
|
1746
|
+
findings.missingRequiredMembers.push({
|
|
1747
|
+
location: sourceFile.getLineAndCharacterOfPosition(
|
|
1748
|
+
classNode.name?.getStart(sourceFile) ?? classNode.getStart(sourceFile)
|
|
1749
|
+
),
|
|
1750
|
+
message: 'static html property must be defined'
|
|
1751
|
+
});
|
|
1568
1752
|
}
|
|
1569
1753
|
|
|
1570
1754
|
const augmentedSource = buildAugmentedSource(
|
|
@@ -1606,7 +1790,12 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1606
1790
|
) {
|
|
1607
1791
|
uniquePush(
|
|
1608
1792
|
findings.invalidEventHandlers,
|
|
1609
|
-
|
|
1793
|
+
expr.location
|
|
1794
|
+
? {
|
|
1795
|
+
location: expr.location,
|
|
1796
|
+
message: `"${expr.text}" is not a defined instance method`
|
|
1797
|
+
}
|
|
1798
|
+
: `"${expr.text}" is not a defined instance method`
|
|
1610
1799
|
);
|
|
1611
1800
|
}
|
|
1612
1801
|
});
|
|
@@ -1618,11 +1807,12 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1618
1807
|
contextKeys: new Set(contextKeys),
|
|
1619
1808
|
checkContextCalls: allCodeItems[index]?.checkContextCalls ?? true,
|
|
1620
1809
|
eventHandler: allCodeItems[index]?.eventHandler ?? false,
|
|
1810
|
+
location: allCodeItems[index]?.location ?? null,
|
|
1621
1811
|
sourceFile: augmentedSourceFile
|
|
1622
1812
|
});
|
|
1623
1813
|
});
|
|
1624
1814
|
|
|
1625
|
-
findings.duplicateProperties.sort();
|
|
1815
|
+
findings.duplicateProperties.sort(compareLocatedFindings);
|
|
1626
1816
|
findings.extraArguments.sort(
|
|
1627
1817
|
(a, b) =>
|
|
1628
1818
|
a.methodName.localeCompare(b.methodName) ||
|
|
@@ -1633,28 +1823,28 @@ export function lintSource(filePath, sourceText, options = {}) {
|
|
|
1633
1823
|
a.methodName.localeCompare(b.methodName) ||
|
|
1634
1824
|
a.parameterName.localeCompare(b.parameterName)
|
|
1635
1825
|
);
|
|
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();
|
|
1826
|
+
findings.incompatibleDeclareTypes.sort(compareLocatedFindings);
|
|
1827
|
+
findings.invalidCheckedBindings.sort(compareLocatedFindings);
|
|
1828
|
+
findings.invalidComputedProperties.sort(compareLocatedFindings);
|
|
1829
|
+
findings.invalidDefaultValues.sort(compareLocatedFindings);
|
|
1830
|
+
findings.invalidEventHandlers.sort(compareLocatedFindings);
|
|
1831
|
+
findings.invalidFormAssocValues.sort(compareLocatedFindings);
|
|
1832
|
+
findings.invalidHtmlNesting.sort(compareLocatedFindings);
|
|
1833
|
+
findings.invalidRefAttributes.sort(compareLocatedFindings);
|
|
1834
|
+
findings.invalidTypeProperties.sort(compareLocatedFindings);
|
|
1835
|
+
findings.invalidUsedByReferences.sort(compareLocatedFindings);
|
|
1836
|
+
findings.invalidUseStateMaps.sort(compareLocatedFindings);
|
|
1837
|
+
findings.invalidValueBindings.sort(compareLocatedFindings);
|
|
1838
|
+
findings.invalidValuesConfigurations.sort(compareLocatedFindings);
|
|
1839
|
+
findings.missingRequiredMembers.sort(compareLocatedFindings);
|
|
1840
|
+
findings.missingTypeProperties.sort(compareLocatedFindings);
|
|
1841
|
+
findings.reservedProperties.sort(compareLocatedFindings);
|
|
1652
1842
|
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();
|
|
1843
|
+
findings.undefinedContextFunctions.sort(compareLocatedFindings);
|
|
1844
|
+
findings.undefinedMethods.sort(compareLocatedFindings);
|
|
1845
|
+
findings.undefinedProperties.sort(compareLocatedFindings);
|
|
1846
|
+
findings.unsupportedEventNames.sort(compareLocatedFindings);
|
|
1847
|
+
findings.unsupportedHtmlAttributes.sort(compareLocatedFindings);
|
|
1658
1848
|
|
|
1659
1849
|
return formatReport(
|
|
1660
1850
|
filePath,
|
|
@@ -1831,7 +2021,12 @@ function typeNodeFromConstructorExpression(expression) {
|
|
|
1831
2021
|
|
|
1832
2022
|
// Pushes a value into an array only if it is not already present.
|
|
1833
2023
|
function uniquePush(array, value) {
|
|
1834
|
-
|
|
2024
|
+
const valueText = getLocatedFindingMessage(value);
|
|
2025
|
+
if (
|
|
2026
|
+
!array.some(existing => getLocatedFindingMessage(existing) === valueText)
|
|
2027
|
+
) {
|
|
2028
|
+
array.push(value);
|
|
2029
|
+
}
|
|
1835
2030
|
}
|
|
1836
2031
|
|
|
1837
2032
|
// Validates checked bindings for checkbox and radio input elements.
|
|
@@ -1840,7 +2035,8 @@ function validateCheckedBinding(
|
|
|
1840
2035
|
attrName,
|
|
1841
2036
|
attrValue,
|
|
1842
2037
|
findings,
|
|
1843
|
-
supportedProps
|
|
2038
|
+
supportedProps,
|
|
2039
|
+
resolveLocation
|
|
1844
2040
|
) {
|
|
1845
2041
|
if (getHtmlTagName(node) !== 'input') return;
|
|
1846
2042
|
|
|
@@ -1862,8 +2058,12 @@ function validateCheckedBinding(
|
|
|
1862
2058
|
const expectedTypeName = getPropertyConfigTypeName(expectedType);
|
|
1863
2059
|
|
|
1864
2060
|
findings.invalidCheckedBindings.push(
|
|
1865
|
-
|
|
1866
|
-
|
|
2061
|
+
{
|
|
2062
|
+
location: getHtmlAttributeLocation(node, attrName, resolveLocation),
|
|
2063
|
+
message:
|
|
2064
|
+
`input type="${inputType}" attribute "${attrName}" refers to ` +
|
|
2065
|
+
`property "${propName}" whose type is not ${expectedTypeName}`
|
|
2066
|
+
}
|
|
1867
2067
|
);
|
|
1868
2068
|
}
|
|
1869
2069
|
|
|
@@ -1873,7 +2073,8 @@ function validateValueBinding(
|
|
|
1873
2073
|
attrName,
|
|
1874
2074
|
attrValue,
|
|
1875
2075
|
findings,
|
|
1876
|
-
supportedProps
|
|
2076
|
+
supportedProps,
|
|
2077
|
+
resolveLocation
|
|
1877
2078
|
) {
|
|
1878
2079
|
const [baseAttrName] = attrName.split(':');
|
|
1879
2080
|
if (baseAttrName !== 'value') return;
|
|
@@ -1890,8 +2091,12 @@ function validateValueBinding(
|
|
|
1890
2091
|
}
|
|
1891
2092
|
|
|
1892
2093
|
findings.invalidValueBindings.push(
|
|
1893
|
-
|
|
1894
|
-
|
|
2094
|
+
{
|
|
2095
|
+
location: getHtmlAttributeLocation(node, attrName, resolveLocation),
|
|
2096
|
+
message:
|
|
2097
|
+
`${getHtmlTagName(node)} attribute "${attrName}" refers to property ` +
|
|
2098
|
+
`"${propName}" whose type is not String or Number`
|
|
2099
|
+
}
|
|
1895
2100
|
);
|
|
1896
2101
|
}
|
|
1897
2102
|
|
|
@@ -1901,7 +2106,8 @@ function validateComputedProperty(
|
|
|
1901
2106
|
computedText,
|
|
1902
2107
|
supportedProps,
|
|
1903
2108
|
classMethods,
|
|
1904
|
-
findings
|
|
2109
|
+
findings,
|
|
2110
|
+
location
|
|
1905
2111
|
) {
|
|
1906
2112
|
for (const match of computedText.matchAll(THIS_REF_RE)) {
|
|
1907
2113
|
const referencedName = match[1];
|
|
@@ -1910,8 +2116,12 @@ function validateComputedProperty(
|
|
|
1910
2116
|
!classMethods.has(referencedName)
|
|
1911
2117
|
) {
|
|
1912
2118
|
findings.invalidComputedProperties.push(
|
|
1913
|
-
|
|
1914
|
-
|
|
2119
|
+
{
|
|
2120
|
+
location,
|
|
2121
|
+
message:
|
|
2122
|
+
`property "${propName}" computed references ` +
|
|
2123
|
+
`missing property "${referencedName}"`
|
|
2124
|
+
}
|
|
1915
2125
|
);
|
|
1916
2126
|
}
|
|
1917
2127
|
}
|
|
@@ -1920,15 +2130,23 @@ function validateComputedProperty(
|
|
|
1920
2130
|
const methodName = match[1];
|
|
1921
2131
|
if (!classMethods.has(methodName)) {
|
|
1922
2132
|
findings.invalidComputedProperties.push(
|
|
1923
|
-
|
|
1924
|
-
|
|
2133
|
+
{
|
|
2134
|
+
location,
|
|
2135
|
+
message:
|
|
2136
|
+
`property "${propName}" computed calls ` +
|
|
2137
|
+
`non-method instance member "${methodName}"`
|
|
2138
|
+
}
|
|
1925
2139
|
);
|
|
1926
2140
|
}
|
|
1927
2141
|
}
|
|
1928
2142
|
}
|
|
1929
2143
|
|
|
1930
2144
|
// Validates that computed properties do not form dependency cycles.
|
|
1931
|
-
function validateComputedPropertyCycles(
|
|
2145
|
+
function validateComputedPropertyCycles(
|
|
2146
|
+
computedDependencies,
|
|
2147
|
+
computedLocations,
|
|
2148
|
+
findings
|
|
2149
|
+
) {
|
|
1932
2150
|
const computedNames = [...computedDependencies.keys()].sort();
|
|
1933
2151
|
const dependencyCountMap = new Map();
|
|
1934
2152
|
const dependentsMap = new Map();
|
|
@@ -1967,8 +2185,17 @@ function validateComputedPropertyCycles(computedDependencies, findings) {
|
|
|
1967
2185
|
const cycleNames = computedNames.filter(
|
|
1968
2186
|
computedName => dependencyCountMap.get(computedName) > 0
|
|
1969
2187
|
);
|
|
2188
|
+
const cycleLocation = cycleNames
|
|
2189
|
+
.map(name => computedLocations.get(name))
|
|
2190
|
+
.filter(Boolean)
|
|
2191
|
+
.sort(
|
|
2192
|
+
(a, b) => a.line - b.line || a.character - b.character
|
|
2193
|
+
)[0];
|
|
1970
2194
|
findings.invalidComputedProperties.push(
|
|
1971
|
-
|
|
2195
|
+
{
|
|
2196
|
+
location: cycleLocation ?? null,
|
|
2197
|
+
message: `computed properties form a cycle: ${cycleNames.join(', ')}`
|
|
2198
|
+
}
|
|
1972
2199
|
);
|
|
1973
2200
|
}
|
|
1974
2201
|
|
|
@@ -2044,7 +2271,13 @@ function validateFilePath(filePath) {
|
|
|
2044
2271
|
}
|
|
2045
2272
|
|
|
2046
2273
|
// Validates the syntax of a form-assoc attribute value.
|
|
2047
|
-
function validateFormAssocAttribute(
|
|
2274
|
+
function validateFormAssocAttribute(
|
|
2275
|
+
node,
|
|
2276
|
+
attrName,
|
|
2277
|
+
attrValue,
|
|
2278
|
+
findings,
|
|
2279
|
+
resolveLocation
|
|
2280
|
+
) {
|
|
2048
2281
|
if (attrName !== 'form-assoc') return;
|
|
2049
2282
|
|
|
2050
2283
|
const pairs = attrValue.split(',');
|
|
@@ -2055,8 +2288,12 @@ function validateFormAssocAttribute(attrName, attrValue, findings) {
|
|
|
2055
2288
|
.map(part => part.trim());
|
|
2056
2289
|
if (!trimmed || rest.length > 0 || !propName || !fieldName) {
|
|
2057
2290
|
findings.invalidFormAssocValues.push(
|
|
2058
|
-
|
|
2059
|
-
|
|
2291
|
+
{
|
|
2292
|
+
location: getHtmlAttributeLocation(node, attrName, resolveLocation),
|
|
2293
|
+
message:
|
|
2294
|
+
`form-assoc="${attrValue}" is invalid; expected ` +
|
|
2295
|
+
'"property:field" or a comma-separated list of them'
|
|
2296
|
+
}
|
|
2060
2297
|
);
|
|
2061
2298
|
return;
|
|
2062
2299
|
}
|
|
@@ -2069,7 +2306,8 @@ function validateFormAssocPropertyMappings(
|
|
|
2069
2306
|
attrName,
|
|
2070
2307
|
attrValue,
|
|
2071
2308
|
findings,
|
|
2072
|
-
componentPropertyMaps
|
|
2309
|
+
componentPropertyMaps,
|
|
2310
|
+
resolveLocation
|
|
2073
2311
|
) {
|
|
2074
2312
|
if (attrName !== 'form-assoc') return;
|
|
2075
2313
|
const tagName = (node.rawTagName || node.tagName || '').toLowerCase();
|
|
@@ -2082,15 +2320,19 @@ function validateFormAssocPropertyMappings(
|
|
|
2082
2320
|
if (!propName) continue;
|
|
2083
2321
|
if (!supportedProps.has(propName)) {
|
|
2084
2322
|
findings.invalidFormAssocValues.push(
|
|
2085
|
-
|
|
2086
|
-
|
|
2323
|
+
{
|
|
2324
|
+
location: getHtmlAttributeLocation(node, attrName, resolveLocation),
|
|
2325
|
+
message:
|
|
2326
|
+
`form-assoc="${attrValue}" refers to ` +
|
|
2327
|
+
`missing component property "${propName}"`
|
|
2328
|
+
}
|
|
2087
2329
|
);
|
|
2088
2330
|
}
|
|
2089
2331
|
}
|
|
2090
2332
|
}
|
|
2091
2333
|
|
|
2092
2334
|
// Validates that an HTML attribute is supported for the current element.
|
|
2093
|
-
function validateHtmlAttribute(node, attrName, findings) {
|
|
2335
|
+
function validateHtmlAttribute(node, attrName, findings, resolveLocation) {
|
|
2094
2336
|
if (attrName.startsWith('aria-') || attrName.startsWith('data-')) return;
|
|
2095
2337
|
if (attrName.startsWith('on')) return;
|
|
2096
2338
|
if (attrName === 'form-assoc') return;
|
|
@@ -2107,12 +2349,15 @@ function validateHtmlAttribute(node, attrName, findings) {
|
|
|
2107
2349
|
if (supported.has(baseAttrName)) return;
|
|
2108
2350
|
|
|
2109
2351
|
findings.unsupportedHtmlAttributes.push(
|
|
2110
|
-
|
|
2352
|
+
{
|
|
2353
|
+
location: getHtmlAttributeLocation(node, attrName, resolveLocation),
|
|
2354
|
+
message: `${tagName} attribute "${attrName}" is not supported`
|
|
2355
|
+
}
|
|
2111
2356
|
);
|
|
2112
2357
|
}
|
|
2113
2358
|
|
|
2114
2359
|
// Validates required parent-child relationships for supported HTML tags.
|
|
2115
|
-
function validateHtmlNesting(node, findings) {
|
|
2360
|
+
function validateHtmlNesting(node, findings, resolveLocation) {
|
|
2116
2361
|
const tagName = getHtmlTagName(node);
|
|
2117
2362
|
if (!tagName || tagName.includes('-')) return;
|
|
2118
2363
|
|
|
@@ -2127,9 +2372,13 @@ function validateHtmlNesting(node, findings) {
|
|
|
2127
2372
|
? `<${parentTagName}>`
|
|
2128
2373
|
: 'the document root';
|
|
2129
2374
|
findings.invalidHtmlNesting.push(
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
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
|
+
}
|
|
2133
2382
|
);
|
|
2134
2383
|
}
|
|
2135
2384
|
|
|
@@ -2143,7 +2392,10 @@ function validateHtmlNesting(node, findings) {
|
|
|
2143
2392
|
if (allowedChildren.has(childTagName)) continue;
|
|
2144
2393
|
|
|
2145
2394
|
findings.invalidHtmlNesting.push(
|
|
2146
|
-
|
|
2395
|
+
{
|
|
2396
|
+
location: getHtmlNodeLocation(child, resolveLocation),
|
|
2397
|
+
message: `<${childTagName}> is not allowed directly inside <${tagName}>`
|
|
2398
|
+
}
|
|
2147
2399
|
);
|
|
2148
2400
|
}
|
|
2149
2401
|
}
|
|
@@ -2160,6 +2412,7 @@ function validatePropertyConfigs(
|
|
|
2160
2412
|
findings
|
|
2161
2413
|
) {
|
|
2162
2414
|
const computedDependencies = new Map();
|
|
2415
|
+
const computedLocations = new Map();
|
|
2163
2416
|
const computedPropNames = new Set();
|
|
2164
2417
|
|
|
2165
2418
|
for (const {config, propName} of propertyEntries) {
|
|
@@ -2170,11 +2423,17 @@ function validatePropertyConfigs(
|
|
|
2170
2423
|
(ts.isStringLiteral(computedProp.initializer) ||
|
|
2171
2424
|
ts.isNoSubstitutionTemplateLiteral(computedProp.initializer))
|
|
2172
2425
|
) {
|
|
2426
|
+
computedLocations.set(
|
|
2427
|
+
propName,
|
|
2428
|
+
sourceFile.getLineAndCharacterOfPosition(
|
|
2429
|
+
computedProp.initializer.getStart(sourceFile)
|
|
2430
|
+
)
|
|
2431
|
+
);
|
|
2173
2432
|
computedPropNames.add(propName);
|
|
2174
2433
|
}
|
|
2175
2434
|
}
|
|
2176
2435
|
|
|
2177
|
-
for (const {config, propName} of propertyEntries) {
|
|
2436
|
+
for (const {config, propName, property} of propertyEntries) {
|
|
2178
2437
|
const computedProp = getObjectProperty(config, 'computed');
|
|
2179
2438
|
const declaredTypeNode = declaredPropertyTypes.get(propName);
|
|
2180
2439
|
const typeProp = getObjectProperty(config, 'type');
|
|
@@ -2182,6 +2441,9 @@ function validatePropertyConfigs(
|
|
|
2182
2441
|
const valueProp = getObjectProperty(config, 'value');
|
|
2183
2442
|
const valuesProp = getObjectProperty(config, 'values');
|
|
2184
2443
|
const valuesConfigError = getValuesConfigurationError(valuesProp);
|
|
2444
|
+
const propertyLocation = sourceFile.getLineAndCharacterOfPosition(
|
|
2445
|
+
property.name.getStart(sourceFile)
|
|
2446
|
+
);
|
|
2185
2447
|
|
|
2186
2448
|
const typeExpression =
|
|
2187
2449
|
typeProp && ts.isPropertyAssignment(typeProp)
|
|
@@ -2190,7 +2452,10 @@ function validatePropertyConfigs(
|
|
|
2190
2452
|
|
|
2191
2453
|
if (!typeExpression) {
|
|
2192
2454
|
findings.missingTypeProperties.push(
|
|
2193
|
-
|
|
2455
|
+
{
|
|
2456
|
+
location: propertyLocation,
|
|
2457
|
+
message: `property "${propName}" does not specify a type`
|
|
2458
|
+
}
|
|
2194
2459
|
);
|
|
2195
2460
|
} else if (
|
|
2196
2461
|
SUPPORTED_PROPERTY_TYPE_NAMES.has(
|
|
@@ -2198,16 +2463,24 @@ function validatePropertyConfigs(
|
|
|
2198
2463
|
)
|
|
2199
2464
|
) {
|
|
2200
2465
|
findings.invalidTypeProperties.push(
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
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
|
+
}
|
|
2204
2473
|
);
|
|
2205
2474
|
} else if (
|
|
2206
2475
|
!SUPPORTED_PROPERTY_TYPE_NAMES.has(typeExpressionKind(typeExpression))
|
|
2207
2476
|
) {
|
|
2208
2477
|
findings.invalidTypeProperties.push(
|
|
2209
|
-
|
|
2210
|
-
|
|
2478
|
+
{
|
|
2479
|
+
location: propertyLocation,
|
|
2480
|
+
message:
|
|
2481
|
+
`property "${propName}" type must be one of ` +
|
|
2482
|
+
'Boolean, Number, String, Object, Array, or HTMLElement'
|
|
2483
|
+
}
|
|
2211
2484
|
);
|
|
2212
2485
|
} else if (declaredTypeNode) {
|
|
2213
2486
|
if (
|
|
@@ -2218,10 +2491,14 @@ function validatePropertyConfigs(
|
|
|
2218
2491
|
)
|
|
2219
2492
|
) {
|
|
2220
2493
|
findings.incompatibleDeclareTypes.push(
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
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
|
+
}
|
|
2225
2502
|
);
|
|
2226
2503
|
}
|
|
2227
2504
|
}
|
|
@@ -2235,16 +2512,24 @@ function validatePropertyConfigs(
|
|
|
2235
2512
|
const getterName = getGetterName(methodName);
|
|
2236
2513
|
if (getterNames.has(getterName)) continue;
|
|
2237
2514
|
findings.invalidUsedByReferences.push(
|
|
2238
|
-
|
|
2239
|
-
|
|
2515
|
+
{
|
|
2516
|
+
location: propertyLocation,
|
|
2517
|
+
message:
|
|
2518
|
+
`property "${propName}" usedBy references ` +
|
|
2519
|
+
`missing getter "${getterName}"`
|
|
2520
|
+
}
|
|
2240
2521
|
);
|
|
2241
2522
|
continue;
|
|
2242
2523
|
}
|
|
2243
2524
|
|
|
2244
2525
|
if (!classMethods.has(methodName)) {
|
|
2245
2526
|
findings.invalidUsedByReferences.push(
|
|
2246
|
-
|
|
2247
|
-
|
|
2527
|
+
{
|
|
2528
|
+
location: propertyLocation,
|
|
2529
|
+
message:
|
|
2530
|
+
`property "${propName}" usedBy references ` +
|
|
2531
|
+
`missing method "${methodName}"`
|
|
2532
|
+
}
|
|
2248
2533
|
);
|
|
2249
2534
|
}
|
|
2250
2535
|
}
|
|
@@ -2269,13 +2554,17 @@ function validatePropertyConfigs(
|
|
|
2269
2554
|
computedProp.initializer.text,
|
|
2270
2555
|
supportedProps,
|
|
2271
2556
|
classMethods,
|
|
2272
|
-
findings
|
|
2557
|
+
findings,
|
|
2558
|
+
computedLocations.get(propName) ?? propertyLocation
|
|
2273
2559
|
);
|
|
2274
2560
|
}
|
|
2275
2561
|
|
|
2276
2562
|
if (valuesConfigError) {
|
|
2277
2563
|
findings.invalidValuesConfigurations.push(
|
|
2278
|
-
|
|
2564
|
+
{
|
|
2565
|
+
location: propertyLocation,
|
|
2566
|
+
message: `property "${propName}" ${valuesConfigError}`
|
|
2567
|
+
}
|
|
2279
2568
|
);
|
|
2280
2569
|
}
|
|
2281
2570
|
|
|
@@ -2283,7 +2572,10 @@ function validatePropertyConfigs(
|
|
|
2283
2572
|
if (values) {
|
|
2284
2573
|
if (typeExpressionKind(typeExpression) !== 'String') {
|
|
2285
2574
|
findings.invalidValuesConfigurations.push(
|
|
2286
|
-
|
|
2575
|
+
{
|
|
2576
|
+
location: propertyLocation,
|
|
2577
|
+
message: `property "${propName}" uses values, but its type is not String`
|
|
2578
|
+
}
|
|
2287
2579
|
);
|
|
2288
2580
|
}
|
|
2289
2581
|
|
|
@@ -2295,8 +2587,12 @@ function validatePropertyConfigs(
|
|
|
2295
2587
|
!values.includes(valueProp.initializer.text)
|
|
2296
2588
|
) {
|
|
2297
2589
|
findings.invalidDefaultValues.push(
|
|
2298
|
-
|
|
2299
|
-
|
|
2590
|
+
{
|
|
2591
|
+
location: propertyLocation,
|
|
2592
|
+
message:
|
|
2593
|
+
`property "${propName}" default value ` +
|
|
2594
|
+
`"${valueProp.initializer.text}" is not in values`
|
|
2595
|
+
}
|
|
2300
2596
|
);
|
|
2301
2597
|
}
|
|
2302
2598
|
}
|
|
@@ -2309,15 +2605,23 @@ function validatePropertyConfigs(
|
|
|
2309
2605
|
);
|
|
2310
2606
|
if (mismatch) {
|
|
2311
2607
|
findings.invalidDefaultValues.push(
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2608
|
+
{
|
|
2609
|
+
location: propertyLocation,
|
|
2610
|
+
message:
|
|
2611
|
+
`property "${propName}" default value ` +
|
|
2612
|
+
`has type ${mismatch.valueTypeName}, ` +
|
|
2613
|
+
`but declared type is ${mismatch.typeName}`
|
|
2614
|
+
}
|
|
2315
2615
|
);
|
|
2316
2616
|
}
|
|
2317
2617
|
}
|
|
2318
2618
|
}
|
|
2319
2619
|
|
|
2320
|
-
validateComputedPropertyCycles(
|
|
2620
|
+
validateComputedPropertyCycles(
|
|
2621
|
+
computedDependencies,
|
|
2622
|
+
computedLocations,
|
|
2623
|
+
findings
|
|
2624
|
+
);
|
|
2321
2625
|
}
|
|
2322
2626
|
|
|
2323
2627
|
// Validates that a ref attribute targets a unique HTMLElement property.
|
|
@@ -2325,7 +2629,8 @@ function validateRefAttribute(
|
|
|
2325
2629
|
attrValue,
|
|
2326
2630
|
supportedProps,
|
|
2327
2631
|
findings,
|
|
2328
|
-
seenRefProps
|
|
2632
|
+
seenRefProps,
|
|
2633
|
+
location
|
|
2329
2634
|
) {
|
|
2330
2635
|
if (!attrValue) return;
|
|
2331
2636
|
|
|
@@ -2335,23 +2640,34 @@ function validateRefAttribute(
|
|
|
2335
2640
|
const propInfo = supportedProps.get(propName);
|
|
2336
2641
|
if (!propInfo) {
|
|
2337
2642
|
findings.invalidRefAttributes.push(
|
|
2338
|
-
|
|
2643
|
+
{
|
|
2644
|
+
location,
|
|
2645
|
+
message: `ref="${attrValue}" refers to missing property "${propName}"`
|
|
2646
|
+
}
|
|
2339
2647
|
);
|
|
2340
2648
|
return;
|
|
2341
2649
|
}
|
|
2342
2650
|
|
|
2343
2651
|
if (propInfo.typeText !== 'HTMLElement') {
|
|
2344
2652
|
findings.invalidRefAttributes.push(
|
|
2345
|
-
|
|
2346
|
-
|
|
2653
|
+
{
|
|
2654
|
+
location,
|
|
2655
|
+
message:
|
|
2656
|
+
`ref="${attrValue}" refers to property "${propName}" ` +
|
|
2657
|
+
'whose type is not HTMLElement'
|
|
2658
|
+
}
|
|
2347
2659
|
);
|
|
2348
2660
|
return;
|
|
2349
2661
|
}
|
|
2350
2662
|
|
|
2351
2663
|
if (seenRefProps.has(propName)) {
|
|
2352
2664
|
findings.invalidRefAttributes.push(
|
|
2353
|
-
|
|
2354
|
-
|
|
2665
|
+
{
|
|
2666
|
+
location,
|
|
2667
|
+
message:
|
|
2668
|
+
`ref="${attrValue}" is a duplicate reference ` +
|
|
2669
|
+
`to the property "${propName}"`
|
|
2670
|
+
}
|
|
2355
2671
|
);
|
|
2356
2672
|
return;
|
|
2357
2673
|
}
|
|
@@ -2360,15 +2676,19 @@ function validateRefAttribute(
|
|
|
2360
2676
|
}
|
|
2361
2677
|
|
|
2362
2678
|
// Validates event names used in value-binding attributes.
|
|
2363
|
-
function validateValueBindingEvent(node, attrName, findings) {
|
|
2679
|
+
function validateValueBindingEvent(node, attrName, findings, resolveLocation) {
|
|
2364
2680
|
const [realAttrName, eventName] = attrName.split(':');
|
|
2365
2681
|
if (realAttrName !== 'value' || !eventName) return;
|
|
2366
2682
|
if (SUPPORTED_EVENT_NAMES.has(eventName)) return;
|
|
2367
2683
|
|
|
2368
2684
|
const tagName = node.rawTagName || node.tagName || 'element';
|
|
2369
2685
|
findings.unsupportedEventNames.push(
|
|
2370
|
-
|
|
2371
|
-
|
|
2686
|
+
{
|
|
2687
|
+
location: getHtmlAttributeLocation(node, attrName, resolveLocation),
|
|
2688
|
+
message:
|
|
2689
|
+
`${tagName} attribute "${attrName}" refers to ` +
|
|
2690
|
+
`an unsupported event name "${eventName}"`
|
|
2691
|
+
}
|
|
2372
2692
|
);
|
|
2373
2693
|
}
|
|
2374
2694
|
|
|
@@ -2379,33 +2699,55 @@ function walkHtmlNode(
|
|
|
2379
2699
|
findings,
|
|
2380
2700
|
componentPropertyMaps,
|
|
2381
2701
|
supportedProps,
|
|
2382
|
-
seenRefProps
|
|
2702
|
+
seenRefProps,
|
|
2703
|
+
resolveLocation
|
|
2383
2704
|
) {
|
|
2384
2705
|
if (node.nodeType === 1) {
|
|
2385
|
-
validateHtmlNesting(node, findings);
|
|
2706
|
+
validateHtmlNesting(node, findings, resolveLocation);
|
|
2386
2707
|
|
|
2387
2708
|
for (const [attrName, attrValue] of Object.entries(node.attributes)) {
|
|
2388
2709
|
if (!attrValue) continue;
|
|
2389
|
-
validateFormAssocAttribute(
|
|
2710
|
+
validateFormAssocAttribute(
|
|
2711
|
+
node,
|
|
2712
|
+
attrName,
|
|
2713
|
+
attrValue,
|
|
2714
|
+
findings,
|
|
2715
|
+
resolveLocation
|
|
2716
|
+
);
|
|
2390
2717
|
validateFormAssocPropertyMappings(
|
|
2391
2718
|
node,
|
|
2392
2719
|
attrName,
|
|
2393
2720
|
attrValue,
|
|
2394
2721
|
findings,
|
|
2395
|
-
componentPropertyMaps
|
|
2722
|
+
componentPropertyMaps,
|
|
2723
|
+
resolveLocation
|
|
2396
2724
|
);
|
|
2397
2725
|
validateCheckedBinding(
|
|
2398
2726
|
node,
|
|
2399
2727
|
attrName,
|
|
2400
2728
|
attrValue,
|
|
2401
2729
|
findings,
|
|
2402
|
-
supportedProps
|
|
2730
|
+
supportedProps,
|
|
2731
|
+
resolveLocation
|
|
2732
|
+
);
|
|
2733
|
+
validateHtmlAttribute(node, attrName, findings, resolveLocation);
|
|
2734
|
+
validateValueBinding(
|
|
2735
|
+
node,
|
|
2736
|
+
attrName,
|
|
2737
|
+
attrValue,
|
|
2738
|
+
findings,
|
|
2739
|
+
supportedProps,
|
|
2740
|
+
resolveLocation
|
|
2403
2741
|
);
|
|
2404
|
-
|
|
2405
|
-
validateValueBinding(node, attrName, attrValue, findings, supportedProps);
|
|
2406
|
-
validateValueBindingEvent(node, attrName, findings);
|
|
2742
|
+
validateValueBindingEvent(node, attrName, findings, resolveLocation);
|
|
2407
2743
|
if (attrName === 'ref') {
|
|
2408
|
-
validateRefAttribute(
|
|
2744
|
+
validateRefAttribute(
|
|
2745
|
+
attrValue,
|
|
2746
|
+
supportedProps,
|
|
2747
|
+
findings,
|
|
2748
|
+
seenRefProps,
|
|
2749
|
+
getHtmlAttributeLocation(node, attrName, resolveLocation)
|
|
2750
|
+
);
|
|
2409
2751
|
}
|
|
2410
2752
|
if (
|
|
2411
2753
|
REFS_TEST_RE.test(attrValue) ||
|
|
@@ -2416,7 +2758,7 @@ function walkHtmlNode(
|
|
|
2416
2758
|
eventHandler: attrName.startsWith('on'),
|
|
2417
2759
|
kind: 'html',
|
|
2418
2760
|
text: attrValue.trim(),
|
|
2419
|
-
location:
|
|
2761
|
+
location: getHtmlAttributeLocation(node, attrName, resolveLocation)
|
|
2420
2762
|
});
|
|
2421
2763
|
}
|
|
2422
2764
|
}
|
|
@@ -2433,7 +2775,7 @@ function walkHtmlNode(
|
|
|
2433
2775
|
eventHandler: false,
|
|
2434
2776
|
kind: 'html',
|
|
2435
2777
|
text,
|
|
2436
|
-
location:
|
|
2778
|
+
location: getHtmlNodeLocation(node, resolveLocation)
|
|
2437
2779
|
});
|
|
2438
2780
|
}
|
|
2439
2781
|
}
|
|
@@ -2445,7 +2787,8 @@ function walkHtmlNode(
|
|
|
2445
2787
|
findings,
|
|
2446
2788
|
componentPropertyMaps,
|
|
2447
2789
|
supportedProps,
|
|
2448
|
-
seenRefProps
|
|
2790
|
+
seenRefProps,
|
|
2791
|
+
resolveLocation
|
|
2449
2792
|
);
|
|
2450
2793
|
}
|
|
2451
2794
|
}
|