Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nasty-pumpkins-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-eslint/eslint-plugin': minor
---

[New rule] Compare operation/fragment name to the file name
5 changes: 5 additions & 0 deletions .changeset/swift-cobras-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-eslint/eslint-plugin': patch
---

NEW PLUGIN: Compare operation/fragment name to the file name
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Each rule has emojis denoting:
| [known-type-names](rules/known-type-names.md) | A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema. |     🔮 | |
| [lone-anonymous-operation](rules/lone-anonymous-operation.md) | A GraphQL document is only valid if when it contains an anonymous operation (the query short-hand) that it contains only that one operation definition. |     🔮 | |
| [lone-schema-definition](rules/lone-schema-definition.md) | A GraphQL document is only valid if it contains only one schema definition. |     🔮 | |
| [match-document-filename](rules/match-document-filename.md) | This rule allows you to enforce that the file name should match the operation name |     🚀 | |
| [naming-convention](rules/naming-convention.md) | Require names to follow specified conventions. |     🚀 | |
| [no-anonymous-operations](rules/no-anonymous-operations.md) | Require name for your GraphQL operations. This is useful since most GraphQL client libraries are using the operation name for caching purposes. |     🚀 | |
| [no-case-insensitive-enum-values-duplicates](rules/no-case-insensitive-enum-values-duplicates.md) | |     🚀 | 🔧 |
Expand Down
152 changes: 152 additions & 0 deletions docs/rules/match-document-filename.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# `match-document-filename`

- Category: `Best Practices`
- Rule name: `@graphql-eslint/match-document-filename`
- Requires GraphQL Schema: `false` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
- Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)

This rule allows you to enforce that the file name should match the operation name

## Usage Examples

### Correct

```graphql
# eslint @graphql-eslint/match-document-filename: ['error', { fileExtension: '.gql' }]

# user.gql
type User {
id: ID!
}
```

### Correct

```graphql
# eslint @graphql-eslint/match-document-filename: ['error', { query: 'snake_case' }]

# user_by_id.gql
query UserById {
userById(id: 5) {
id
name
fullName
}
}
```

### Correct

```graphql
# eslint @graphql-eslint/match-document-filename: ['error', { fragment: { style: 'kebab-case', suffix: '.fragment' } }]

# user-fields.fragment.gql
fragment user_fields on User {
id
email
}
```

### Correct

```graphql
# eslint @graphql-eslint/match-document-filename: ['error', { mutation: { style: 'PascalCase', suffix: 'Mutation' } }]

# DeleteUserMutation.gql
mutation DELETE_USER {
deleteUser(id: 5)
}
```

### Incorrect

```graphql
# eslint @graphql-eslint/match-document-filename: ['error', { fileExtension: '.graphql' }]

# post.gql
type Post {
id: ID!
}
```

### Incorrect

```graphql
# eslint @graphql-eslint/match-document-filename: ['error', { query: 'PascalCase' }]

# user-by-id.gql
query UserById {
userById(id: 5) {
id
name
fullName
}
}
```

## Config Schema

### (array)

The schema defines an array with all elements of the type `object`.

The array object has the following properties:

#### `fileExtension` (string, enum)

This element must be one of the following enum values:

* `.gql`
* `.graphql`

#### `query`

The object must be one of the following types:

* `asString`
* `asObject`

#### `mutation`

The object must be one of the following types:

* `asString`
* `asObject`

#### `subscription`

The object must be one of the following types:

* `asString`
* `asObject`

#### `fragment`

The object must be one of the following types:

* `asString`
* `asObject`

---

# Sub Schemas

The schema defines the following additional types:

## `asString` (string)

One of: `camelCase`, `PascalCase`, `snake_case`, `UPPER_CASE`, `kebab-case`

## `asObject` (object)

Properties of the `asObject` object:

### `style` (string, enum)

This element must be one of the following enum values:

* `camelCase`
* `PascalCase`
* `snake_case`
* `UPPER_CASE`
* `kebab-case`
6 changes: 4 additions & 2 deletions packages/plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,16 @@
"@graphql-tools/import": "^6.3.1",
"@graphql-tools/utils": "^8.0.2",
"graphql-config": "^4.0.1",
"graphql-depth-limit": "1.1.0"
"graphql-depth-limit": "1.1.0",
"lodash.lowercase": "^4.3.0"
},
"devDependencies": {
"@types/eslint": "7.28.0",
"@types/graphql-depth-limit": "1.1.2",
"bob-the-bundler": "1.5.1",
"graphql": "15.5.3",
"typescript": "4.4.2"
"typescript": "4.4.2",
"@types/lodash.camelcase": "^4.3.6"
},
"peerDependencies": {
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
Expand Down
74 changes: 33 additions & 41 deletions packages/plugin/src/rules/avoid-operation-name-prefix.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GraphQLESLintRule, GraphQLESLintRuleContext } from '../types';
import { GraphQLESLintRule } from '../types';
import { GraphQLESTreeNode } from '../estree-parser/estree-ast';
import { OperationDefinitionNode, FragmentDefinitionNode } from 'graphql';

