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/curvy-cameras-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-eslint/eslint-plugin': minor
---

NEW RULE: no-hashtag-description
5 changes: 5 additions & 0 deletions .changeset/sweet-colts-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-eslint/eslint-plugin': patch
---

Fixed missing `loc` property when rawNode is called on Document node
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- [`unique-fragment-name`](./rules/unique-fragment-name.md)
- [`unique-operation-name`](./rules/unique-operation-name.md)
- [`validate-against-schema`](./rules/validate-against-schema.md)
- [`no-hashtag-description`](./rules/no-hashtag-description.md)
- [`no-anonymous-operations`](./rules/no-anonymous-operations.md)
- [`no-operation-name-suffix`](./rules/no-operation-name-suffix.md)
- [`require-deprecation-reason`](./rules/require-deprecation-reason.md)
Expand Down
50 changes: 50 additions & 0 deletions docs/rules/no-hashtag-description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# `no-hashtag-description`

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

Requires to use """ or " for adding a GraphQL description instead of #.
This rule allows you to use hashtag for comments, as long as it's not attached to a AST definition.

## Usage Examples

### Incorrect

```graphql
# eslint @graphql-eslint/no-hashtag-description: ["error"]

# Represents a user
type User {
id: ID!
name: String
}
```

### Correct

```graphql
# eslint @graphql-eslint/no-hashtag-description: ["error"]

" Represents a user "
type User {
id: ID!
name: String
}
```

### Correct

```graphql
# eslint @graphql-eslint/no-hashtag-description: ["error"]

# This file defines the basic User type.
# This comment is valid because it's not attached specifically to an AST object.

" Represents a user "
type User {
id: ID! # This one is also valid, since it comes after the AST object
name: String
}
```
8 changes: 5 additions & 3 deletions packages/plugin/src/estree-parser/converter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { convertDescription, convertLocation, convertRange, extractCommentsFromAst } from './utils';
import { GraphQLESTreeNode, SafeGraphQLType } from './estree-ast';
import { ASTNode, TypeNode, TypeInfo, visit, visitWithTypeInfo, Location, Kind } from 'graphql';
import { ASTNode, TypeNode, TypeInfo, visit, visitWithTypeInfo, Location, Kind, DocumentNode } from 'graphql';
import { Comment } from 'estree';

export function convertToESTree<T extends ASTNode>(
Expand Down Expand Up @@ -71,7 +71,8 @@ const convertNode = (typeInfo?: TypeInfo) => <T extends ASTNode>(
rawNode: () => {
if (!parent || key === undefined) {
if (node && (node as any).definitions) {
return {
return <DocumentNode>{
loc: gqlLocation,
kind: Kind.DOCUMENT,
definitions: (node as any).definitions.map(d => d.rawNode()),
};
Expand All @@ -96,7 +97,8 @@ const convertNode = (typeInfo?: TypeInfo) => <T extends ASTNode>(
rawNode: () => {
if (!parent || key === undefined) {
if (node && (node as any).definitions) {
return {
return <DocumentNode>{
loc: gqlLocation,
kind: Kind.DOCUMENT,
definitions: (node as any).definitions.map(d => d.rawNode()),
};
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import inputName from './input-name';
import uniqueFragmentName from './unique-fragment-name';
import uniqueOperationName from './unique-operation-name';
import noDeprecated from './no-deprecated';
import noHashtagDescription from './no-hashtag-description';
import { GraphQLESLintRule } from '../types';
import { GRAPHQL_JS_VALIDATIONS } from './graphql-js-validation';

Expand All @@ -21,6 +22,7 @@ export const rules: Record<string, GraphQLESLintRule> = {
'unique-fragment-name': uniqueFragmentName,
'unique-operation-name': uniqueOperationName,
'validate-against-schema': validate,
'no-hashtag-description': noHashtagDescription,
'no-anonymous-operations': noAnonymousOperations,
'no-operation-name-suffix': noOperationNameSuffix,
'require-deprecation-reason': requireDeprecationReason,
Expand Down
117 changes: 117 additions & 0 deletions packages/plugin/src/rules/no-hashtag-description.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { GraphQLESLintRule } from '../types';
import { TokenKind } from 'graphql';

const HASHTAG_COMMENT = 'HASHTAG_COMMENT';

const rule: GraphQLESLintRule = {
meta: {
messages: {
[HASHTAG_COMMENT]: `Using hashtag (#) for adding GraphQL descriptions is not allowed. Prefer using """ for multiline, or " for a single line description.`,
},
docs: {
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.`,
category: 'Best Practices',
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-hashtag-description.md`,
requiresSchema: false,
requiresSiblings: false,
examples: [
{
title: 'Incorrect',
code: /* GraphQL */ `
# Represents a user
type User {
id: ID!
name: String
}
`,
},
{
title: 'Correct',
code: /* GraphQL */ `
" Represents a user "
type User {
id: ID!
name: String
}
`,
},
{
title: 'Correct',
code: /* GraphQL */ `
# This file defines the basic User type.
# This comment is valid because it's not attached specifically to an AST object.

" Represents a user "
type User {
id: ID! # This one is also valid, since it comes after the AST object
name: String
}
`,
},
],
},
type: 'suggestion',
},
create(context) {
return {
Document(node) {
if (node) {
const rawNode = node.rawNode();

if (rawNode && rawNode.loc && rawNode.loc.startToken) {
let token = rawNode.loc.startToken;

while (token !== null) {
if (token.kind === TokenKind.COMMENT && token.next && token.prev) {
if (
token.prev.kind !== TokenKind.SOF &&
token.prev.kind !== TokenKind.COMMENT &&
token.next.kind !== TokenKind.COMMENT &&
token.next.line - token.line > 1 &&
token.prev.line !== token.line
) {
context.report({
messageId: HASHTAG_COMMENT,
loc: {
start: {
line: token.line,
column: token.column,
},
end: {
line: token.line,
column: token.column,
},
},
});
} else if (
token.next.kind !== TokenKind.COMMENT &&
token.next.kind !== TokenKind.EOF &&
token.next.line - token.line < 2 &&
token.prev.line !== token.line
) {
context.report({
messageId: HASHTAG_COMMENT,
loc: {
start: {
line: token.line,
column: token.column,
},
end: {
line: token.line,
column: token.column,
},
},
});
}
}

token = token.next;
}
}
}
},
};
},
};

