Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rule-tester): port checkDuplicateTestCases from ESLint #9026

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
620416d
port checkDuplicate from ESLint
abrahamguo Apr 29, 2024
72faa0d
lint
abrahamguo Apr 29, 2024
2870725
revert lib change
abrahamguo Apr 29, 2024
c0da154
move serialization to utils
abrahamguo Apr 29, 2024
4cab72b
add issuenum for Object.hasOwn
abrahamguo Apr 29, 2024
9cacae9
Merge branch 'main' of github.com:abrahamguo/typescript-eslint into d…
abrahamguo Apr 30, 2024
b4e0d87
test cases for duplication
abrahamguo Apr 30, 2024
c31e1be
dedupe some bulk tests
abrahamguo Apr 30, 2024
1f1a290
Merge branch 'main' of github.com:abrahamguo/typescript-eslint into d…
abrahamguo May 2, 2024
c2db7bd
dedupe naming-convention
abrahamguo May 2, 2024
646aa94
make utility
abrahamguo May 2, 2024
0a02368
prefer-nullish-coalescing
abrahamguo May 2, 2024
618307d
prefer-readonly-parameter-types
abrahamguo May 2, 2024
88efa1b
member-ordering
abrahamguo May 2, 2024
3e8231d
sort
abrahamguo May 2, 2024
fcae2d0
consistent-type-assertion
abrahamguo May 3, 2024
dd112cf
add comment
abrahamguo May 3, 2024
73c7a9d
cleanup
abrahamguo May 3, 2024
ddd20bb
fix formatting
abrahamguo May 3, 2024
ac89ac3
add explanation
abrahamguo May 3, 2024
146ae8f
func-call-spacing
abrahamguo May 3, 2024
b9617b5
Merge branch 'main' of github.com:abrahamguo/typescript-eslint into d…
abrahamguo May 3, 2024
ba8c6cc
remove naming-convention
abrahamguo May 3, 2024
6693ea3
remove debugging
abrahamguo May 3, 2024
4fe40ce
Merge branch 'main' of github.com:abrahamguo/typescript-eslint into d…
abrahamguo May 3, 2024
d136b04
Merge branch 'main' of github.com:abrahamguo/typescript-eslint into d…
abrahamguo May 28, 2024
3cf2411
change quotes
abrahamguo May 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/rule-tester/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@typescript-eslint/typescript-estree": "7.7.1",
"@typescript-eslint/utils": "7.7.1",
"ajv": "^6.12.6",
"json-stable-stringify-without-jsonify": "^1.0.1",
abrahamguo marked this conversation as resolved.
Show resolved Hide resolved
"lodash.merge": "4.6.2",
"semver": "^7.6.0"
},
Expand All @@ -59,6 +60,7 @@
"eslint": "^8.56.0"
},
"devDependencies": {
"@types/json-stable-stringify-without-jsonify": "^1.0.2",
"@types/lodash.merge": "4.6.9",
"@typescript-eslint/parser": "7.7.1",
"chai": "^4.4.1",
Expand Down
49 changes: 47 additions & 2 deletions packages/rule-tester/src/RuleTester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Linter } from '@typescript-eslint/utils/ts-eslint';
// we intentionally import from eslint here because we need to use the same class
// that ESLint uses, not our custom override typed version
import { SourceCode } from 'eslint';
import stringify from 'json-stable-stringify-without-jsonify';
import merge from 'lodash.merge';

import { TestFramework } from './TestFramework';
Expand All @@ -40,6 +41,7 @@ import { getRuleOptionsSchema } from './utils/getRuleOptionsSchema';
import { hasOwnProperty } from './utils/hasOwnProperty';
import { interpolate } from './utils/interpolate';
import { isReadonlyArray } from './utils/isReadonlyArray';
import { isSerializable } from './utils/serialization';
import * as SourceCodeFixer from './utils/SourceCodeFixer';
import {
emitLegacyRuleAPIWarning,
Expand Down Expand Up @@ -354,6 +356,9 @@ export class RuleTester extends TestFramework {
);
}

const seenValidTestCases = new Set<string>();
const seenInvalidTestCases = new Set<string>();

if (typeof rule === 'function') {
emitLegacyRuleAPIWarning(ruleName);
}
Expand Down Expand Up @@ -403,7 +408,12 @@ export class RuleTester extends TestFramework {
return valid.name;
})();
constructor[getTestMethod(valid)](sanitize(testName), () => {
this.#testValidTemplate(ruleName, rule, valid);
this.#testValidTemplate(
ruleName,
rule,
valid,
seenValidTestCases,
);
});
});
});
Expand All @@ -419,7 +429,12 @@ export class RuleTester extends TestFramework {
return invalid.name;
})();
constructor[getTestMethod(invalid)](sanitize(name), () => {
this.#testInvalidTemplate(ruleName, rule, invalid);
this.#testInvalidTemplate(
ruleName,
rule,
invalid,
seenInvalidTestCases,
);
});
});
});
Expand Down Expand Up @@ -631,6 +646,7 @@ export class RuleTester extends TestFramework {
ruleName: string,
rule: RuleModule<MessageIds, Options>,
itemIn: ValidTestCase<Options> | string,
seenValidTestCases: Set<string>,
): void {
const item: ValidTestCase<Options> =
typeof itemIn === 'object' ? itemIn : { code: itemIn };
Expand All @@ -646,6 +662,8 @@ export class RuleTester extends TestFramework {
);
}

