eslint 10.0.0-beta.0 → 10.0.0-rc.0

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.
@@ -11,6 +11,7 @@
11
11
  //------------------------------------------------------------------------------
12
12
 
13
13
  const assert = require("node:assert"),
14
+ { existsSync, readFileSync } = require("node:fs"),
14
15
  util = require("node:util"),
15
16
  path = require("node:path"),
16
17
  equal = require("fast-deep-equal"),
@@ -653,6 +654,190 @@ function assertInvalidTestCase(
653
654
  checkDuplicateTestCase(item, seenTestCases);
654
655
  }
655
656
 
657
+ /**
658
+ * Gets the invocation location from the stack trace for later use.
659
+ * @param {Function} relative The function before the invocation point.
660
+ * @returns {string} The invocation location.
661
+ */
662
+ function getInvocationLocation(relative = getInvocationLocation) {
663
+ const dummyObject = {};
664
+ Error.captureStackTrace(dummyObject, relative);
665
+ const { stack } = dummyObject;
666
+
667
+ return stack.split("\n")[1].replace(/.*?\(/u, "").replace(/\)$/u, "");
668
+ }
669
+
670
+ /**
671
+ * Estimates the location of the test case in the source file.
672
+ * @param {Function} invoker The method that runs the tests.
673
+ * @returns {(key: string) => string} The lazy resolver for the estimated location of the test case.
674
+ */
675
+ function buildLazyTestLocationEstimator(invoker) {
676
+ const invocationLocation = getInvocationLocation(invoker);
677
+ let testLocations = null;
678
+ return key => {
679
+ if (testLocations === null) {
680
+ let [sourceFile, sourceLine = "1", sourceColumn = "1"] =
681
+ invocationLocation
682
+ .replace(/:\\/u, "\\\\") // Windows workaround for C:\\
683
+ .split(":");
684
+ sourceFile = sourceFile.replace(/\\\\/u, ":\\");
685
+ sourceLine = Number(sourceLine);
686
+ sourceColumn = Number(sourceColumn);
687
+ testLocations = { root: invocationLocation };
688
+
689
+ if (existsSync(sourceFile)) {
690
+ let content = readFileSync(sourceFile, "utf8")
691
+ .split("\n")
692
+ .slice(sourceLine - 1);
693
+ content[0] = content[0].slice(Math.max(0, sourceColumn - 1));
694
+ content = content.map(
695
+ l =>
696
+ l
697
+ .trim() // Remove whitespace
698
+ .replace(/\s*\/\/.*$(?<!,)/u, ""), // and trailing in-line comments that aren't part of the test `code`
699
+ );
700
+
701
+ // Roots
702
+ const validStartIndex = content.findIndex(line =>
703
+ /\bvalid:/u.test(line),
704
+ );
705
+ const invalidStartIndex = content.findIndex(line =>
706
+ /\binvalid:/u.test(line),
707
+ );
708
+
709
+ testLocations.valid = `${sourceFile}:${
710
+ sourceLine + validStartIndex
711
+ }`;
712
+ testLocations.invalid = `${sourceFile}:${
713
+ sourceLine + invalidStartIndex
714
+ }`;
715
+
716
+ // Scenario basics
717
+ const validEndIndex =
718
+ validStartIndex < invalidStartIndex
719
+ ? invalidStartIndex
720
+ : content.length;
721
+ const invalidEndIndex =
722
+ validStartIndex < invalidStartIndex
723
+ ? content.length
724
+ : validStartIndex;
725
+
726
+ const validLines = content.slice(
727
+ validStartIndex,
728
+ validEndIndex,
729
+ );
730
+ const invalidLines = content.slice(
731
+ invalidStartIndex,
732
+ invalidEndIndex,
733
+ );
734
+
735
+ let objectDepth = 0;
736
+ const validLineIndexes = validLines
737
+ .map((l, i) => {
738
+ // matches `key: {` and `{`
739
+ if (/^(?:\w+\s*:\s*)?\{/u.test(l)) {
740
+ objectDepth++;
741
+ }
742
+
743
+ if (objectDepth > 0) {
744
+ if (l.endsWith("}") || l.endsWith("},")) {
745
+ objectDepth--;
746
+ }
747
+
748
+ return objectDepth <= 1 && l.includes("code:")
749
+ ? i
750
+ : null;
751
+ }
752
+
753
+ return l.endsWith(",") ? i : null;
754
+ })
755
+ .filter(Boolean);
756
+ const invalidLineIndexes = invalidLines
757
+ .map((l, i) =>
758
+ l.trimStart().startsWith("errors:") ? i : null,
759
+ )
760
+ .filter(Boolean);
761
+
762
+ Object.assign(
763
+ testLocations,
764
+ {
765
+ [`valid[0]`]: `${sourceFile}:${
766
+ sourceLine + validStartIndex
767
+ }`,
768
+ },
769
+ Object.fromEntries(
770
+ validLineIndexes.map((location, validIndex) => [
771
+ `valid[${validIndex}]`,
772
+ `${sourceFile}:${
773
+ sourceLine + validStartIndex + location
774
+ }`,
775
+ ]),
776
+ ),
777
+ Object.fromEntries(
778
+ invalidLineIndexes.map((location, invalidIndex) => [
779
+ `invalid[${invalidIndex}]`,
780
+ `${sourceFile}:${
781
+ sourceLine + invalidStartIndex + location
782
+ }`,
783
+ ]),
784
+ ),
785
+ );
786
+
787
+ // Indexes for errors inside each invalid test case
788
+ invalidLineIndexes.push(invalidLines.length);
789
+
790
+ for (let i = 0; i < invalidLineIndexes.length - 1; i++) {
791
+ const start = invalidLineIndexes[i];
792
+ const end = invalidLineIndexes[i + 1];
793
+ const errorLines = invalidLines.slice(start, end);
794
+ let errorObjectDepth = 0;
795
+ const errorLineIndexes = errorLines
796
+ .map((l, j) => {
797
+ if (l.startsWith("{") || l.endsWith("{")) {
798
+ errorObjectDepth++;
799
+
800
+ if (l.endsWith("}") || l.endsWith("},")) {
801
+ errorObjectDepth--;
802
+ }
803
+
804
+ return errorObjectDepth <= 1 ? j : null;
805
+ }
806
+
807
+ if (errorObjectDepth > 0) {
808
+ if (l.endsWith("}") || l.endsWith("},")) {
809
+ errorObjectDepth--;
810
+ }
811
+
812
+ return null;
813
+ }
814
+
815
+ return l.endsWith(",") ? j : null;
816
+ })
817
+ .filter(Boolean);
818
+
819
+ Object.assign(
820
+ testLocations,
821
+ Object.fromEntries(
822
+ errorLineIndexes.map((line, errorIndex) => [
823
+ `invalid[${i}].errors[${errorIndex}]`,
824
+ `${sourceFile}:${
825
+ sourceLine +
826
+ invalidStartIndex +
827
+ start +
828
+ line
829
+ }`,
830
+ ]),
831
+ ),
832
+ );
833
+ }
834
+ }
835
+ }
836
+
837
+ return testLocations[key] || "unknown source";
838
+ };
839
+ }
840
+
656
841
  //------------------------------------------------------------------------------
657
842
  // Public Interface
658
843
  //------------------------------------------------------------------------------
@@ -836,6 +1021,7 @@ class RuleTester {
836
1021
  * assertionOptions?: {
837
1022
  * requireMessage?: boolean | "message" | "messageId",
838
1023
  * requireLocation?: boolean
1024
+ * requireData?: boolean | "error" | "suggestion"
839
1025
  * },
840
1026
  * valid: (ValidTestCase | string)[],
841
1027
  * invalid: InvalidTestCase[]
@@ -852,6 +1038,8 @@ class RuleTester {
852
1038
  assertRule(rule, ruleName);
853
1039
  assertTest(test, ruleName);
854
1040
 
1041
+ const estimateTestLocation = buildLazyTestLocationEstimator(this.run);
1042
+
855
1043
  const baseConfig = [
856
1044
  {
857
1045
  plugins: {
@@ -1215,10 +1403,14 @@ class RuleTester {
1215
1403
  * @param {Object} item Item to run the rule against
1216
1404
  * @returns {void}
1217
1405
  * @private
1406
+ * @throws {Error} If the test case is invalid or has an invalid error.
1218
1407
  */
1219
1408
  function testInvalidTemplate(item) {
1220
- const { requireMessage = false, requireLocation = false } =
1221
- test.assertionOptions ?? {};
1409
+ const {
1410
+ requireMessage = false,
1411
+ requireLocation = false,
1412
+ requireData = false,
1413
+ } = test.assertionOptions ?? {};
1222
1414
 
1223
1415
  const ruleHasMetaMessages =
1224
1416
  hasOwnProperty(rule, "meta") &&
@@ -1286,338 +1478,384 @@ class RuleTester {
1286
1478
  );
1287
1479
 
1288
1480
  for (let i = 0, l = item.errors.length; i < l; i++) {
1289
- const error = item.errors[i];
1290
- const message = messages[i];
1291
-
1292
- assert(
1293
- hasMessageOfThisRule,
1294
- "Error rule name should be the same as the name of the rule being tested",
1295
- );
1481
+ try {
1482
+ const error = item.errors[i];
1483
+ const message = messages[i];
1296
1484
 
1297
- if (typeof error === "string" || error instanceof RegExp) {
1298
- // Just an error message.
1299
- assertMessageMatches(message.message, error);
1300
- assert.ok(
1301
- message.suggestions === void 0,
1302
- `Error at index ${i} has suggestions. Please convert the test error into an object and specify 'suggestions' property on it to test suggestions.`,
1485
+ assert(
1486
+ hasMessageOfThisRule,
1487
+ "Error rule name should be the same as the name of the rule being tested",
1303
1488
  );
1304
- } else if (typeof error === "object" && error !== null) {
1305
- /*
1306
- * Error object.
1307
- * This may have a message, messageId, data, line, and/or column.
1308
- */
1309
1489
 
1310
- if (hasOwnProperty(error, "message")) {
1311
- assertMessageMatches(
1312
- message.message,
1313
- error.message,
1314
- );
1315
- } else if (hasOwnProperty(error, "messageId")) {
1490
+ if (
1491
+ typeof error === "string" ||
1492
+ error instanceof RegExp
1493
+ ) {
1494
+ // Just an error message.
1495
+ assertMessageMatches(message.message, error);
1316
1496
  assert.ok(
1317
- ruleHasMetaMessages,
1318
- "Error can not use 'messageId' if rule under test doesn't define 'meta.messages'.",
1497
+ message.suggestions === void 0,
1498
+ `Error at index ${i} has suggestions. Please convert the test error into an object and specify 'suggestions' property on it to test suggestions.`,
1319
1499
  );
1320
- if (
1321
- !hasOwnProperty(
1322
- rule.meta.messages,
1500
+ } else if (
1501
+ typeof error === "object" &&
1502
+ error !== null
1503
+ ) {
1504
+ /*
1505
+ * Error object.
1506
+ * This may have a message, messageId, data, line, and/or column.
1507
+ */
1508
+
1509
+ if (hasOwnProperty(error, "message")) {
1510
+ assertMessageMatches(
1511
+ message.message,
1512
+ error.message,
1513
+ );
1514
+ } else if (hasOwnProperty(error, "messageId")) {
1515
+ assert.ok(
1516
+ ruleHasMetaMessages,
1517
+ "Error can not use 'messageId' if rule under test doesn't define 'meta.messages'.",
1518
+ );
1519
+ if (
1520
+ !hasOwnProperty(
1521
+ rule.meta.messages,
1522
+ error.messageId,
1523
+ )
1524
+ ) {
1525
+ assert(
1526
+ false,
1527
+ `Invalid messageId '${error.messageId}'. Expected one of ${friendlyIDList}.`,
1528
+ );
1529
+ }
1530
+ assert.strictEqual(
1531
+ message.messageId,
1323
1532
  error.messageId,
1324
- )
1325
- ) {
1326
- assert(
1327
- false,
1328
- `Invalid messageId '${error.messageId}'. Expected one of ${friendlyIDList}.`,
1533
+ `messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.`,
1329
1534
  );
1330
- }
1331
- assert.strictEqual(
1332
- message.messageId,
1333
- error.messageId,
1334
- `messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.`,
1335
- );
1336
1535
 
1337
- const unsubstitutedPlaceholders =
1338
- getUnsubstitutedMessagePlaceholders(
1339
- message.message,
1340
- rule.meta.messages[message.messageId],
1341
- error.data,
1536
+ const unsubstitutedPlaceholders =
1537
+ getUnsubstitutedMessagePlaceholders(
1538
+ message.message,
1539
+ rule.meta.messages[message.messageId],
1540
+ error.data,
1541
+ );
1542
+
1543
+ assert.ok(
1544
+ unsubstitutedPlaceholders.length === 0,
1545
+ `The reported message has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the 'data' property in the context.report() call.`,
1342
1546
  );
1343
1547
 
1344
- assert.ok(
1345
- unsubstitutedPlaceholders.length === 0,
1346
- `The reported message has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the 'data' property in the context.report() call.`,
1347
- );
1548
+ if (hasOwnProperty(error, "data")) {
1549
+ /*
1550
+ * if data was provided, then directly compare the returned message to a synthetic
1551
+ * interpolated message using the same message ID and data provided in the test.
1552
+ * See https://github.com/eslint/eslint/issues/9890 for context.
1553
+ */
1554
+ const unformattedOriginalMessage =
1555
+ rule.meta.messages[error.messageId];
1556
+ const rehydratedMessage = interpolate(
1557
+ unformattedOriginalMessage,
1558
+ error.data,
1559
+ );
1348
1560
 
1349
- if (hasOwnProperty(error, "data")) {
1350
- /*
1351
- * if data was provided, then directly compare the returned message to a synthetic
1352
- * interpolated message using the same message ID and data provided in the test.
1353
- * See https://github.com/eslint/eslint/issues/9890 for context.
1354
- */
1355
- const unformattedOriginalMessage =
1356
- rule.meta.messages[error.messageId];
1357
- const rehydratedMessage = interpolate(
1358
- unformattedOriginalMessage,
1359
- error.data,
1360
- );
1561
+ assert.strictEqual(
1562
+ message.message,
1563
+ rehydratedMessage,
1564
+ `Hydrated message "${rehydratedMessage}" does not match "${message.message}"`,
1565
+ );
1566
+ } else {
1567
+ const requiresDataProperty =
1568
+ requireData === true ||
1569
+ requireData === "error";
1570
+ const hasPlaceholders =
1571
+ getMessagePlaceholders(
1572
+ rule.meta.messages[error.messageId],
1573
+ ).length > 0;
1574
+ assert.ok(
1575
+ !requiresDataProperty ||
1576
+ !hasPlaceholders,
1577
+ `Error should specify the 'data' property as the referenced message has placeholders.`,
1578
+ );
1579
+ }
1580
+ }
1361
1581
 
1362
- assert.strictEqual(
1363
- message.message,
1364
- rehydratedMessage,
1365
- `Hydrated message "${rehydratedMessage}" does not match "${message.message}"`,
1582
+ const locationProperties = [
1583
+ "line",
1584
+ "column",
1585
+ "endLine",
1586
+ "endColumn",
1587
+ ];
1588
+ const actualLocation = {};
1589
+ const expectedLocation = {};
1590
+
1591
+ for (const key of locationProperties) {
1592
+ if (hasOwnProperty(error, key)) {
1593
+ actualLocation[key] = message[key];
1594
+ expectedLocation[key] = error[key];
1595
+ }
1596
+ }
1597
+
1598
+ if (requireLocation) {
1599
+ const missingKeys = locationProperties.filter(
1600
+ key =>
1601
+ !hasOwnProperty(error, key) &&
1602
+ hasOwnProperty(message, key),
1603
+ );
1604
+ assert.ok(
1605
+ missingKeys.length === 0,
1606
+ `Error is missing expected location properties: ${missingKeys.join(", ")}`,
1366
1607
  );
1367
1608
  }
1368
- }
1369
1609
 
1370
- const locationProperties = [
1371
- "line",
1372
- "column",
1373
- "endLine",
1374
- "endColumn",
1375
- ];
1376
- const actualLocation = {};
1377
- const expectedLocation = {};
1378
-
1379
- for (const key of locationProperties) {
1380
- if (hasOwnProperty(error, key)) {
1381
- actualLocation[key] = message[key];
1382
- expectedLocation[key] = error[key];
1610
+ if (Object.keys(expectedLocation).length > 0) {
1611
+ assert.deepStrictEqual(
1612
+ actualLocation,
1613
+ expectedLocation,
1614
+ "Actual error location does not match expected error location.",
1615
+ );
1383
1616
  }
1384
- }
1385
1617
 
1386
- if (requireLocation) {
1387
- const missingKeys = locationProperties.filter(
1388
- key =>
1389
- !hasOwnProperty(error, key) &&
1390
- hasOwnProperty(message, key),
1391
- );
1392
1618
  assert.ok(
1393
- missingKeys.length === 0,
1394
- `Error is missing expected location properties: ${missingKeys.join(", ")}`,
1619
+ !message.suggestions ||
1620
+ hasOwnProperty(error, "suggestions"),
1621
+ `Error at index ${i} has suggestions. Please specify 'suggestions' property on the test error object.`,
1395
1622
  );
1396
- }
1397
-
1398
- if (Object.keys(expectedLocation).length > 0) {
1399
- assert.deepStrictEqual(
1400
- actualLocation,
1401
- expectedLocation,
1402
- "Actual error location does not match expected error location.",
1403
- );
1404
- }
1405
-
1406
- assert.ok(
1407
- !message.suggestions ||
1408
- hasOwnProperty(error, "suggestions"),
1409
- `Error at index ${i} has suggestions. Please specify 'suggestions' property on the test error object.`,
1410
- );
1411
- if (hasOwnProperty(error, "suggestions")) {
1412
- // Support asserting there are no suggestions
1413
- const expectsSuggestions = Array.isArray(
1414
- error.suggestions,
1415
- )
1416
- ? error.suggestions.length > 0
1417
- : Boolean(error.suggestions);
1418
- const hasSuggestions =
1419
- message.suggestions !== void 0;
1420
-
1421
- if (!hasSuggestions && expectsSuggestions) {
1422
- assert.ok(
1423
- !error.suggestions,
1424
- `Error should have suggestions on error with message: "${message.message}"`,
1425
- );
1426
- } else if (hasSuggestions) {
1427
- assert.ok(
1428
- expectsSuggestions,
1429
- `Error should have no suggestions on error with message: "${message.message}"`,
1430
- );
1431
- if (typeof error.suggestions === "number") {
1432
- assert.strictEqual(
1433
- message.suggestions.length,
1434
- error.suggestions,
1435
- `Error should have ${error.suggestions} suggestions. Instead found ${message.suggestions.length} suggestions`,
1623
+ if (hasOwnProperty(error, "suggestions")) {
1624
+ // Support asserting there are no suggestions
1625
+ const expectsSuggestions = Array.isArray(
1626
+ error.suggestions,
1627
+ )
1628
+ ? error.suggestions.length > 0
1629
+ : Boolean(error.suggestions);
1630
+ const hasSuggestions =
1631
+ message.suggestions !== void 0;
1632
+
1633
+ if (!hasSuggestions && expectsSuggestions) {
1634
+ assert.ok(
1635
+ !error.suggestions,
1636
+ `Error should have suggestions on error with message: "${message.message}"`,
1436
1637
  );
1437
- } else if (Array.isArray(error.suggestions)) {
1438
- assert.strictEqual(
1439
- message.suggestions.length,
1440
- error.suggestions.length,
1441
- `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`,
1638
+ } else if (hasSuggestions) {
1639
+ assert.ok(
1640
+ expectsSuggestions,
1641
+ `Error should have no suggestions on error with message: "${message.message}"`,
1442
1642
  );
1443
-
1444
- error.suggestions.forEach(
1445
- (expectedSuggestion, index) => {
1446
- assert.ok(
1447
- typeof expectedSuggestion ===
1448
- "object" &&
1449
- expectedSuggestion !== null,
1450
- "Test suggestion in 'suggestions' array must be an object.",
1451
- );
1452
- Object.keys(
1453
- expectedSuggestion,
1454
- ).forEach(propertyName => {
1643
+ if (typeof error.suggestions === "number") {
1644
+ assert.strictEqual(
1645
+ message.suggestions.length,
1646
+ error.suggestions,
1647
+ `Error should have ${error.suggestions} suggestions. Instead found ${message.suggestions.length} suggestions`,
1648
+ );
1649
+ } else if (
1650
+ Array.isArray(error.suggestions)
1651
+ ) {
1652
+ assert.strictEqual(
1653
+ message.suggestions.length,
1654
+ error.suggestions.length,
1655
+ `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`,
1656
+ );
1657
+
1658
+ error.suggestions.forEach(
1659
+ (expectedSuggestion, index) => {
1455
1660
  assert.ok(
1456
- suggestionObjectParameters.has(
1457
- propertyName,
1458
- ),
1459
- `Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.`,
1661
+ typeof expectedSuggestion ===
1662
+ "object" &&
1663
+ expectedSuggestion !==
1664
+ null,
1665
+ "Test suggestion in 'suggestions' array must be an object.",
1460
1666
  );
1461
- });
1667
+ Object.keys(
1668
+ expectedSuggestion,
1669
+ ).forEach(propertyName => {
1670
+ assert.ok(
1671
+ suggestionObjectParameters.has(
1672
+ propertyName,
1673
+ ),
1674
+ `Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.`,
1675
+ );
1676
+ });
1462
1677
 
1463
- const actualSuggestion =
1464
- message.suggestions[index];
1465
- const suggestionPrefix = `Error Suggestion at index ${index}:`;
1678
+ const actualSuggestion =
1679
+ message.suggestions[index];
1680
+ const suggestionPrefix = `Error Suggestion at index ${index}:`;
1466
1681
 
1467
- if (
1468
- hasOwnProperty(
1469
- expectedSuggestion,
1470
- "desc",
1471
- )
1472
- ) {
1473
- assert.ok(
1474
- !hasOwnProperty(
1682
+ if (
1683
+ hasOwnProperty(
1475
1684
  expectedSuggestion,
1476
- "data",
1477
- ),
1478
- `${suggestionPrefix} Test should not specify both 'desc' and 'data'.`,
1479
- );
1480
- assert.ok(
1481
- !hasOwnProperty(
1685
+ "desc",
1686
+ )
1687
+ ) {
1688
+ assert.ok(
1689
+ !hasOwnProperty(
1690
+ expectedSuggestion,
1691
+ "data",
1692
+ ),
1693
+ `${suggestionPrefix} Test should not specify both 'desc' and 'data'.`,
1694
+ );
1695
+ assert.ok(
1696
+ !hasOwnProperty(
1697
+ expectedSuggestion,
1698
+ "messageId",
1699
+ ),
1700
+ `${suggestionPrefix} Test should not specify both 'desc' and 'messageId'.`,
1701
+ );
1702
+ assert.strictEqual(
1703
+ actualSuggestion.desc,
1704
+ expectedSuggestion.desc,
1705
+ `${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.`,
1706
+ );
1707
+ } else if (
1708
+ hasOwnProperty(
1482
1709
  expectedSuggestion,
1483
1710
  "messageId",
1484
- ),
1485
- `${suggestionPrefix} Test should not specify both 'desc' and 'messageId'.`,
1486
- );
1487
- assert.strictEqual(
1488
- actualSuggestion.desc,
1489
- expectedSuggestion.desc,
1490
- `${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.`,
1491
- );
1492
- } else if (
1493
- hasOwnProperty(
1494
- expectedSuggestion,
1495
- "messageId",
1496
- )
1497
- ) {
1498
- assert.ok(
1499
- ruleHasMetaMessages,
1500
- `${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`,
1501
- );
1502
- assert.ok(
1503
- hasOwnProperty(
1504
- rule.meta.messages,
1711
+ )
1712
+ ) {
1713
+ assert.ok(
1714
+ ruleHasMetaMessages,
1715
+ `${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`,
1716
+ );
1717
+ assert.ok(
1718
+ hasOwnProperty(
1719
+ rule.meta.messages,
1720
+ expectedSuggestion.messageId,
1721
+ ),
1722
+ `${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`,
1723
+ );
1724
+ assert.strictEqual(
1725
+ actualSuggestion.messageId,
1505
1726
  expectedSuggestion.messageId,
1506
- ),
1507
- `${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`,
1508
- );
1509
- assert.strictEqual(
1510
- actualSuggestion.messageId,
1511
- expectedSuggestion.messageId,
1512
- `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`,
1513
- );
1727
+ `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`,
1728
+ );
1514
1729
 
1515
- const unsubstitutedPlaceholders =
1516
- getUnsubstitutedMessagePlaceholders(
1517
- actualSuggestion.desc,
1730
+ const rawSuggestionMessage =
1518
1731
  rule.meta.messages[
1519
1732
  expectedSuggestion
1520
1733
  .messageId
1521
- ],
1522
- expectedSuggestion.data,
1523
- );
1734
+ ];
1735
+ const unsubstitutedPlaceholders =
1736
+ getUnsubstitutedMessagePlaceholders(
1737
+ actualSuggestion.desc,
1738
+ rawSuggestionMessage,
1739
+ expectedSuggestion.data,
1740
+ );
1524
1741
 
1525
- assert.ok(
1526
- unsubstitutedPlaceholders.length ===
1527
- 0,
1528
- `The message of the suggestion has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the 'data' property for the suggestion in the context.report() call.`,
1529
- );
1742
+ assert.ok(
1743
+ unsubstitutedPlaceholders.length ===
1744
+ 0,
1745
+ `The message of the suggestion has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the 'data' property for the suggestion in the context.report() call.`,
1746
+ );
1530
1747
 
1531
- if (
1748
+ if (
1749
+ hasOwnProperty(
1750
+ expectedSuggestion,
1751
+ "data",
1752
+ )
1753
+ ) {
1754
+ const unformattedMetaMessage =
1755
+ rule.meta.messages[
1756
+ expectedSuggestion
1757
+ .messageId
1758
+ ];
1759
+ const rehydratedDesc =
1760
+ interpolate(
1761
+ unformattedMetaMessage,
1762
+ expectedSuggestion.data,
1763
+ );
1764
+
1765
+ assert.strictEqual(
1766
+ actualSuggestion.desc,
1767
+ rehydratedDesc,
1768
+ `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`,
1769
+ );
1770
+ } else {
1771
+ const requiresDataProperty =
1772
+ requireData ===
1773
+ true ||
1774
+ requireData ===
1775
+ "suggestion";
1776
+ const hasPlaceholders =
1777
+ getMessagePlaceholders(
1778
+ rawSuggestionMessage,
1779
+ ).length > 0;
1780
+ assert.ok(
1781
+ !requiresDataProperty ||
1782
+ !hasPlaceholders,
1783
+ `${suggestionPrefix} Suggestion should specify the 'data' property as the referenced message has placeholders.`,
1784
+ );
1785
+ }
1786
+ } else if (
1532
1787
  hasOwnProperty(
1533
1788
  expectedSuggestion,
1534
1789
  "data",
1535
1790
  )
1536
1791
  ) {
1537
- const unformattedMetaMessage =
1538
- rule.meta.messages[
1539
- expectedSuggestion
1540
- .messageId
1541
- ];
1542
- const rehydratedDesc =
1543
- interpolate(
1544
- unformattedMetaMessage,
1545
- expectedSuggestion.data,
1546
- );
1547
-
1548
- assert.strictEqual(
1549
- actualSuggestion.desc,
1550
- rehydratedDesc,
1551
- `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`,
1792
+ assert.fail(
1793
+ `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`,
1794
+ );
1795
+ } else {
1796
+ assert.fail(
1797
+ `${suggestionPrefix} Test must specify either 'messageId' or 'desc'.`,
1552
1798
  );
1553
1799
  }
1554
- } else if (
1555
- hasOwnProperty(
1556
- expectedSuggestion,
1557
- "data",
1558
- )
1559
- ) {
1560
- assert.fail(
1561
- `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`,
1800
+
1801
+ assert.ok(
1802
+ hasOwnProperty(
1803
+ expectedSuggestion,
1804
+ "output",
1805
+ ),
1806
+ `${suggestionPrefix} The "output" property is required.`,
1562
1807
  );
1563
- } else {
1564
- assert.fail(
1565
- `${suggestionPrefix} Test must specify either 'messageId' or 'desc'.`,
1808
+ const codeWithAppliedSuggestion =
1809
+ SourceCodeFixer.applyFixes(
1810
+ item.code,
1811
+ [actualSuggestion],
1812
+ ).output;
1813
+
1814
+ // Verify if suggestion fix makes a syntax error or not.
1815
+ const errorMessageInSuggestion =
1816
+ linter
1817
+ .verify(
1818
+ codeWithAppliedSuggestion,
1819
+ result.configs,
1820
+ result.filename,
1821
+ )
1822
+ .find(m => m.fatal);
1823
+
1824
+ assert(
1825
+ !errorMessageInSuggestion,
1826
+ [
1827
+ "A fatal parsing error occurred in suggestion fix.",
1828
+ `Error: ${errorMessageInSuggestion && errorMessageInSuggestion.message}`,
1829
+ "Suggestion output:",
1830
+ codeWithAppliedSuggestion,
1831
+ ].join("\n"),
1566
1832
  );
1567
- }
1568
-
1569
- assert.ok(
1570
- hasOwnProperty(
1571
- expectedSuggestion,
1572
- "output",
1573
- ),
1574
- `${suggestionPrefix} The "output" property is required.`,
1575
- );
1576
- const codeWithAppliedSuggestion =
1577
- SourceCodeFixer.applyFixes(
1578
- item.code,
1579
- [actualSuggestion],
1580
- ).output;
1581
1833
 
1582
- // Verify if suggestion fix makes a syntax error or not.
1583
- const errorMessageInSuggestion =
1584
- linter
1585
- .verify(
1586
- codeWithAppliedSuggestion,
1587
- result.configs,
1588
- result.filename,
1589
- )
1590
- .find(m => m.fatal);
1591
-
1592
- assert(
1593
- !errorMessageInSuggestion,
1594
- [
1595
- "A fatal parsing error occurred in suggestion fix.",
1596
- `Error: ${errorMessageInSuggestion && errorMessageInSuggestion.message}`,
1597
- "Suggestion output:",
1834
+ assert.strictEqual(
1598
1835
  codeWithAppliedSuggestion,
1599
- ].join("\n"),
1600
- );
1601
-
1602
- assert.strictEqual(
1603
- codeWithAppliedSuggestion,
1604
- expectedSuggestion.output,
1605
- `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`,
1606
- );
1607
- assert.notStrictEqual(
1608
- expectedSuggestion.output,
1609
- item.code,
1610
- `The output of a suggestion should differ from the original source code for suggestion at index: ${index} on error with message: "${message.message}"`,
1611
- );
1612
- },
1613
- );
1614
- } else {
1615
- assert.fail(
1616
- "Test error object property 'suggestions' should be an array or a number",
1617
- );
1836
+ expectedSuggestion.output,
1837
+ `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`,
1838
+ );
1839
+ assert.notStrictEqual(
1840
+ expectedSuggestion.output,
1841
+ item.code,
1842
+ `The output of a suggestion should differ from the original source code for suggestion at index: ${index} on error with message: "${message.message}"`,
1843
+ );
1844
+ },
1845
+ );
1846
+ } else {
1847
+ assert.fail(
1848
+ "Test error object property 'suggestions' should be an array or a number",
1849
+ );
1850
+ }
1618
1851
  }
1619
1852
  }
1620
1853
  }
1854
+ } catch (error) {
1855
+ if (error instanceof Error) {
1856
+ error.errorIndex = i;
1857
+ }
1858
+ throw error;
1621
1859
  }
1622
1860
  }
1623
1861
  }
@@ -1662,7 +1900,7 @@ class RuleTester {
1662
1900
  if (test.valid.length > 0) {
1663
1901
  this.constructor.describe("valid", () => {
1664
1902
  const seenTestCases = new Set();
1665
- test.valid.forEach(valid => {
1903
+ test.valid.forEach((valid, index) => {
1666
1904
  const item = normalizeTestCase(valid);
1667
1905
  this.constructor[valid.only ? "itOnly" : "it"](
1668
1906
  sanitize(item.name || item.code),
@@ -1671,6 +1909,21 @@ class RuleTester {
1671
1909
  runHook(item, "before");
1672
1910
  assertValidTestCase(item, seenTestCases);
1673
1911
  testValidTemplate(item);
1912
+ } catch (error) {
1913
+ if (error instanceof Error) {
1914
+ error.scenarioType = "valid";
1915
+ error.scenarioIndex = index;
1916
+ error.stack = error.stack.replace(
1917
+ /^ +at /mu,
1918
+ [
1919
+ ` roughly at RuleTester.run.valid[${index}] (${estimateTestLocation(`valid[${index}]`)})`,
1920
+ ` roughly at RuleTester.run.valid (${estimateTestLocation("valid")})`,
1921
+ ` at RuleTester.run (${estimateTestLocation("root")})`,
1922
+ " at ",
1923
+ ].join("\n"),
1924
+ );
1925
+ }
1926
+ throw error;
1674
1927
  } finally {
1675
1928
  runHook(item, "after");
1676
1929
  }
@@ -1683,7 +1936,7 @@ class RuleTester {
1683
1936
  if (test.invalid.length > 0) {
1684
1937
  this.constructor.describe("invalid", () => {
1685
1938
  const seenTestCases = new Set();
1686
- test.invalid.forEach(invalid => {
1939
+ test.invalid.forEach((invalid, index) => {
1687
1940
  const item = normalizeTestCase(invalid);
1688
1941
  this.constructor[item.only ? "itOnly" : "it"](
1689
1942
  sanitize(item.name || item.code),
@@ -1697,6 +1950,28 @@ class RuleTester {
1697
1950
  test.assertionOptions,
1698
1951
  );
1699
1952
  testInvalidTemplate(item);
1953
+ } catch (error) {
1954
+ if (error instanceof Error) {
1955
+ error.scenarioType = "invalid";
1956
+ error.scenarioIndex = index;
1957
+ const errorIndex = error.errorIndex;
1958
+ error.stack = error.stack.replace(
1959
+ /^ +at /mu,
1960
+ [
1961
+ ...(typeof errorIndex ===
1962
+ "number"
1963
+ ? [
1964
+ ` roughly at RuleTester.run.invalid[${index}].error[${errorIndex}] (${estimateTestLocation(`invalid[${index}].errors[${errorIndex}]`)})`,
1965
+ ]
1966
+ : []),
1967
+ ` roughly at RuleTester.run.invalid[${index}] (${estimateTestLocation(`invalid[${index}]`)})`,
1968
+ ` roughly at RuleTester.run.invalid (${estimateTestLocation("invalid")})`,
1969
+ ` at RuleTester.run (${estimateTestLocation("root")})`,
1970
+ " at ",
1971
+ ].join("\n"),
1972
+ );
1973
+ }
1974
+ throw error;
1700
1975
  } finally {
1701
1976
  runHook(item, "after");
1702
1977
  }