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.
- package/README.md +1 -1
- package/lib/languages/js/index.js +0 -1
- package/lib/linter/linter.js +14 -22
- package/lib/rule-tester/rule-tester.js +562 -287
- package/lib/rules/max-params.js +29 -10
- package/lib/types/index.d.ts +51 -12
- package/lib/types/rules.d.ts +5 -0
- package/package.json +5 -5
- package/lib/linter/rules.js +0 -71
|
@@ -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 {
|
|
1221
|
-
|
|
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
|
-
|
|
1290
|
-
|
|
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
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
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 (
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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
|
-
|
|
1318
|
-
|
|
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
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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
|
-
|
|
1394
|
-
|
|
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
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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 (
|
|
1438
|
-
assert.
|
|
1439
|
-
|
|
1440
|
-
error.
|
|
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
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
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
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
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
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1678
|
+
const actualSuggestion =
|
|
1679
|
+
message.suggestions[index];
|
|
1680
|
+
const suggestionPrefix = `Error Suggestion at index ${index}:`;
|
|
1466
1681
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
expectedSuggestion,
|
|
1470
|
-
"desc",
|
|
1471
|
-
)
|
|
1472
|
-
) {
|
|
1473
|
-
assert.ok(
|
|
1474
|
-
!hasOwnProperty(
|
|
1682
|
+
if (
|
|
1683
|
+
hasOwnProperty(
|
|
1475
1684
|
expectedSuggestion,
|
|
1476
|
-
"
|
|
1477
|
-
)
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
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
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1516
|
-
getUnsubstitutedMessagePlaceholders(
|
|
1517
|
-
actualSuggestion.desc,
|
|
1730
|
+
const rawSuggestionMessage =
|
|
1518
1731
|
rule.meta.messages[
|
|
1519
1732
|
expectedSuggestion
|
|
1520
1733
|
.messageId
|
|
1521
|
-
]
|
|
1522
|
-
|
|
1523
|
-
|
|
1734
|
+
];
|
|
1735
|
+
const unsubstitutedPlaceholders =
|
|
1736
|
+
getUnsubstitutedMessagePlaceholders(
|
|
1737
|
+
actualSuggestion.desc,
|
|
1738
|
+
rawSuggestionMessage,
|
|
1739
|
+
expectedSuggestion.data,
|
|
1740
|
+
);
|
|
1524
1741
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
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
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
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
|
}
|