Expand All @@ -11,41 +11,6 @@ export type AvoidOperationNamePrefixConfig = [

const AVOID_OPERATION_NAME_PREFIX = 'AVOID_OPERATION_NAME_PREFIX';

function verifyRule(
context: GraphQLESLintRuleContext<AvoidOperationNamePrefixConfig>,
node: GraphQLESTreeNode<OperationDefinitionNode> | GraphQLESTreeNode<FragmentDefinitionNode>
) {
const config = context.options[0] || { keywords: [], caseSensitive: false };
const caseSensitive = config.caseSensitive;
const keywords = config.keywords || [];

if (node && node.name && node.name.value !== '') {
for (const keyword of keywords) {
const testKeyword = caseSensitive ? keyword : keyword.toLowerCase();
const testName = caseSensitive ? node.name.value : node.name.value.toLowerCase();

if (testName.startsWith(testKeyword)) {
context.report({
loc: {
start: {
line: node.name.loc.start.line,
column: node.name.loc.start.column - 1,
},
end: {
line: node.name.loc.start.line,
column: node.name.loc.start.column + testKeyword.length - 1,
},
},
data: {
invalidPrefix: keyword,
},
messageId: AVOID_OPERATION_NAME_PREFIX,
});
}
}
}
}

const rule: GraphQLESLintRule<AvoidOperationNamePrefixConfig> = {
meta: {
type: 'suggestion',
Expand Down Expand Up @@ -100,11 +65,38 @@ const rule: GraphQLESLintRule<AvoidOperationNamePrefixConfig> = {
},
create(context) {
return {
OperationDefinition(node) {
verifyRule(context, node);
},
FragmentDefinition(node) {
verifyRule(context, node);
'OperationDefinition, FragmentDefinition'(
node: GraphQLESTreeNode<OperationDefinitionNode | FragmentDefinitionNode>
) {
const config = context.options[0] || { keywords: [], caseSensitive: false };
const caseSensitive = config.caseSensitive;
const keywords = config.keywords || [];

if (node && node.name && node.name.value !== '') {
for (const keyword of keywords) {
const testKeyword = caseSensitive ? keyword : keyword.toLowerCase();
const testName = caseSensitive ? node.name.value : node.name.value.toLowerCase();

if (testName.startsWith(testKeyword)) {
context.report({
loc: {
start: {
line: node.name.loc.start.line,
column: node.name.loc.start.column - 1,
},
end: {
line: node.name.loc.start.line,
column: node.name.loc.start.column + testKeyword.length - 1,
},
},
data: {
invalidPrefix: keyword,
},
messageId: AVOID_OPERATION_NAME_PREFIX,
});
}
}
}
},
};
},
Expand Down
65 changes: 29 additions & 36 deletions packages/plugin/src/rules/avoid-typename-prefix.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,14 @@
import { FieldDefinitionNode } from 'graphql';
import {
InterfaceTypeDefinitionNode,
InterfaceTypeExtensionNode,
ObjectTypeDefinitionNode,
ObjectTypeExtensionNode,
} from 'graphql';
import { GraphQLESTreeNode } from '../estree-parser';
import { GraphQLESLintRule, GraphQLESLintRuleContext } from '../types';
import { GraphQLESLintRule } from '../types';

const AVOID_TYPENAME_PREFIX = 'AVOID_TYPENAME_PREFIX';

function checkNode(
context: GraphQLESLintRuleContext<any>,
typeName: string,
fields: GraphQLESTreeNode<FieldDefinitionNode>[]
) {
const lowerTypeName = (typeName || '').toLowerCase();

for (const field of fields) {
const fieldName = field.name.value || '';

if (fieldName && lowerTypeName && fieldName.toLowerCase().startsWith(lowerTypeName)) {
context.report({
node: field.name,
data: {
fieldName,
typeName,
},
messageId: AVOID_TYPENAME_PREFIX,
});
}
}
}

const rule: GraphQLESLintRule = {
meta: {
type: 'suggestion',
Expand Down Expand Up @@ -60,17 +42,28 @@ const rule: GraphQLESLintRule = {
},
create(context) {
return {
ObjectTypeDefinition(node) {
checkNode(context, node.name.value, node.fields);
},
ObjectTypeExtension(node) {
checkNode(context, node.name.value, node.fields);
},
InterfaceTypeDefinition(node) {
checkNode(context, node.name.value, node.fields);
},
InterfaceTypeExtension(node) {
checkNode(context, node.name.value, node.fields);
'ObjectTypeDefinition, ObjectTypeExtension, InterfaceTypeDefinition, InterfaceTypeExtension'(
node: GraphQLESTreeNode<
ObjectTypeDefinitionNode | ObjectTypeExtensionNode | InterfaceTypeDefinitionNode | InterfaceTypeExtensionNode
>
) {
const typeName = node.name.value;
const lowerTypeName = (typeName || '').toLowerCase();

for (const field of node.fields) {
const fieldName = field.name.value || '';

if (fieldName && lowerTypeName && fieldName.toLowerCase().startsWith(lowerTypeName)) {
context.report({
node: field.name,
data: {
fieldName,
typeName,
},
messageId: AVOID_TYPENAME_PREFIX,
});
}
}
},
};
},
Expand Down
Loading