Skip to content

Commit bb4a9ab

Browse files
author
Dimitri POSTOLOV
authored
feat: add snapshots for report location (#733)
1 parent 7fab078 commit bb4a9ab

37 files changed

+32474
-242
lines changed

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ module.exports = {
99
...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/' }),
1010
'@eslint/eslintrc/universal': '@eslint/eslintrc/dist/eslintrc-universal.cjs',
1111
},
12+
snapshotSerializers: ['jest-snapshot-serializer-raw/always'],
1213
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"eslint-plugin-unicorn": "37.0.1",
4646
"husky": "7.0.4",
4747
"jest": "27.3.1",
48+
"jest-snapshot-serializer-raw": "^1.2.0",
4849
"json-schema-to-markdown": "1.1.1",
4950
"lint-staged": "11.2.6",
5051
"patch-package": "6.4.7",

packages/plugin/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
"lodash.lowercase": "^4.3.0"
3636
},
3737
"devDependencies": {
38+
"@babel/code-frame": "^7.15.8",
39+
"@types/babel__code-frame": "^7.0.3",
3840
"@types/eslint": "7.28.2",
3941
"@types/graphql-depth-limit": "1.1.3",
4042
"@types/lodash.lowercase": "4.3.6",
@@ -48,6 +50,8 @@
4850
"buildOptions": {
4951
"input": "./src/index.ts",
5052
"external": [
53+
"@babel/code-frame",
54+
"eslint",
5155
"graphql",
5256
"graphql/validation/rules/ExecutableDefinitionsRule",
5357
"graphql/validation/rules/FieldsOnCorrectTypeRule",

packages/plugin/src/rules/match-document-filename.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ const rule: GraphQLESLintRule<MatchDocumentFilenameRuleConfig> = {
172172
Document(documentNode) {
173173
if (options.fileExtension && options.fileExtension !== fileExtension) {
174174
context.report({
175-
node: documentNode,
175+
// Report on first character
176+
loc: { column: 0, line: 1 },
176177
messageId: MATCH_EXTENSION,
177178
data: {
178179
fileExtension,
@@ -215,7 +216,8 @@ const rule: GraphQLESLintRule<MatchDocumentFilenameRuleConfig> = {
215216

216217
if (expectedFilename !== filenameWithExtension) {
217218
context.report({
218-
node: documentNode,
219+
// Report on first character
220+
loc: { column: 0, line: 1 },
219221
messageId: MATCH_STYLE,
220222
data: {
221223
expectedFilename,

packages/plugin/src/testkit.ts

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import { RuleTester } from 'eslint';
21
import { readFileSync } from 'fs';
3-
import { ASTKindToNode } from 'graphql';
42
import { resolve } from 'path';
3+
import { RuleTester, AST, Linter, Rule } from 'eslint';
4+
import { ASTKindToNode } from 'graphql';
5+
import { codeFrameColumns } from '@babel/code-frame';
56
import { GraphQLESTreeNode } from './estree-parser';
67
import { GraphQLESLintRule, ParserOptions } from './types';
78

89
export type GraphQLESLintRuleListener<WithTypeInfo extends boolean = false> = {
910
[K in keyof ASTKindToNode]?: (node: GraphQLESTreeNode<ASTKindToNode[K], WithTypeInfo>) => void;
10-
} &
11-
Record<string, any>;
11+
} & Record<string, any>;
1212

1313
export type GraphQLValidTestCase<Options> = Omit<RuleTester.ValidTestCase, 'options' | 'parserOptions'> & {
1414
options?: Options;
@@ -20,15 +20,22 @@ export type GraphQLInvalidTestCase<T> = GraphQLValidTestCase<T> & {
2020
output?: string | null;
2121
};
2222

23-
export class GraphQLRuleTester extends require('eslint').RuleTester {
23+
export class GraphQLRuleTester extends RuleTester {
24+
config: {
25+
parser: string;
26+
parserOptions: ParserOptions;
27+
};
28+
2429
constructor(parserOptions: ParserOptions = {}) {
25-
super({
30+
const config = {
2631
parser: require.resolve('@graphql-eslint/eslint-plugin'),
2732
parserOptions: {
2833
...parserOptions,
2934
skipGraphQLConfig: true,
3035
},
31-
});
36+
};
37+
super(config);
38+
this.config = config;
3239
}
3340

3441
fromMockFile(path: string): string {
@@ -43,6 +50,89 @@ export class GraphQLRuleTester extends require('eslint').RuleTester {
4350
invalid: GraphQLInvalidTestCase<Config>[];
4451
}
4552
): void {
46-
super.run(name, rule, tests);
53+
super.run(name, rule as Rule.RuleModule, tests);
54+
55+
// Skip snapshot testing if `expect` variable is not defined
56+
if (typeof expect === 'undefined') {
57+
return;
58+
}
59+
60+
const linter = new Linter();
61+
linter.defineRule(name, rule as Rule.RuleModule);
62+
63+
for (const testCase of tests.invalid) {
64+
const verifyConfig = getVerifyConfig(name, this.config, testCase);
65+
defineParser(linter, verifyConfig.parser);
66+
67+
const { code, filename } = testCase;
68+
69+
const messages = linter.verify(code, verifyConfig, { filename });
70+
71+
for (const message of messages) {
72+
if (message.fatal) {
73+
throw new Error(message.message);
74+
}
75+
76+
const messageForSnapshot = visualizeEslintMessage(code, message);
77+
// eslint-disable-next-line no-undef
78+
expect(messageForSnapshot).toMatchSnapshot();
79+
}
80+
}
4781
}
4882
}
83+
84+
function getVerifyConfig(ruleId: string, testerConfig, testCase) {
85+
const { options, parserOptions, parser = testerConfig.parser } = testCase;
86+
87+
return {
88+
...testerConfig,
89+
parser,
90+
parserOptions: {
91+
...testerConfig.parserOptions,
92+
...parserOptions,
93+
},
94+
rules: {
95+
[ruleId]: ['error', ...(Array.isArray(options) ? options : [])],
96+
},
97+
};
98+
}
99+
100+
const parsers = new WeakMap();
101+
102+
function defineParser(linter: Linter, parser: string): void {
103+
if (!parser) {
104+
return;
105+
}
106+
if (!parsers.has(linter)) {
107+
parsers.set(linter, new Set());
108+
}
109+
110+
const defined = parsers.get(linter);
111+
if (!defined.has(parser)) {
112+
defined.add(parser);
113+
linter.defineParser(parser, require(parser));
114+
}
115+
}
116+
117+
function visualizeEslintMessage(text: string, result: Linter.LintMessage): string {
118+
const { line, column, endLine, endColumn, message } = result;
119+
const location: Partial<AST.SourceLocation> = {
120+
start: {
121+
line,
122+
column,
123+
},
124+
};
125+
126+
if (typeof endLine === 'number' && typeof endColumn === 'number') {
127+
location.end = {
128+
line: endLine,
129+
column: endColumn,
130+
};
131+
}
132+
133+
return codeFrameColumns(text, location as AST.SourceLocation, {
134+
linesAbove: Number.POSITIVE_INFINITY,
135+
linesBelow: Number.POSITIVE_INFINITY,
136+
message,
137+
});
138+
}

packages/plugin/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export type RuleDocsInfo<T> = Rule.RuleMetaData & {
5959
usage?: T;
6060
}[];
6161
optionsForConfig?: T;
62-
graphQLJSRuleName?: string
62+
graphQLJSRuleName?: string;
6363
};
6464
};
6565

0 commit comments

Comments
 (0)