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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/lint.js +497 -154
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.2",
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,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 value = match[2]?.trim();
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: null
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(name => lines.push(` ${name}`));
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(message =>
935
- lines.push(` ${message}`)
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(message =>
942
- lines.push(` ${message}`)
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(message =>
949
- lines.push(` ${message}`)
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(message =>
956
- lines.push(` ${message}`)
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(message =>
963
- lines.push(` ${message}`)
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(message =>
970
- lines.push(` ${message}`)
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(message => lines.push(` ${message}`));
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(message =>
982
- lines.push(` ${message}`)
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(message =>
989
- lines.push(` ${message}`)
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(message =>
996
- lines.push(` ${message}`)
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(message => lines.push(` ${message}`));
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(message =>
1008
- lines.push(` ${message}`)
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(message =>
1015
- lines.push(` ${message}`)
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(message =>
1022
- lines.push(` ${message}`)
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(message =>
1029
- lines.push(` ${message}`)
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(name => lines.push(` ${name}`));
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
- lines.push(` ${finding.expression}: ${finding.message}`);
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(name => lines.push(` ${name}`));
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(name => lines.push(` ${name}`));
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(name => lines.push(` ${name}`));
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(message =>
1063
- lines.push(` ${message}`)
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(message =>
1070
- lines.push(` ${message}`)
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
- 'formAssociatedCallback is defined, but static formAssociated is not true'
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
- 'static html property must be defined'
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
- `"${expr.text}" is not a defined instance method`
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
- if (!array.includes(value)) array.push(value);
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
- `input type="${inputType}" attribute "${attrName}" refers to ` +
1866
- `property "${propName}" whose type is not ${expectedTypeName}`
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
- `${getHtmlTagName(node)} attribute "${attrName}" refers to property ` +
1894
- `"${propName}" whose type is not String or Number`
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
- `property "${propName}" computed references ` +
1914
- `missing property "${referencedName}"`
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
- `property "${propName}" computed calls ` +
1924
- `non-method instance member "${methodName}"`
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(computedDependencies, findings) {
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
- `computed properties form a cycle: ${cycleNames.join(', ')}`
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(attrName, attrValue, findings) {
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
- `form-assoc="${attrValue}" is invalid; expected ` +
2059
- '"property:field" or a comma-separated list of them'
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
- `form-assoc="${attrValue}" refers to ` +
2086
- `missing component property "${propName}"`
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
- `${tagName} attribute "${attrName}" is not supported`
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
- `<${tagName}> must be nested inside ${[...allowedParents]
2131
- .map(name => `<${name}>`)
2132
- .join(' or ')}, but parent is ${parentDescription}`
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
- `<${childTagName}> is not allowed directly inside <${tagName}>`
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
- `property "${propName}" does not specify a type`
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
- `property "${propName}" type cannot use generic syntax like ` +
2202
- `"${typeExpression.getText(sourceFile).trim()}"; use ` +
2203
- `"${getPropertyTypeGenericBaseName(sourceFile, typeExpression)}" instead`
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
- `property "${propName}" type must be one of ` +
2210
- 'Boolean, Number, String, Object, Array, or HTMLElement'
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
- `property "${propName}" declare type ` +
2222
- `"${getPropertyTypeTextFromNode(sourceFile, declaredTypeNode)}" ` +
2223
- `is not compatible with static properties type ` +
2224
- `"${getPropertyConfigTypeName(typeExpressionKind(typeExpression))}"`
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
- `property "${propName}" usedBy references ` +
2239
- `missing getter "${getterName}"`
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
- `property "${propName}" usedBy references ` +
2247
- `missing method "${methodName}"`
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
- `property "${propName}" ${valuesConfigError}`
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
- `property "${propName}" uses values, but its type is not String`
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
- `property "${propName}" default value ` +
2299
- `"${valueProp.initializer.text}" is not in values`
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
- `property "${propName}" default value ` +
2313
- `has type ${mismatch.valueTypeName}, ` +
2314
- `but declared type is ${mismatch.typeName}`
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(computedDependencies, findings);
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
- `ref="${attrValue}" refers to missing property "${propName}"`
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
- `ref="${attrValue}" refers to property "${propName}" ` +
2346
- 'whose type is not HTMLElement'
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
- `ref="${attrValue}" is a duplicate reference ` +
2354
- `to the property "${propName}"`
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
- `${tagName} attribute "${attrName}" refers to ` +
2371
- `an unsupported event name "${eventName}"`
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(attrName, attrValue, findings);
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
- validateHtmlAttribute(node, attrName, findings);
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(attrValue, supportedProps, findings, seenRefProps);
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: null
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: null
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
  }