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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/lint.js +489 -152
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "wrec",
3
3
  "description": "a library that greatly simplifies building web components",
4
4
  "author": "R. Mark Volkmann",
5
- "version": "0.40.0",
5
+ "version": "0.40.1",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
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
- `"${codeNode.text}" is not a defined instance method`
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(findings.undefinedMethods, name);
188
+ uniquePush(
189
+ findings.undefinedMethods,
190
+ metadata.location ? {location: metadata.location, message: name} : name
191
+ );
184
192
  } else {
185
- uniquePush(findings.undefinedProperties, name);
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(findings.undefinedContextFunctions, callee.text);
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) uniquePush(findings.undefinedMethods, callee.name.text);
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
- `useState maps state property "${statePath}" to ` +
519
- `missing component property "${componentProp}"`
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.includes(propName)
678
+ !duplicateProperties.some(
679
+ finding => getLocatedFindingMessage(finding).startsWith(`"${propName}" `)
680
+ )
642
681
  ) {
643
- duplicateProperties.push(propName);
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.includes(propName)
692
+ !reservedProperties.some(
693
+ finding => getLocatedFindingMessage(finding) === propName
694
+ )
648
695
  ) {
649
- reservedProperties.push(propName);
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(name => lines.push(` ${name}`));
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(message =>
935
- lines.push(` ${message}`)
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(message =>
942
- lines.push(` ${message}`)
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(message =>
949
- lines.push(` ${message}`)
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(message =>
956
- lines.push(` ${message}`)
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(message =>
963
- lines.push(` ${message}`)
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(message =>
970
- lines.push(` ${message}`)
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(message => lines.push(` ${message}`));
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(message =>
982
- lines.push(` ${message}`)
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(message =>
989
- lines.push(` ${message}`)
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(message =>
996
- lines.push(` ${message}`)
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(message => lines.push(` ${message}`));
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(message =>
1008
- lines.push(` ${message}`)
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(message =>
1015
- lines.push(` ${message}`)
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(message =>
1022
- lines.push(` ${message}`)
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(message =>
1029
- lines.push(` ${message}`)
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(name => lines.push(` ${name}`));
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
- lines.push(` ${finding.expression}: ${finding.message}`);
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(name => lines.push(` ${name}`));
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(name => lines.push(` ${name}`));
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(name => lines.push(` ${name}`));
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(message =>
1063
- lines.push(` ${message}`)
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(message =>
1070
- lines.push(` ${message}`)
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
- 'formAssociatedCallback is defined, but static formAssociated is not true'
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
- 'static html property must be defined'
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
- `"${expr.text}" is not a defined instance method`
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
- if (!array.includes(value)) array.push(value);
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
- `input type="${inputType}" attribute "${attrName}" refers to ` +
1866
- `property "${propName}" whose type is not ${expectedTypeName}`
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
- `${getHtmlTagName(node)} attribute "${attrName}" refers to property ` +
1894
- `"${propName}" whose type is not String or Number`
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
- `property "${propName}" computed references ` +
1914
- `missing property "${referencedName}"`
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
- `property "${propName}" computed calls ` +
1924
- `non-method instance member "${methodName}"`
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(computedDependencies, findings) {
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
- `computed properties form a cycle: ${cycleNames.join(', ')}`
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(attrName, attrValue, findings) {
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
- `form-assoc="${attrValue}" is invalid; expected ` +
2059
- '"property:field" or a comma-separated list of them'
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
- `form-assoc="${attrValue}" refers to ` +
2086
- `missing component property "${propName}"`
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
- `${tagName} attribute "${attrName}" is not supported`
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
- `<${tagName}> must be nested inside ${[...allowedParents]
2131
- .map(name => `<${name}>`)
2132
- .join(' or ')}, but parent is ${parentDescription}`
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
- `<${childTagName}> is not allowed directly inside <${tagName}>`
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
- `property "${propName}" does not specify a type`
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
- `property "${propName}" type cannot use generic syntax like ` +
2202
- `"${typeExpression.getText(sourceFile).trim()}"; use ` +
2203
- `"${getPropertyTypeGenericBaseName(sourceFile, typeExpression)}" instead`
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
- `property "${propName}" type must be one of ` +
2210
- 'Boolean, Number, String, Object, Array, or HTMLElement'
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
- `property "${propName}" declare type ` +
2222
- `"${getPropertyTypeTextFromNode(sourceFile, declaredTypeNode)}" ` +
2223
- `is not compatible with static properties type ` +
2224
- `"${getPropertyConfigTypeName(typeExpressionKind(typeExpression))}"`
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
- `property "${propName}" usedBy references ` +
2239
- `missing getter "${getterName}"`
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
- `property "${propName}" usedBy references ` +
2247
- `missing method "${methodName}"`
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
- `property "${propName}" ${valuesConfigError}`
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
- `property "${propName}" uses values, but its type is not String`
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
- `property "${propName}" default value ` +
2299
- `"${valueProp.initializer.text}" is not in values`
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
- `property "${propName}" default value ` +
2313
- `has type ${mismatch.valueTypeName}, ` +
2314
- `but declared type is ${mismatch.typeName}`
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(computedDependencies, findings);
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
- `ref="${attrValue}" refers to missing property "${propName}"`
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
- `ref="${attrValue}" refers to property "${propName}" ` +
2346
- 'whose type is not HTMLElement'
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
- `ref="${attrValue}" is a duplicate reference ` +
2354
- `to the property "${propName}"`
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
- `${tagName} attribute "${attrName}" refers to ` +
2371
- `an unsupported event name "${eventName}"`
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(attrName, attrValue, findings);
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
- validateHtmlAttribute(node, attrName, findings);
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(attrValue, supportedProps, findings, seenRefProps);
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: null
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: null
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
  }