Skip to content

Commit 2df9134

Browse files
authored
NEW RULE: no-hashtag-description (#238)
1 parent d3ff768 commit 2df9134

File tree

8 files changed

+284
-3
lines changed

8 files changed

+284
-3
lines changed

.changeset/curvy-cameras-fetch.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': minor
3+
---
4+
5+
NEW RULE: no-hashtag-description

.changeset/sweet-colts-jam.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+
Fixed missing `loc` property when rawNode is called on Document node

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- [`unique-fragment-name`](./rules/unique-fragment-name.md)
66
- [`unique-operation-name`](./rules/unique-operation-name.md)
77
- [`validate-against-schema`](./rules/validate-against-schema.md)
8+
- [`no-hashtag-description`](./rules/no-hashtag-description.md)
89
- [`no-anonymous-operations`](./rules/no-anonymous-operations.md)
910
- [`no-operation-name-suffix`](./rules/no-operation-name-suffix.md)
1011
- [`require-deprecation-reason`](./rules/require-deprecation-reason.md)

docs/rules/no-hashtag-description.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# `no-hashtag-description`
2+
3+
- Category: `Best Practices`
4+
- Rule name: `@graphql-eslint/no-hashtag-description`
5+
- Requires GraphQL Schema: `false` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
6+
- Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
7+
8+
Requires to use """ or " for adding a GraphQL description instead of #.
9+
This rule allows you to use hashtag for comments, as long as it's not attached to a AST definition.
10+
11+
## Usage Examples
12+
13+
### Incorrect
14+
15+
```graphql
16+
# eslint @graphql-eslint/no-hashtag-description: ["error"]
17+
18+
# Represents a user
19+
type User {
20+
id: ID!
21+
name: String
22+
}
23+
```
24+
25+
### Correct
26+
27+
```graphql
28+
# eslint @graphql-eslint/no-hashtag-description: ["error"]
29+
30+
" Represents a user "
31+
type User {
32+
id: ID!
33+
name: String
34+
}
35+
```
36+
37+
### Correct
38+
39+
```graphql
40+
# eslint @graphql-eslint/no-hashtag-description: ["error"]
41+
42+
# This file defines the basic User type.
43+
# This comment is valid because it's not attached specifically to an AST object.
44+
45+
" Represents a user "
46+
type User {
47+
id: ID! # This one is also valid, since it comes after the AST object
48+
name: String
49+
}
50+
```

packages/plugin/src/estree-parser/converter.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { convertDescription, convertLocation, convertRange, extractCommentsFromAst } from './utils';
22
import { GraphQLESTreeNode, SafeGraphQLType } from './estree-ast';
3-
import { ASTNode, TypeNode, TypeInfo, visit, visitWithTypeInfo, Location, Kind } from 'graphql';
3+
import { ASTNode, TypeNode, TypeInfo, visit, visitWithTypeInfo, Location, Kind, DocumentNode } from 'graphql';
44
import { Comment } from 'estree';
55

66
export function convertToESTree<T extends ASTNode>(
@@ -71,7 +71,8 @@ const convertNode = (typeInfo?: TypeInfo) => <T extends ASTNode>(
7171
rawNode: () => {
7272
if (!parent || key === undefined) {
7373
if (node && (node as any).definitions) {
74-
return {
74+
return <DocumentNode>{
75+
loc: gqlLocation,
7576
kind: Kind.DOCUMENT,
7677
definitions: (node as any).definitions.map(d => d.rawNode()),
7778
};
@@ -96,7 +97,8 @@ const convertNode = (typeInfo?: TypeInfo) => <T extends ASTNode>(
9697
rawNode: () => {
9798
if (!parent || key === undefined) {
9899
if (node && (node as any).definitions) {
99-
return {
100+
return <DocumentNode>{
101+
loc: gqlLocation,
100102
kind: Kind.DOCUMENT,
101103
definitions: (node as any).definitions.map(d => d.rawNode()),
102104
};

packages/plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import inputName from './input-name';
1313
import uniqueFragmentName from './unique-fragment-name';
1414
import uniqueOperationName from './unique-operation-name';
1515
import noDeprecated from './no-deprecated';
16+
import noHashtagDescription from './no-hashtag-description';
1617
import { GraphQLESLintRule } from '../types';
1718
import { GRAPHQL_JS_VALIDATIONS } from './graphql-js-validation';
1819

@@ -21,6 +22,7 @@ export const rules: Record<string, GraphQLESLintRule> = {
2122
'unique-fragment-name': uniqueFragmentName,
2223
'unique-operation-name': uniqueOperationName,
2324
'validate-against-schema': validate,
25+
'no-hashtag-description': noHashtagDescription,
2426
'no-anonymous-operations': noAnonymousOperations,
2527
'no-operation-name-suffix': noOperationNameSuffix,
2628
'require-deprecation-reason': requireDeprecationReason,
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { GraphQLESLintRule } from '../types';
2+
import { TokenKind } from 'graphql';
3+
4+
const HASHTAG_COMMENT = 'HASHTAG_COMMENT';
5+
6+
const rule: GraphQLESLintRule = {
7+
meta: {
8+
messages: {
9+
[HASHTAG_COMMENT]: `Using hashtag (#) for adding GraphQL descriptions is not allowed. Prefer using """ for multiline, or " for a single line description.`,
10+
},
11+
docs: {
12+
description: `Requires to use """ or " for adding a GraphQL description instead of #.\nThis rule allows you to use hashtag for comments, as long as it's not attached to a AST definition.`,
13+
category: 'Best Practices',
14+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-hashtag-description.md`,
15+
requiresSchema: false,
16+
requiresSiblings: false,
17+
examples: [
18+
{
19+
title: 'Incorrect',
20+
code: /* GraphQL */ `
21+
# Represents a user
22+
type User {
23+
id: ID!
24+
name: String
25+
}
26+
`,
27+
},
28+
{
29+
title: 'Correct',
30+
code: /* GraphQL */ `
31+
" Represents a user "
32+
type User {
33+
id: ID!
34+
name: String
35+
}
36+
`,
37+
},
38+
{
39+
title: 'Correct',
40+
code: /* GraphQL */ `
41+
# This file defines the basic User type.
42+
# This comment is valid because it's not attached specifically to an AST object.
43+
44+
" Represents a user "
45+
type User {
46+
id: ID! # This one is also valid, since it comes after the AST object
47+
name: String
48+
}
49+
`,
50+
},
51+
],
52+
},
53+
type: 'suggestion',
54+
},
55+
create(context) {
56+
return {
57+
Document(node) {
58+
if (node) {
59+
const rawNode = node.rawNode();
60+
61+
if (rawNode && rawNode.loc && rawNode.loc.startToken) {
62+
let token = rawNode.loc.startToken;
63+
64+
while (token !== null) {
65+
if (token.kind === TokenKind.COMMENT && token.next && token.prev) {
66+
if (
67+
token.prev.kind !== TokenKind.SOF &&
68+
token.prev.kind !== TokenKind.COMMENT &&
69+
token.next.kind !== TokenKind.COMMENT &&
70+
token.next.line - token.line > 1 &&
71+
token.prev.line !== token.line
72+
) {
73+
context.report({
74+
messageId: HASHTAG_COMMENT,
75+
loc: {
76+
start: {
77+
line: token.line,
78+
column: token.column,
79+
},
80+
end: {
81+
line: token.line,
82+
column: token.column,
83+
},
84+
},
85+
});
86+
} else if (
87+
token.next.kind !== TokenKind.COMMENT &&
88+
token.next.kind !== TokenKind.EOF &&
89+
token.next.line - token.line < 2 &&
90+
token.prev.line !== token.line
91+
) {
92+
context.report({
93+
messageId: HASHTAG_COMMENT,
94+
loc: {
95+
start: {
96+
line: token.line,
97+
column: token.column,
98+
},
99+
end: {
100+
line: token.line,
101+
column: token.column,
102+
},
103+
},
104+
});
105+
}
106+
}
107+
108+
token = token.next;
109+
}
110+
}
111+
}
112+
},
113+
};
114+
},
115+
};
116+
117+
export default rule;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { GraphQLRuleTester } from '../src/testkit';
2+
import rule from '../src/rules/no-hashtag-description';
3+
import { Kind } from 'graphql';
4+
5+
const ruleTester = new GraphQLRuleTester();
6+
7+
ruleTester.runGraphQLTests('no-hashtag-description', rule, {
8+
valid: [
9+
{
10+
code: /* GraphQL */ `
11+
" test "
12+
type Query {
13+
foo: String
14+
}
15+
`,
16+
},
17+
{
18+
code: /* GraphQL */ `
19+
# Test
20+
21+
type Query {
22+
foo: String
23+
}
24+
`,
25+
},
26+
{
27+
code: `#import t
28+
29+
type Query {
30+
foo: String
31+
}
32+
`,
33+
},
34+
{
35+
code: /* GraphQL */ `
36+
# multiline
37+
# multiline
38+
# multiline
39+
# multiline
40+
41+
type Query {
42+
foo: String
43+
}
44+
`,
45+
},
46+
{
47+
code: /* GraphQL */ `
48+
type Query {
49+
foo: String
50+
}
51+
52+
# Test
53+
`,
54+
},
55+
{
56+
code: /* GraphQL */ `
57+
type Query {
58+
foo: String # this is also fine, comes after the definition
59+
}
60+
`,
61+
},
62+
{
63+
code: /* GraphQL */ `
64+
type Query { # this is also fine, comes after the definition
65+
foo: String
66+
} # this is also fine, comes after the definition
67+
`,
68+
},
69+
{
70+
code: /* GraphQL */ `
71+
type Query {
72+
foo: String
73+
}
74+
75+
# Test
76+
`,
77+
},
78+
],
79+
invalid: [
80+
{
81+
code: /* GraphQL */ `
82+
# Test
83+
type Query {
84+
foo: String
85+
}
86+
`,
87+
errors: [{ messageId: 'HASHTAG_COMMENT' }],
88+
},
89+
{
90+
code: /* GraphQL */ `
91+
type Query {
92+
# Test
93+
foo: String
94+
}
95+
`,
96+
errors: [{ messageId: 'HASHTAG_COMMENT' }],
97+
},
98+
],
99+
});

0 commit comments

Comments
 (0)