Skip to content

Commit 01ade2e

Browse files
author
Dimitri POSTOLOV
authored
2️⃣ Fix unique-fragment-name and no-unused-fragments rules, skip graphql #import comments for siblings (#511)
1 parent 4c14da7 commit 01ade2e

13 files changed

+162
-54
lines changed

.changeset/witty-beans-turn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-eslint/eslint-plugin': patch
3+
---
4+
5+
fix unique-fragment-name and no-unused-fragments rule

docs/rules/no-unused-fragments.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
- Category: `Validation`
44
- Rule name: `@graphql-eslint/no-unused-fragments`
55
- Requires GraphQL Schema: `true` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
6-
- Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
6+
- Requires GraphQL Operations: `true` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
77

88
A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.
99

packages/plugin/package.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,23 @@
1616
"prepack": "bob prepack"
1717
},
1818
"dependencies": {
19-
"@graphql-tools/utils": "~7.10.0",
20-
"@graphql-tools/load": "~6.2.0",
19+
"@graphql-tools/code-file-loader": "~6.3.0",
2120
"@graphql-tools/graphql-file-loader": "~6.2.0",
21+
"@graphql-tools/graphql-tag-pluck": "~6.5.0",
22+
"@graphql-tools/import": "^6.3.1",
2223
"@graphql-tools/json-file-loader": "~6.2.6",
23-
"@graphql-tools/code-file-loader": "~6.3.0",
24+
"@graphql-tools/load": "~6.2.0",
2425
"@graphql-tools/url-loader": "~6.10.0",
25-
"@graphql-tools/graphql-tag-pluck": "~6.5.0",
26+
"@graphql-tools/utils": "~7.10.0",
2627
"graphql-config": "^3.2.0",
2728
"graphql-depth-limit": "1.1.0"
2829
},
2930
"devDependencies": {
30-
"@types/graphql-depth-limit": "1.1.2",
3131
"@types/eslint": "7.2.13",
32+
"@types/graphql-depth-limit": "1.1.2",
3233
"bob-the-bundler": "1.2.1",
33-
"graphql-config": "3.3.0",
3434
"graphql": "15.5.1",
35+
"graphql-config": "3.3.0",
3536
"typescript": "4.3.5"
3637
},
3738
"peerDependencies": {

packages/plugin/src/rules/graphql-js-validation.ts

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { validate, GraphQLSchema, DocumentNode, ASTNode, ValidationRule } from 'graphql';
22
import { validateSDL } from 'graphql/validation/validate';
3-
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
4-
import fs from 'fs';
3+
import { parseImportLine, processImport } from '@graphql-tools/import';
4+
import { existsSync } from 'fs';
5+
import { join, dirname } from 'path';
56
import { GraphQLESLintRule, GraphQLESLintRuleContext } from '../types';
6-
import { requireGraphQLSchemaFromContext } from '../utils';
7+
import { requireGraphQLSchemaFromContext, requireSiblingsOperations } from '../utils';
78
import { GraphQLESTreeNode } from '../estree-parser';
89

910
function extractRuleName(stack: string | undefined): string | null {
@@ -24,7 +25,7 @@ export function validateDoc(
2425
rules: ReadonlyArray<ValidationRule>,
2526
ruleName: string | null = null
2627
): void {
27-
if (documentNode && documentNode.definitions && documentNode.definitions.length > 0) {
28+
if (documentNode?.definitions?.length > 0) {
2829
try {
2930
const validationErrors = schema ? validate(schema, documentNode, rules) : validateSDL(documentNode, null, rules);
3031

@@ -69,15 +70,15 @@ const validationToRule = (
6970
}
7071

7172
const requiresSchema = docs.requiresSchema ?? true;
72-
73+
const requiresSiblings = docs.requiresSiblings ?? false;
7374
return {
7475
[name]: {
7576
meta: {
7677
docs: {
7778
...docs,
7879
category: 'Validation',
7980
requiresSchema,
80-
requiresSiblings: false,
81+
requiresSiblings,
8182
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${name}.md`,
8283
description: `${docs.description}\n\n> This rule is a wrapper around a \`graphql-js\` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/main/src/validation/rules/${ruleName}Rule.ts).`,
8384
},
@@ -96,9 +97,8 @@ const validationToRule = (
9697
const schema = requiresSchema ? requireGraphQLSchemaFromContext(name, context) : null;
9798

9899
let documentNode: DocumentNode;
99-
const filePath = context.getFilename();
100-
const isVirtualFile = !fs.existsSync(filePath);
101-
if (!isVirtualFile && getDocumentNode) {
100+
const isRealFile = existsSync(context.getFilename());
101+
if (isRealFile && getDocumentNode) {
102102
documentNode = getDocumentNode(context);
103103
}
104104
validateDoc(node, context, schema, documentNode || node.rawNode(), [ruleFn], ruleName);
@@ -180,10 +180,8 @@ export const GRAPHQL_JS_VALIDATIONS = Object.assign(
180180
if (!isGraphQLImportFile(code)) {
181181
return null;
182182
}
183-
// Import documents if file contains '#import' comments
184-
const fileLoader = new GraphQLFileLoader();
185-
const graphqlAst = fileLoader.handleFileContent(code, context.getFilename(), { noLocation: true });
186-
return graphqlAst.document;
183+
// Import documents because file contains '#import' comments
184+
return processImport(context.getFilename());
187185
}
188186
),
189187
validationToRule('known-type-names', 'KnownTypeNames', {
@@ -202,9 +200,45 @@ export const GRAPHQL_JS_VALIDATIONS = Object.assign(
202200
validationToRule('no-undefined-variables', 'NoUndefinedVariables', {
203201
description: `A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.`,
204202
}),
205-
validationToRule('no-unused-fragments', 'NoUnusedFragments', {
206-
description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`,
207-
}),
203+
validationToRule(
204+
'no-unused-fragments',
205+
'NoUnusedFragments',
206+
{
207+
description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`,
208+
requiresSiblings: true,
209+
},
210+
context => {
211+
const siblings = requireSiblingsOperations('no-unused-fragments', context);
212+
const documents = [...siblings.getOperations(), ...siblings.getFragments()]
213+
.filter(({ document }) => isGraphQLImportFile(document.loc.source.body))
214+
.map(({ filePath, document }) => ({
215+
filePath,
216+
code: document.loc.source.body,
217+
}));
218+
219+
const getParentNode = (filePath: string): DocumentNode | null => {
220+
for (const { filePath: docFilePath, code } of documents) {
221+
const isFileImported = code
222+
.split('\n')
223+
.filter(isGraphQLImportFile)
224+
.map(line => parseImportLine(line.replace('#', '')))
225+
.some(o => filePath === join(dirname(docFilePath), o.from));
226+
227+
if (!isFileImported) {
228+
continue;
229+
}
230+
// Import first file that import this file
231+
const document = processImport(docFilePath);
232+
// Import most top file that import this file
233+
return getParentNode(docFilePath) || document;
234+
}
235+
236+
return null;
237+
};
238+
239+
return getParentNode(context.getFilename());
240+
}
241+
),
208242
validationToRule('no-unused-variables', 'NoUnusedVariables', {
209243
description: `A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.`,
210244
}),

packages/plugin/src/rules/unique-fragment-name.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { relative } from 'path';
12
import { GraphQLESLintRule } from '../types';
23
import { normalizePath, requireSiblingsOperations } from '../utils';
34

@@ -51,19 +52,20 @@ const rule: GraphQLESLintRule<[], false> = {
5152
},
5253
},
5354
create(context) {
55+
const filename = context.getFilename();
5456
return {
55-
FragmentDefinition: node => {
57+
FragmentDefinition(node) {
5658
const fragmentName = node.name?.value;
5759
const siblings = requireSiblingsOperations('unique-fragment-name', context);
5860

5961
if (fragmentName) {
6062
const siblingFragments = siblings.getFragment(fragmentName);
63+
const conflictingFragments = siblingFragments.filter(f => {
64+
const isSameName = f.document.name?.value === fragmentName;
65+
const isSamePath = normalizePath(f.filePath) === normalizePath(filename);
6166

62-
const conflictingFragments = siblingFragments.filter(
63-
f =>
64-
f.document.name?.value === fragmentName &&
65-
normalizePath(f.filePath) !== normalizePath(context.getFilename())
66-
);
67+
return isSameName && !isSamePath;
68+
});
6769

6870
if (conflictingFragments.length > 0) {
6971
context.report({
@@ -80,7 +82,7 @@ const rule: GraphQLESLintRule<[], false> = {
8082
messageId: UNIQUE_FRAGMENT_NAME,
8183
data: {
8284
fragmentName,
83-
fragmentsSummary: conflictingFragments.map(f => `\t${f.filePath}`).join('\n'),
85+
fragmentsSummary: conflictingFragments.map(f => `\t${relative(process.cwd(), f.filePath)}`).join('\n'),
8486
},
8587
});
8688
}

packages/plugin/src/sibling-operations.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ function loadSiblings(baseDir: string, loadPaths: string[]): Source[] {
5050
return loadDocumentsSync(loadPaths, {
5151
cwd: baseDir,
5252
loaders: operationsLoaders,
53+
skipGraphQLImport: true,
5354
});
5455
}
5556

@@ -69,8 +70,10 @@ export function getSiblingOperations(options: ParserOptions, gqlConfig: GraphQLC
6970
} else {
7071
const projectForFile = gqlConfig.getProjectForFile(options.filePath);
7172

72-
if (projectForFile) {
73-
siblings = projectForFile.getDocumentsSync();
73+
if (projectForFile && projectForFile.documents.length > 0) {
74+
siblings = projectForFile.loadDocumentsSync(projectForFile.documents, {
75+
skipGraphQLImport: true,
76+
});
7477
operationsCache.set(fileDir, siblings);
7578
}
7679
}

packages/plugin/tests/known-fragment-names.spec.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,6 @@ import { join } from 'path';
22
import { GraphQLRuleTester } from '../src';
33
import { GRAPHQL_JS_VALIDATIONS } from '../src/rules/graphql-js-validation';
44

5-
const TEST_SCHEMA = /* GraphQL */ `
6-
type User {
7-
id: ID!
8-
firstName: String!
9-
}
10-
11-
type Query {
12-
user: User
13-
}
14-
`;
15-
165
const ruleTester = new GraphQLRuleTester();
176

187
ruleTester.runGraphQLTests('known-fragment-names', GRAPHQL_JS_VALIDATIONS['known-fragment-names'], {
@@ -21,7 +10,7 @@ ruleTester.runGraphQLTests('known-fragment-names', GRAPHQL_JS_VALIDATIONS['known
2110
filename: join(__dirname, 'mocks/user.graphql'),
2211
code: ruleTester.fromMockFile('user.graphql'),
2312
parserOptions: {
24-
schema: TEST_SCHEMA,
13+
schema: join(__dirname, 'mocks/user-schema.graphql'),
2514
},
2615
},
2716
],
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#import './user-fields.graphql'
2+
3+
fragment PostFields on Post {
4+
user {
5+
...UserFields
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#import "./post-fields.graphql"
2+
3+
query Post {
4+
post {
5+
...PostFields
6+
}
7+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
type User {
2+
id: ID!
3+
firstName: String!
4+
}
5+
6+
type Post {
7+
id: ID!
8+
user: User!
9+
}
10+
11+
type Query {
12+
user: User
13+
post: Post
14+
}

0 commit comments

Comments
 (0)