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.
- package/README.md +2 -2
- package/lib/languages/js/index.js +0 -1
- package/lib/linter/linter.js +14 -22
- package/lib/rule-tester/rule-tester.js +569 -287
- package/lib/rules/max-params.js +29 -10
- package/lib/rules/no-shadow.js +89 -24
- package/lib/types/index.d.ts +54 -14
- package/lib/types/rules.d.ts +5 -0
- package/package.json +7 -7
- 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,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 {
|
|
1221
|
-
|
|
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
|
-
|
|
1290
|
-
|
|
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
|
-
|
|
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.`,
|
|
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 (
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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
|
-
|
|
1318
|
-
|
|
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
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
-
|
|
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];
|
|
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
|
-
|
|
1394
|
-
|
|
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
|
-
|
|
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`,
|
|
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 (
|
|
1438
|
-
assert.
|
|
1439
|
-
|
|
1440
|
-
error.
|
|
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
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
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
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
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
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1685
|
+
const actualSuggestion =
|
|
1686
|
+
message.suggestions[index];
|
|
1687
|
+
const suggestionPrefix = `Error Suggestion at index ${index}:`;
|
|
1466
1688
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
expectedSuggestion,
|
|
1470
|
-
"desc",
|
|
1471
|
-
)
|
|
1472
|
-
) {
|
|
1473
|
-
assert.ok(
|
|
1474
|
-
!hasOwnProperty(
|
|
1689
|
+
if (
|
|
1690
|
+
hasOwnProperty(
|
|
1475
1691
|
expectedSuggestion,
|
|
1476
|
-
"
|
|
1477
|
-
)
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1516
|
-
getUnsubstitutedMessagePlaceholders(
|
|
1517
|
-
actualSuggestion.desc,
|
|
1737
|
+
const rawSuggestionMessage =
|
|
1518
1738
|
rule.meta.messages[
|
|
1519
1739
|
expectedSuggestion
|
|
1520
1740
|
.messageId
|
|
1521
|
-
]
|
|
1522
|
-
|
|
1523
|
-
|
|
1741
|
+
];
|
|
1742
|
+
const unsubstitutedPlaceholders =
|
|
1743
|
+
getUnsubstitutedMessagePlaceholders(
|
|
1744
|
+
actualSuggestion.desc,
|
|
1745
|
+
rawSuggestionMessage,
|
|
1746
|
+
expectedSuggestion.data,
|
|
1747
|
+
);
|
|
1524
1748
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}".`,
|
|
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
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
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
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
);
|
|
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
|
}
|