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