Skip to content

Add support for autofixing #261

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
11 changes: 11 additions & 0 deletions src/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class Configuration {
- customRulePaths: [string array] path to additional custom rules to be loaded
- commentDescriptions: [boolean] use old way of defining descriptions in GraphQL SDL
- oldImplementsSyntax: [boolean] use old way of defining implemented interfaces in GraphQL SDL
- fix: [boolean] automatically fix errors where possible
*/
constructor(schema, options = {}) {
const defaultOptions = {
Expand All @@ -22,6 +23,7 @@ export class Configuration {
commentDescriptions: false,
oldImplementsSyntax: false,
ignore: {},
fix: false,
};

this.schema = schema;
Expand Down Expand Up @@ -120,6 +122,15 @@ export class Configuration {
validate() {
const issues = [];

if (this.options.stdin && this.options.fix) {
this.options.fix = false;
issues.push({
message: `--fix and --stdin are incompatible; --fix will be ignored`,
field: 'fix',
type: 'warning',
});
}

let rules;

try {
Expand Down
54 changes: 54 additions & 0 deletions src/fix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { writeFileSync } from 'fs';

// Returns {<path>: <new content>} for only those paths which changed.
export function fixSchema(errors, sourceMap) {
// Select fixable errors, and sort in by start-location of the fix.
errors = errors.filter((error) => error.fix != null);
if (errors.length === 0) {
return {};
}
errors.sort((first, second) => first.fix.loc.start - second.fix.loc.start);

// Apply the fixes by iterating through files, walking the errors list for
// each at the same time.
let fileStartOffset = 0;
const fixedPaths = {};
let errorIndex = 0;
Object.entries(sourceMap.sourceFiles).forEach(([path, text]) => {
const fileEndOffset = fileStartOffset + text.length;
const fixedParts = [];
let currentFileOffset = 0;
while (
errorIndex < errors.length &&
errors[errorIndex].fix.loc.start <= fileEndOffset
) {
const { loc, replacement } = errors[errorIndex].fix;
fixedParts.push(
text.slice(currentFileOffset, loc.start - fileStartOffset)
);
fixedParts.push(replacement);
currentFileOffset = loc.end - fileStartOffset;
errorIndex++;
}

if (fixedParts.length > 0) {
fixedParts.push(text.slice(currentFileOffset));
fixedPaths[path] = fixedParts.join('');
}

fileStartOffset = fileEndOffset + 1; // sourceMap adds a newline in between
});

return fixedPaths;
}

// Given output from fixSchema, write the fixes to disk.
export function applyFixes(fixes) {
for (const [path, text] of Object.entries(fixes)) {
try {
writeFileSync(path, text);
} catch (e) {
console.error(e.message);
}
}
}
17 changes: 16 additions & 1 deletion src/rules/descriptions_are_capitalized.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,26 @@ export function DescriptionsAreCapitalized(configuration, context) {
const fieldName = node.name.value;
const parentName = ancestors[ancestors.length - 1].name.value;

let fix;
if (node.description != null) {
// Supporting autofixes for comment-descriptions is a bunch of extra
// work, just do it for real descriptions.
let start = node.description.loc.start;
while (node.description.loc.source.body[start] === '"') {
start++;
}
fix = {
loc: { start, end: start + 1 },
replacement: node.description.loc.source.body[start].toUpperCase(),
};
}

context.reportError(
new ValidationError(
'descriptions-are-capitalized',
`The description for field \`${parentName}.${fieldName}\` should be capitalized.`,
[node]
[node],
fix
)
);
},
Expand Down
3 changes: 2 additions & 1 deletion src/rules/enum_values_all_caps.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export function EnumValuesAllCaps(context) {
new ValidationError(
'enum-values-all-caps',
`The enum value \`${parentName}.${enumValueName}\` should be uppercase.`,
[node]
[node],
{ loc: node.name.loc, replacement: enumValueName.toUpperCase() }
)
);
}
Expand Down
16 changes: 8 additions & 8 deletions src/rules/enum_values_sorted_alphabetically.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { ValidationError } from '../validation_error';
import listIsAlphabetical from '../util/listIsAlphabetical';
import alphabetizeNodes from '../util/alphabetizeNodes';

export function EnumValuesSortedAlphabetically(context) {
return {
EnumTypeDefinition(node, key, parent, path, ancestors) {
const enumValues = node.values.map((val) => {
return val.name.value;
});

const { isSorted, sortedList } = listIsAlphabetical(enumValues);
const { isSorted, sortedNames, fix } = alphabetizeNodes(
node.values,
(val) => val.name.value
);

if (!isSorted) {
context.reportError(
new ValidationError(
'enum-values-sorted-alphabetically',
`The enum \`${node.name.value}\` should be sorted alphabetically. ` +
`Expected sorting: ${sortedList.join(', ')}`,
[node]
`Expected sorting: ${sortedNames.join(', ')}`,
[node],
fix
)
);
}
Expand Down
8 changes: 7 additions & 1 deletion src/rules/fields_are_camel_cased.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { ValidationError } from '../validation_error';

const camelCaseTest = RegExp('^[a-z][a-zA-Z0-9]*$');

function makeCamelCase(name) {
name = name.replace(/_+([^_])/g, (match, g1) => g1.toUpperCase());
return name[0].toLowerCase() + name.slice(1);
}

export function FieldsAreCamelCased(context) {
return {
FieldDefinition(node, key, parent, path, ancestors) {
Expand All @@ -12,7 +17,8 @@ export function FieldsAreCamelCased(context) {
new ValidationError(
'fields-are-camel-cased',
`The field \`${parentName}.${fieldName}\` is not camel cased.`,
[node]
[node],
{ loc: node.name.loc, replacement: makeCamelCase(fieldName) }
)
);
}
Expand Down
13 changes: 8 additions & 5 deletions src/rules/input_object_fields_sorted_alphabetically.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { ValidationError } from '../validation_error';
import listIsAlphabetical from '../util/listIsAlphabetical';
import alphabetizeNodes from '../util/alphabetizeNodes';

export function InputObjectFieldsSortedAlphabetically(context) {
return {
InputObjectTypeDefinition(node) {
const fieldList = (node.fields || []).map((field) => field.name.value);
const { isSorted, sortedList } = listIsAlphabetical(fieldList);
const { isSorted, sortedNames, fix } = alphabetizeNodes(
node.fields || [],
(field) => field.name.value
);

if (!isSorted) {
context.reportError(
new ValidationError(
'input-object-fields-sorted-alphabetically',
`The fields of input type \`${node.name.value}\` should be sorted alphabetically. ` +
`Expected sorting: ${sortedList.join(', ')}`,
[node]
`Expected sorting: ${sortedNames.join(', ')}`,
[node],
fix
)
);
}
Expand Down
8 changes: 7 additions & 1 deletion src/rules/input_object_values_are_camel_cased.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { ValidationError } from '../validation_error';

const camelCaseTest = RegExp('^[a-z][a-zA-Z0-9]*$');

function makeCamelCase(name) {
name = name.replace(/_+([^_])/g, (match, g1) => g1.toUpperCase());
return name[0].toLowerCase() + name.slice(1);
}

export function InputObjectValuesAreCamelCased(context) {
return {
InputValueDefinition(node, key, parent, path, ancestors) {
Expand All @@ -15,7 +20,8 @@ export function InputObjectValuesAreCamelCased(context) {
new ValidationError(
'input-object-values-are-camel-cased',
`The input value \`${inputObjectName}.${inputValueName}\` is not camel cased.`,
[node]
[node],
{ loc: node.name.loc, replacement: makeCamelCase(fieldName) }
)
);
}
Expand Down
13 changes: 8 additions & 5 deletions src/rules/type_fields_sorted_alphabetically.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { ValidationError } from '../validation_error';
import listIsAlphabetical from '../util/listIsAlphabetical';
import alphabetizeNodes from '../util/alphabetizeNodes';

export function TypeFieldsSortedAlphabetically(context) {
return {
ObjectTypeDefinition(node) {
const fieldList = (node.fields || []).map((field) => field.name.value);
const { isSorted, sortedList } = listIsAlphabetical(fieldList);
const { isSorted, sortedNames, fix } = alphabetizeNodes(
node.fields || [],
(field) => field.name.value
);

if (!isSorted) {
context.reportError(
new ValidationError(
'type-fields-sorted-alphabetically',
`The fields of object type \`${node.name.value}\` should be sorted alphabetically. ` +
`Expected sorting: ${sortedList.join(', ')}`,
[node]
`Expected sorting: ${sortedNames.join(', ')}`,
[node],
fix
)
);
}
Expand Down
12 changes: 10 additions & 2 deletions src/rules/types_are_capitalized.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ export function TypesAreCapitalized(context) {
new ValidationError(
'types-are-capitalized',
`The object type \`${typeName}\` should start with a capital letter.`,
[node.name]
[node.name],
{
loc: node.name.loc,
replacement: typeName[0].toUpperCase() + typeName.slice(1),
}
)
);
}
Expand All @@ -22,7 +26,11 @@ export function TypesAreCapitalized(context) {
new ValidationError(
'types-are-capitalized',
`The interface type \`${typeName}\` should start with a capital letter.`,
[node.name]
[node.name],
{
loc: node.name.loc,
replacement: typeName[0].toUpperCase() + typeName.slice(1),
}
)
);
}
Expand Down
13 changes: 13 additions & 0 deletions src/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Command } from 'commander';
import { Configuration } from './configuration.js';
import { loadSchema } from './schema.js';
import { loadOptionsFromConfigDir } from './options.js';
import { applyFixes, fixSchema } from './fix.js';
import figures from './figures';
import chalk from 'chalk';

Expand Down Expand Up @@ -42,6 +43,10 @@ export async function run(stdout, stdin, stderr, argv) {
'--old-implements-syntax',
'use old way of defining implemented interfaces in GraphQL SDL'
)
.option(
'--fix',
'when possible, automatically update input files to fix errors; incompatible with --stdin'
)
// DEPRECATED - This code should be removed in v1.0.0.
.option(
'-o, --only <rules>',
Expand Down Expand Up @@ -107,6 +112,10 @@ export async function run(stdout, stdin, stderr, argv) {
const errors = validateSchemaDefinition(schema, rules, configuration);
const groupedErrors = groupErrorsBySchemaFilePath(errors, schema.sourceMap);

if (configuration.options.fix) {
applyFixes(fixSchema(errors, schema.sourceMap));
}

stdout.write(formatter(groupedErrors));

return errors.length > 0 ? 1 : 0;
Expand Down Expand Up @@ -168,6 +177,10 @@ function getOptionsFromCommander(commander) {
options.oldImplementsSyntax = commander.oldImplementsSyntax;
}

if (commander.fix) {
options.fix = commander.fix;
}

if (commander.args && commander.args.length) {
options.schemaPaths = commander.args;
}
Expand Down
Loading