export default rule;
99 changes: 99 additions & 0 deletions packages/plugin/tests/no-hashtag-description.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { GraphQLRuleTester } from '../src/testkit';
import rule from '../src/rules/no-hashtag-description';
import { Kind } from 'graphql';

const ruleTester = new GraphQLRuleTester();

ruleTester.runGraphQLTests('no-hashtag-description', rule, {
valid: [
{
code: /* GraphQL */ `
" test "
type Query {
foo: String
}
`,
},
{
code: /* GraphQL */ `
# Test

type Query {
foo: String
}
`,
},
{
code: `#import t

type Query {
foo: String
}
`,
},
{
code: /* GraphQL */ `
# multiline
# multiline
# multiline
# multiline

type Query {
foo: String
}
`,
},
{
code: /* GraphQL */ `
type Query {
foo: String
}

# Test
`,
},
{
code: /* GraphQL */ `
type Query {
foo: String # this is also fine, comes after the definition
}
`,
},
{
code: /* GraphQL */ `
type Query { # this is also fine, comes after the definition
foo: String
} # this is also fine, comes after the definition
`,
},
{
code: /* GraphQL */ `
type Query {
foo: String
}

# Test
`,
},
],
invalid: [
{
code: /* GraphQL */ `
# Test
type Query {
foo: String
}
`,
errors: [{ messageId: 'HASHTAG_COMMENT' }],
},
{
code: /* GraphQL */ `
type Query {
# Test
foo: String
}
`,
errors: [{ messageId: 'HASHTAG_COMMENT' }],
},
],
});