Skip to content

Commit

Permalink
Add OperationsMustHaveNames lint rule (#47)
Browse files Browse the repository at this point in the history
* Add OperationsMustHaveNames lint rule
  • Loading branch information
gauravmk authored and jnwng committed Mar 14, 2017
1 parent 746ae8d commit 45549a8
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 31 deletions.
101 changes: 70 additions & 31 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
without,
} from 'lodash';

import * as customRules from './rules';

const allGraphQLValidatorNames = allGraphQLValidators.map(rule => rule.name);

// Map of env name to list of rule names.
Expand All @@ -39,6 +41,41 @@ const envGraphQLValidatorNames = {
const internalTag = 'ESLintPluginGraphQLFile';
const gqlFiles = ['gql', 'graphql'];

const defaultRuleProperties = {
schemaJson: {
type: 'object',
},
schemaJsonFilepath: {
type: 'string',
},
tagName: {
type: 'string',
pattern: '^[$_a-zA-Z$_][a-zA-Z0-9$_]+(\\.[a-zA-Z0-9$_]+)?$',
},
}

function createRule(context, optionParser) {
const tagNames = new Set();
const tagRules = [];
for (const optionGroup of context.options) {
const {schema, env, tagName, validators} = optionParser(optionGroup);
if (tagNames.has(tagName)) {
throw new Error('Multiple options for GraphQL tag ' + tagName);
}
tagNames.add(tagName);
tagRules.push({schema, env, tagName, validators});
}
return {
TaggedTemplateExpression(node) {
for (const {schema, env, tagName, validators} of tagRules) {
if (templateExpressionMatchesTag(tagName, node)) {
return handleTemplateTag(node, context, schema, env, validators);
}
}
},
};
}

const rules = {
'template-strings': {
meta: {
Expand All @@ -48,12 +85,7 @@ const rules = {
items: {
additionalProperties: false,
properties: {
schemaJson: {
type: 'object',
},
schemaJsonFilepath: {
type: 'string',
},
...defaultRuleProperties,
env: {
enum: [
'lokka',
Expand All @@ -75,10 +107,6 @@ const rules = {
],
}],
},
tagName: {
type: 'string',
pattern: '^[$_a-zA-Z$_][a-zA-Z0-9$_]+(\\.[a-zA-Z0-9$_]+)?$',
},
},
// schemaJson and schemaJsonFilepath are mutually exclusive:
oneOf: [{
Expand All @@ -91,28 +119,33 @@ const rules = {
}
},
},
create(context) {
const tagNames = new Set();
const tagRules = [];
for (const optionGroup of context.options) {
const {schema, env, tagName, validators} = parseOptions(optionGroup);
if (tagNames.has(tagName)) {
throw new Error('Multiple options for GraphQL tag ' + tagName);
}
tagNames.add(tagName);
tagRules.push({schema, env, tagName, validators});
}
return {
TaggedTemplateExpression(node) {
for (const {schema, env, tagName, validators} of tagRules) {
if (templateExpressionMatchesTag(tagName, node)) {
return handleTemplateTag(node, context, schema, env, validators);
}
}
create: (context) => createRule(context, parseOptions)
},
'named-operations': {
meta: {
schema: {
type: 'array',
minLength: 1,
items: {
additionalProperties: false,
properties: { ...defaultRuleProperties },
oneOf: [{
required: ['schemaJson'],
not: { required: ['schemaJsonFilepath'], },
}, {
required: ['schemaJsonFilepath'],
not: { required: ['schemaJson'], },
}],
},
};
},
},
},
create: (context) => {
return createRule(context, (optionGroup) => parseOptions({
validators: ['OperationsMustHaveNames'],
...optionGroup,
}));;
},
}
};

function parseOptions(optionGroup) {
Expand Down Expand Up @@ -165,7 +198,13 @@ function parseOptions(optionGroup) {
validatorNames = envGraphQLValidatorNames[env] || allGraphQLValidatorNames;
}

const validators = validatorNames.map(name => require(`graphql/validation/rules/${name}`)[name]);
const validators = validatorNames.map(name => {
if (name in customRules) {
return customRules[name];
} else {
return require(`graphql/validation/rules/${name}`)[name];
}
});
return {schema, env, tagName, validators};
}

Expand Down
14 changes: 14 additions & 0 deletions src/rules.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { GraphQLError } from 'graphql';

export function OperationsMustHaveNames(context) {
return {
OperationDefinition(node) {
if (!node.name) {
context.reportError(
new GraphQLError("All operations must be named", [ node ])
);
}
},
};
}

20 changes: 20 additions & 0 deletions test/makeRule.js
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,17 @@ const validatorCases = {
},
};

const namedOperationsValidatorCases = {
'OperationsMustHaveNames': {
pass: 'const x = gql`query Test { sum(a: 1, b: 2) }`',
fail: 'const x = gql`query { sum(a: 1, b: 2) }`',
errors: [{
message: 'All operations must be named',
type: 'TaggedTemplateExpression',
}],
},
};

{
let options = [{
schemaJson, tagName: 'gql',
Expand Down Expand Up @@ -826,4 +837,13 @@ const validatorCases = {
invalid: [{options, parserOptions, errors, code: fail}],
});
}

// Validate the named-operations rule
options = [{
schemaJson, tagName: 'gql',
}];
ruleTester.run('testing named-operations rule', rules['named-operations'], {
valid: Object.values(namedOperationsValidatorCases).map(({pass: code}) => ({options, parserOptions, code})),
invalid: Object.values(namedOperationsValidatorCases).map(({fail: code, errors}) => ({options, parserOptions, code, errors})),
});
}

0 comments on commit 45549a8

Please sign in to comment.