checkDuplicateTestCase(item, seenValidTestCases);

const result = this.runRuleForItem(ruleName, rule, item);
const messages = result.messages;

Expand Down Expand Up @@ -673,6 +691,7 @@ export class RuleTester extends TestFramework {
ruleName: string,
rule: RuleModule<MessageIds, Options>,
item: InvalidTestCase<MessageIds, Options>,
seenInvalidTestCases: Set<string>,
): void {
assert.ok(
typeof item.code === 'string',
Expand All @@ -693,6 +712,8 @@ export class RuleTester extends TestFramework {
assert.fail('Invalid cases must have at least one error');
}

checkDuplicateTestCase(item, seenInvalidTestCases);

const ruleHasMetaMessages =
hasOwnProperty(rule, 'meta') && hasOwnProperty(rule.meta, 'messages');
const friendlyIDList = ruleHasMetaMessages
Expand Down Expand Up @@ -1030,6 +1051,30 @@ function assertASTDidntChange(beforeAST: unknown, afterAST: unknown): void {
assert.deepStrictEqual(beforeAST, afterAST, 'Rule should not modify AST.');
}

/**
abrahamguo marked this conversation as resolved.
Show resolved Hide resolved
* Check if this test case is a duplicate of one we have seen before.
*/
function checkDuplicateTestCase(
item: unknown,
seenTestCases: Set<unknown>,
): void {
if (!isSerializable(item)) {
/*
* If we can't serialize a test case (because it contains a function, RegExp, etc), skip the check.
* This might happen with properties like: options, plugins, settings, languageOptions.parser, languageOptions.parserOptions.
*/
return;
}

const serializedTestCase = stringify(item);

assert(
!seenTestCases.has(serializedTestCase),
'detected duplicate test case',
);
seenTestCases.add(serializedTestCase);
}

/**
* Asserts that the message matches its expected value. If the expected
* value is a regular expression, it is checked against the actual
Expand Down
42 changes: 42 additions & 0 deletions packages/rule-tester/src/utils/serialization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Check if a value is a primitive or plain object created by the Object constructor.
*/
function isSerializablePrimitiveOrPlainObject(val: unknown): boolean {
return (
// eslint-disable-next-line eqeqeq
val === null ||
typeof val === 'string' ||
typeof val === 'boolean' ||
typeof val === 'number' ||
(typeof val === 'object' && val.constructor === Object) ||
Array.isArray(val)
);
}

/**
* Check if a value is serializable.
* Functions or objects like RegExp cannot be serialized by JSON.stringify().
* Inspired by: https://stackoverflow.com/questions/30579940/reliable-way-to-check-if-objects-is-serializable-in-javascript
*/
export function isSerializable(val: unknown): boolean {
if (!isSerializablePrimitiveOrPlainObject(val)) {
return false;
}
if (typeof val === 'object') {
const valAsObj = val as Record<string, unknown>;
for (const property in valAsObj) {
// TODO(#9028): use `Object.hasOwn` (used in eslint@9) once we upgrade to eslint@9
if (Object.prototype.hasOwnProperty.call(valAsObj, property)) {
if (!isSerializablePrimitiveOrPlainObject(valAsObj[property])) {
return false;
}
if (typeof valAsObj[property] === 'object') {
if (!isSerializable(valAsObj[property])) {
return false;
}
}
}
}
}
return true;
}
9 changes: 9 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5197,6 +5197,13 @@ __metadata:
languageName: node
linkType: hard

"@types/json-stable-stringify-without-jsonify@npm:^1.0.2":
version: 1.0.2
resolution: "@types/json-stable-stringify-without-jsonify@npm:1.0.2"
checksum: b8822ef38b1e845cca8151ef2baf5c99bc935364e94317b91eb1ffabb9280a0debd791b3b450f99e15bd121c0ecbecae926095b9f6b169e95a4659b4eb59f90f
languageName: node
linkType: hard

"@types/json5@npm:^0.0.29":
version: 0.0.29
resolution: "@types/json5@npm:0.0.29"
Expand Down Expand Up @@ -5663,12 +5670,14 @@ __metadata:
version: 0.0.0-use.local
resolution: "@typescript-eslint/rule-tester@workspace:packages/rule-tester"
dependencies:
"@types/json-stable-stringify-without-jsonify": ^1.0.2
"@types/lodash.merge": 4.6.9
"@typescript-eslint/parser": 7.7.1
"@typescript-eslint/typescript-estree": 7.7.1
"@typescript-eslint/utils": 7.7.1
ajv: ^6.12.6
chai: ^4.4.1
json-stable-stringify-without-jsonify: ^1.0.1
lodash.merge: 4.6.2
mocha: ^10.4.0
semver: ^7.6.0
Expand Down