Skip to content

Commit

Permalink
feat: add autofix for alphabetize rule and change sort order to lex…
Browse files Browse the repository at this point in the history
…icographic (#936)

* feat: add autofix for `alphabetize` rule

* feat: improve rule-tester

* feat: skip autofix for fields with comments between or around

* should compare with lexicographic order

* add info about autofix in generate-docs script

* update examples deps to trigger patches folder changes

* fix build

* fix test on node 12

* add autofix even for fields with comments

* fix ESTree parser, convert `loc.column` to 0-based column number (#939)

* fixes after rebase

* fixes tests on graphql 15
  • Loading branch information
dimaMachina committed Feb 1, 2022
1 parent d9bdbd3 commit 1729313
Show file tree
Hide file tree
Showing 77 changed files with 1,253 additions and 625 deletions.
5 changes: 5 additions & 0 deletions .changeset/great-fans-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-eslint/eslint-plugin': patch
---

fix ESTree parser, convert `loc.column` to 0-based column number
5 changes: 5 additions & 0 deletions .changeset/silver-worms-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-eslint/eslint-plugin': minor
---

feat: add autofix for `alphabetize` rule and change sort order to lexicographic
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module.exports = {
reportUnusedDisableDirectives: true,
ignorePatterns: ['examples'],
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'standard', 'prettier'],
extends: ['eslint:recommended', 'standard', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['unicorn'],
rules: {
'no-empty': 'off',
Expand Down
2 changes: 1 addition & 1 deletion docs/parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Here's a list of changes that the parser performs, in order to make the GraphQL

**Problem**: GraphQL uses `location` field to store the AST locations, while ESTree also uses it in a different structure.

**Solution**: The parser creates a new `location` field that is compatible with ESTree, and renames the GraphQL `location` to `gqlLocation`.
**Solution**: The parser creates a new `location` field that is compatible with ESTree.

### Loading GraphQL Schema

Expand Down
2 changes: 2 additions & 0 deletions docs/rules/alphabetize.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# `alphabetize`

🔧 The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule.

- Category: `Schema & Operations`
- Rule name: `@graphql-eslint/alphabetize`
- Requires GraphQL Schema: `false` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
Expand Down
2 changes: 1 addition & 1 deletion examples/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
},
"devDependencies": {
"@graphql-eslint/eslint-plugin": "3.7.0",
"eslint": "8.2.0"
"eslint": "8.7.0"
}
}
2 changes: 1 addition & 1 deletion examples/code-file/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
},
"devDependencies": {
"@graphql-eslint/eslint-plugin": "3.7.0",
"eslint": "8.2.0"
"eslint": "8.7.0"
}
}
2 changes: 1 addition & 1 deletion examples/graphql-config-code-file/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
},
"devDependencies": {
"@graphql-eslint/eslint-plugin": "3.7.0",
"eslint": "8.2.0"
"eslint": "8.7.0"
}
}
2 changes: 1 addition & 1 deletion examples/graphql-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
},
"devDependencies": {
"@graphql-eslint/eslint-plugin": "3.7.0",
"eslint": "8.2.0"
"eslint": "8.7.0"
}
}
2 changes: 1 addition & 1 deletion examples/prettier/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"devDependencies": {
"@graphql-eslint/eslint-plugin": "3.7.0",
"eslint": "8.2.0",
"eslint": "8.7.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-prettier": "4.0.0",
"prettier": "2.4.1"
Expand Down
56 changes: 38 additions & 18 deletions packages/plugin/src/estree-parser/converter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { convertDescription, convertLocation, convertRange, extractCommentsFromAst } from './utils';
import type { SourceLocation } from 'estree';
import { convertDescription, extractCommentsFromAst } from './utils';
import { GraphQLESTreeNode, SafeGraphQLType } from './estree-ast';
import { ASTNode, TypeNode, TypeInfo, visit, visitWithTypeInfo, Location, Kind, DocumentNode, ASTVisitor } from 'graphql';
import {
ASTNode,
TypeNode,
TypeInfo,
visit,
visitWithTypeInfo,
Location,
Kind,
DocumentNode,
ASTVisitor,
} from 'graphql';

export function convertToESTree<T extends ASTNode>(
node: T,
typeInfo?: TypeInfo
) {
export function convertToESTree<T extends ASTNode>(node: T, typeInfo?: TypeInfo) {
const visitor: ASTVisitor = { leave: convertNode(typeInfo) };
return {
rootTree: visit(node, typeInfo ? visitWithTypeInfo(typeInfo, visitor) : visitor) as GraphQLESTreeNode<T>,
Expand All @@ -17,16 +25,30 @@ function hasTypeField<T extends ASTNode>(obj: any): obj is T & { readonly type:
return obj && !!(obj as any).type;
}

/**
* Strips tokens information from `location` object - this is needed since it's created as linked list in GraphQL-JS,
* causing eslint to fail on circular JSON
* @param location
*/
function stripTokens(location: Location): Pick<Location, 'start' | 'end'> {
return {
end: location.end,
start: location.start,
function convertLocation(location: Location): SourceLocation {
const { startToken, endToken, source, start, end } = location;
/*
* ESLint has 0-based column number
* https://eslint.org/docs/developer-guide/working-with-rules#contextreport
*/
const loc = {
start: {
/*
* Kind.Document has startToken: { line: 0, column: 0 }, we set line as 1 and column as 0
*/
line: startToken.line === 0 ? 1 : startToken.line,
column: startToken.column === 0 ? 0 : startToken.column - 1,
},
end: {
line: endToken.line,
column: endToken.column - 1,
},
source: source.body,
};
if (loc.start.column === loc.end.column) {
loc.end.column += end - start;
}
return loc;
}

const convertNode = (typeInfo?: TypeInfo) => <T extends ASTNode>(
Expand All @@ -52,7 +74,7 @@ const convertNode = (typeInfo?: TypeInfo) => <T extends ASTNode>(
typeInfo: () => calculatedTypeInfo,
leadingComments: convertDescription(node),
loc: convertLocation(node.loc),
range: convertRange(node.loc),
range: [node.loc.start, node.loc.end],
};

if (hasTypeField<T>(node)) {
Expand Down Expand Up @@ -80,7 +102,6 @@ const convertNode = (typeInfo?: TypeInfo) => <T extends ASTNode>(

return parent[key];
},
gqlLocation: stripTokens(gqlLocation),
} as any) as GraphQLESTreeNode<T>;

return estreeNode;
Expand All @@ -106,7 +127,6 @@ const convertNode = (typeInfo?: TypeInfo) => <T extends ASTNode>(

return parent[key];
},
gqlLocation: stripTokens(gqlLocation),
} as any) as GraphQLESTreeNode<T>;

return estreeNode;
Expand Down
3 changes: 1 addition & 2 deletions packages/plugin/src/estree-parser/estree-ast.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ASTNode, Location, TypeInfo, TypeNode, ValueNode } from 'graphql';
import { ASTNode, TypeInfo, TypeNode, ValueNode } from 'graphql';
import { BaseNode } from 'estree';

export type SafeGraphQLType<T extends ASTNode | ValueNode> = Omit<
Expand All @@ -10,7 +10,6 @@ export type SingleESTreeNode<T, WithTypeInfo extends boolean> = T extends ASTNod
? SafeGraphQLType<T> &
Pick<BaseNode, 'leadingComments' | 'loc' | 'range'> & {
type: T['kind'];
gqlLocation: Location;
} & (WithTypeInfo extends true
? {
typeInfo?: () => {
Expand Down
123 changes: 93 additions & 30 deletions packages/plugin/src/estree-parser/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ import {
GraphQLNamedType,
isNonNullType,
isListType,
Token,
Source,
Lexer,
} from 'graphql';
import { SourceLocation, Comment } from 'estree';
import { GraphQLESTreeNode } from './estree-ast';
import type { Comment } from 'estree';
import type { AST } from 'eslint';
import type { GraphQLESTreeNode } from './estree-ast';

export default function keyValMap<T, V>(
list: ReadonlyArray<T>,
Expand Down Expand Up @@ -57,8 +61,85 @@ export function getBaseType(type: GraphQLOutputType): GraphQLNamedType {
return type;
}

export function convertRange(gqlLocation: Location): [number, number] {
return [gqlLocation.start, gqlLocation.end];
// Hardcoded type because tests fails on graphql 15
type TokenKindValue =
| '<SOF>'
// | '<EOF>'
| '!'
| '$'
| '&'
| '('
| ')'
| '...'
| ':'
| '='
| '@'
| '['
| ']'
| '{'
| '|'
| '}'
| 'Name'
| 'Int'
| 'Float'
| 'String'
| 'BlockString'
| 'Comment';

export function convertToken<T extends 'Line' | 'Block' | TokenKindValue>(
token: Token,
type: T
): Omit<AST.Token, 'type'> & { type: T } {
const { line, column, end, start, value } = token;
return {
type,
value,
/*
* ESLint has 0-based column number
* https://eslint.org/docs/developer-guide/working-with-rules#contextreport
*/
loc: {
start: {
line,
column: column - 1,
},
end: {
line,
column: column - 1 + (end - start),
},
},
range: [start, end],
};
}

function getLexer(source: Source): Lexer {
// GraphQL v14
const gqlLanguage = require('graphql/language');
if (gqlLanguage && gqlLanguage.createLexer) {
return gqlLanguage.createLexer(source, {});
}

// GraphQL v15
const { Lexer: LexerCls } = require('graphql');
if (LexerCls && typeof LexerCls === 'function') {
return new LexerCls(source);
}

throw new Error('Unsupported GraphQL version! Please make sure to use GraphQL v14 or newer!');
}

export function extractTokens(source: Source): AST.Token[] {
const lexer = getLexer(source);
const tokens: AST.Token[] = [];
let token = lexer.advance();

while (token && token.kind !== TokenKind.EOF) {
const result = convertToken(token, token.kind) as AST.Token;
tokens.push(result);
token = lexer.advance();
}

return tokens;
}

export function extractCommentsFromAst(loc: Location): Comment[] {
Expand All @@ -69,37 +150,19 @@ export function extractCommentsFromAst(loc: Location): Comment[] {
let token = loc.startToken;

while (token !== null) {
const { kind, value, line, column, start, end, next } = token;
if (kind === TokenKind.COMMENT) {
comments.push({
type: 'Block',
value,
loc: {
start: { line, column },
end: { line, column },
},
range: [start, end],
});
if (token.kind === TokenKind.COMMENT) {
const comment = convertToken(
token,
// `eslint-disable` directive works only with `Block` type comment
token.value.trimStart().startsWith('eslint') ? 'Block' : 'Line'
);
comments.push(comment);
}
token = next;
token = token.next;
}
return comments;
}

export function convertLocation(gqlLocation: Location): SourceLocation {
return {
start: {
column: gqlLocation.startToken.column,
line: gqlLocation.startToken.line,
},
end: {
column: gqlLocation.endToken.column,
line: gqlLocation.endToken.line,
},
source: gqlLocation.source.body,
};
}

export function isNodeWithDescription<T extends ASTNode>(
obj: T
): obj is T & { readonly description?: StringValueNode } {
Expand Down
3 changes: 1 addition & 2 deletions packages/plugin/src/parser.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { parseGraphQLSDL } from '@graphql-tools/utils';
import { ASTNode, GraphQLError, TypeInfo, Source } from 'graphql';
import { Linter } from 'eslint';
import { convertToESTree } from './estree-parser';
import { convertToESTree, extractTokens } from './estree-parser';
import { GraphQLESLintParseResult, ParserOptions, ParserServices } from './types';
import { extractTokens } from './utils';
import { getSchema } from './schema';
import { getSiblingOperations } from './sibling-operations';
import { loadGraphQLConfig } from './graphql-config';
Expand Down

0 comments on commit 1729313

Please sign in to comment.