eslint-plugin-graphql
Advanced tools
Comparing version
# Change log | ||
### vNEXT | ||
### v3.0.0 | ||
- BREAKING: The `required-fields` rule has been significantly changed to make it a completely reliable method of ensuring an `id` field (or any other field name) is always requested when available. [PR #199](https://github.com/apollographql/eslint-plugin-graphql/pull/199) Here is the behavior, let's say we are requiring field `id`: | ||
- On any field whose return type defines a field called `id`, the selection set must directly contain `id`. | ||
- In any named fragment declaration whose type defines a field called `id`, the selection set must directly contain `id`. | ||
- An inline fragment whose type defines a field called `id` must contain `id` in its selection set unless its parent is also an inline fragment that contains the field `id`. | ||
- Here's a specific case which is _no longer valid_: | ||
- `query { greetings { hello ... on Greetings { id } } }` | ||
- This must now be written as `query { greetings { id hello ... on Greetings { id } } }` | ||
- This is a more conservative approach than before, driven by the fact that it's quite hard to ensure that a combination of inline fragments actually covers all of the possible types of a selection set. | ||
- Fix breaking change in `graphql@^14.0.0` that renamed `ProvidedNonNullArguments` to `ProvidedRequiredArguments` [#192](https://github.com/apollographql/eslint-plugin-graphql/pull/192) | ||
- Update dependencies to graphql-tools 4 and eslint 5.9 [#193](https://github.com/apollographql/eslint-plugin-graphql/pull/193) | ||
### v2.1.1 | ||
- Fix support for InlineFragments with the `required-fields` rule in [#140](https://github.com/apollographql/eslint-plugin-graphql/pull/140/files) by [Steve Hollaar](https://github.com/stevehollaar) | ||
- Fix error location information for literal .graphql files and strings with leading newlines in [#122](https://github.com/apollographql/eslint-plugin-graphql/pull/122) by [Dan Freeman](https://github.com/dfreeman) | ||
- Add [`fraql`](https://github.com/smooth-code/fraql) environment | ||
@@ -8,0 +22,0 @@ ### v2.1.0 |
272
lib/index.js
@@ -24,6 +24,10 @@ 'use strict'; | ||
var _rules = require('./rules'); | ||
var _customGraphQLValidationRules = require('./customGraphQLValidationRules'); | ||
var customRules = _interopRequireWildcard(_rules); | ||
var customRules = _interopRequireWildcard(_customGraphQLValidationRules); | ||
var _constants = require('./constants'); | ||
var _createRule = require('./createRule'); | ||
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } | ||
@@ -43,7 +47,11 @@ | ||
lokka: (0, _lodash.without)(allGraphQLValidatorNames, 'KnownFragmentNames', 'NoUnusedFragments'), | ||
relay: (0, _lodash.without)(allGraphQLValidatorNames, 'KnownDirectives', 'KnownFragmentNames', 'NoUndefinedVariables', 'NoUnusedFragments', 'ProvidedNonNullArguments', 'ScalarLeafs'), | ||
fraql: (0, _lodash.without)(allGraphQLValidatorNames, 'KnownFragmentNames', 'NoUnusedFragments'), | ||
relay: (0, _lodash.without)(allGraphQLValidatorNames, 'KnownDirectives', 'KnownFragmentNames', 'NoUndefinedVariables', 'NoUnusedFragments', | ||
// `graphql` < 14 | ||
'ProvidedNonNullArguments', | ||
// `graphql`@14 | ||
'ProvidedRequiredArguments', 'ScalarLeafs'), | ||
literal: (0, _lodash.without)(allGraphQLValidatorNames, 'KnownFragmentNames', 'NoUnusedFragments') | ||
}; | ||
var internalTag = 'ESLintPluginGraphQLFile'; | ||
var gqlFiles = ['gql', 'graphql']; | ||
@@ -53,3 +61,3 @@ | ||
env: { | ||
enum: ['lokka', 'relay', 'apollo', 'literal'] | ||
enum: ['lokka', 'fraql', 'relay', 'apollo', 'literal'] | ||
}, | ||
@@ -72,90 +80,5 @@ schemaJson: { | ||
} | ||
}; | ||
function createRule(context, optionParser) { | ||
var tagNames = new Set(); | ||
var tagRules = []; | ||
var options = context.options.length === 0 ? [{}] : context.options; | ||
var _iteratorNormalCompletion = true; | ||
var _didIteratorError = false; | ||
var _iteratorError = undefined; | ||
try { | ||
var _loop = function _loop() { | ||
var optionGroup = _step.value; | ||
var _optionParser = optionParser(optionGroup), | ||
schema = _optionParser.schema, | ||
env = _optionParser.env, | ||
tagName = _optionParser.tagName, | ||
validators = _optionParser.validators; | ||
var boundValidators = validators.map(function (v) { | ||
return function (ctx) { | ||
return v(ctx, optionGroup); | ||
}; | ||
}); | ||
if (tagNames.has(tagName)) { | ||
throw new Error('Multiple options for GraphQL tag ' + tagName); | ||
} | ||
tagNames.add(tagName); | ||
tagRules.push({ schema: schema, env: env, tagName: tagName, validators: boundValidators }); | ||
}; | ||
for (var _iterator = options[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { | ||
_loop(); | ||
} | ||
} catch (err) { | ||
_didIteratorError = true; | ||
_iteratorError = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion && _iterator.return) { | ||
_iterator.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError) { | ||
throw _iteratorError; | ||
} | ||
} | ||
} | ||
return { | ||
TaggedTemplateExpression: function TaggedTemplateExpression(node) { | ||
var _iteratorNormalCompletion2 = true; | ||
var _didIteratorError2 = false; | ||
var _iteratorError2 = undefined; | ||
try { | ||
for (var _iterator2 = tagRules[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { | ||
var _ref2 = _step2.value; | ||
var schema = _ref2.schema, | ||
env = _ref2.env, | ||
tagName = _ref2.tagName, | ||
validators = _ref2.validators; | ||
if (templateExpressionMatchesTag(tagName, node)) { | ||
return handleTemplateTag(node, context, schema, env, validators); | ||
} | ||
} | ||
} catch (err) { | ||
_didIteratorError2 = true; | ||
_iteratorError2 = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion2 && _iterator2.return) { | ||
_iterator2.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError2) { | ||
throw _iteratorError2; | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
} | ||
// schemaJson, schemaJsonFilepath, schemaString and projectName are mutually exclusive: | ||
var schemaPropsExclusiveness = { | ||
// schemaJson, schemaJsonFilepath, schemaString and projectName are mutually exclusive: | ||
};var schemaPropsExclusiveness = { | ||
oneOf: [{ | ||
@@ -201,3 +124,3 @@ required: ['schemaJson'], | ||
create: function create(context) { | ||
return createRule(context, function (optionGroup) { | ||
return (0, _createRule.createRule)(context, function (optionGroup) { | ||
return parseOptions(optionGroup, context); | ||
@@ -218,3 +141,3 @@ }); | ||
create: function create(context) { | ||
return createRule(context, function (optionGroup) { | ||
return (0, _createRule.createRule)(context, function (optionGroup) { | ||
return parseOptions(_extends({ | ||
@@ -246,3 +169,3 @@ validators: ['OperationsMustHaveNames'] | ||
create: function create(context) { | ||
return createRule(context, function (optionGroup) { | ||
return (0, _createRule.createRule)(context, function (optionGroup) { | ||
return parseOptions(_extends({ | ||
@@ -266,3 +189,3 @@ validators: ['RequiredFields'], | ||
create: function create(context) { | ||
return createRule(context, function (optionGroup) { | ||
return (0, _createRule.createRule)(context, function (optionGroup) { | ||
return parseOptions(_extends({ | ||
@@ -285,3 +208,3 @@ validators: ['typeNamesShouldBeCapitalized'] | ||
create: function create(context) { | ||
return createRule(context, function (optionGroup) { | ||
return (0, _createRule.createRule)(context, function (optionGroup) { | ||
return parseOptions(_extends({ | ||
@@ -341,4 +264,4 @@ validators: ['noDeprecatedFields'] | ||
// Validate env | ||
if (env && env !== 'lokka' && env !== 'relay' && env !== 'apollo' && env !== 'literal') { | ||
throw new Error('Invalid option for env, only `apollo`, `lokka`, `relay`, and `literal` supported.'); | ||
if (env && env !== 'lokka' && env !== 'fraql' && env !== 'relay' && env !== 'apollo' && env !== 'literal') { | ||
throw new Error('Invalid option for env, only `apollo`, `lokka`, `fraql`, `relay`, and `literal` supported.'); | ||
} | ||
@@ -353,3 +276,3 @@ | ||
} else if (env === 'literal') { | ||
tagName = internalTag; | ||
tagName = _constants.internalTag; | ||
} else { | ||
@@ -400,149 +323,2 @@ tagName = 'gql'; | ||
function templateExpressionMatchesTag(tagName, node) { | ||
var tagNameSegments = tagName.split('.').length; | ||
if (tagNameSegments === 1) { | ||
// Check for single identifier, like 'gql' | ||
if (node.tag.type !== 'Identifier' || node.tag.name !== tagName) { | ||
return false; | ||
} | ||
} else if (tagNameSegments === 2) { | ||
// Check for dotted identifier, like 'Relay.QL' | ||
if (node.tag.type !== 'MemberExpression' || node.tag.object.name + '.' + node.tag.property.name !== tagName) { | ||
return false; | ||
} | ||
} else { | ||
// We don't currently support 3 segments so ignore | ||
return false; | ||
} | ||
return true; | ||
} | ||
function handleTemplateTag(node, context, schema, env, validators) { | ||
var text = void 0; | ||
try { | ||
text = replaceExpressions(node.quasi, context, env); | ||
} catch (e) { | ||
if (e.message !== 'Invalid interpolation') { | ||
console.log(e); | ||
} | ||
return; | ||
} | ||
// Re-implement syntax sugar for fragment names, which is technically not valid | ||
// graphql | ||
if ((env === 'lokka' || env === 'relay') && /fragment\s+on/.test(text)) { | ||
text = text.replace('fragment', 'fragment _'); | ||
} | ||
var ast = void 0; | ||
try { | ||
ast = (0, _graphql.parse)(text); | ||
} catch (error) { | ||
context.report({ | ||
node: node, | ||
message: error.message.split('\n')[0], | ||
loc: locFrom(node, error) | ||
}); | ||
return; | ||
} | ||
var validationErrors = schema ? (0, _graphql.validate)(schema, ast, validators) : []; | ||
if (validationErrors && validationErrors.length > 0) { | ||
context.report({ | ||
node: node, | ||
message: validationErrors[0].message, | ||
loc: locFrom(node, validationErrors[0]) | ||
}); | ||
return; | ||
} | ||
} | ||
function locFrom(node, error) { | ||
if (!error.locations || !error.locations.length) { | ||
return; | ||
} | ||
var location = error.locations[0]; | ||
var line = void 0; | ||
var column = void 0; | ||
if (location.line === 1 && node.tag.name !== internalTag) { | ||
line = node.loc.start.line; | ||
column = node.tag.loc.end.column + location.column; | ||
} else { | ||
line = node.loc.start.line + location.line - 1; | ||
column = location.column - 1; | ||
} | ||
return { | ||
line: line, | ||
column: column | ||
}; | ||
} | ||
function replaceExpressions(node, context, env) { | ||
var chunks = []; | ||
node.quasis.forEach(function (element, i) { | ||
var chunk = element.value.cooked; | ||
var value = node.expressions[i]; | ||
chunks.push(chunk); | ||
if (!env || env === 'apollo') { | ||
// In Apollo, interpolation is only valid outside top-level structures like `query` or `mutation`. | ||
// We'll check to make sure there's an equivalent set of opening and closing brackets, otherwise | ||
// we're attempting to do an invalid interpolation. | ||
if (chunk.split('{').length - 1 !== chunk.split('}').length - 1) { | ||
context.report({ | ||
node: value, | ||
message: 'Invalid interpolation - fragment interpolation must occur outside of the brackets.' | ||
}); | ||
throw new Error('Invalid interpolation'); | ||
} | ||
} | ||
if (!element.tail) { | ||
// Preserve location of errors by replacing with exactly the same length | ||
var nameLength = value.end - value.start; | ||
if (env === 'relay' && /:\s*$/.test(chunk)) { | ||
// The chunk before this one had a colon at the end, so this | ||
// is a variable | ||
// Add 2 for brackets in the interpolation | ||
var placeholder = strWithLen(nameLength + 2); | ||
chunks.push('$' + placeholder); | ||
} else if (env === 'lokka' && /\.\.\.\s*$/.test(chunk)) { | ||
// This is Lokka-style fragment interpolation where you actually type the '...' yourself | ||
var _placeholder = strWithLen(nameLength + 3); | ||
chunks.push(_placeholder); | ||
} else if (env === 'relay') { | ||
// This is Relay-style fragment interpolation where you don't type '...' | ||
// Ellipsis cancels out extra characters | ||
var _placeholder2 = strWithLen(nameLength); | ||
chunks.push('...' + _placeholder2); | ||
} else if (!env || env === 'apollo') { | ||
// In Apollo, fragment interpolation is only valid outside of brackets | ||
// Since we don't know what we'd interpolate here (that occurs at runtime), | ||
// we're not going to do anything with this interpolation. | ||
} else { | ||
// Invalid interpolation | ||
context.report({ | ||
node: value, | ||
message: 'Invalid interpolation - not a valid fragment or variable.' | ||
}); | ||
throw new Error('Invalid interpolation'); | ||
} | ||
} | ||
}); | ||
return chunks.join(''); | ||
} | ||
function strWithLen(len) { | ||
// from http://stackoverflow.com/questions/14343844/create-a-string-of-variable-length-filled-with-a-repeated-character | ||
return new Array(len + 1).join('x'); | ||
} | ||
var gqlProcessor = { | ||
@@ -560,3 +336,3 @@ preprocess: function preprocess(text) { | ||
var escaped = text.replace(/[`\\]|\$\{/g, '\\$&'); | ||
return [internalTag + '`' + escaped + '`']; | ||
return [_constants.internalTag + '`' + escaped + '`']; | ||
}, | ||
@@ -563,0 +339,0 @@ postprocess: function postprocess(messages) { |
{ | ||
"name": "eslint-plugin-graphql", | ||
"version": "2.1.1", | ||
"version": "3.0.0", | ||
"description": "GraphQL ESLint plugin.", | ||
@@ -10,3 +10,5 @@ "author": "Sashko Stubailo", | ||
"prepublish": "babel ./src --ignore test --out-dir ./lib", | ||
"pretest": "node test/updateSchemaJson.js" | ||
"pretest": "node test/updateSchemaJson.js", | ||
"tav": "tav", | ||
"lint": "eslint 'src/**/*.js' 'test/**/*.js'" | ||
}, | ||
@@ -19,14 +21,13 @@ "homepage": "https://github.com/apollostack/eslint-plugin-graphql", | ||
"devDependencies": { | ||
"babel": "6.23.0", | ||
"babel-cli": "6.26.0", | ||
"babel-core": "6.26.3", | ||
"babel-eslint": "8.2.3", | ||
"babel-eslint": "10.0.1", | ||
"babel-plugin-transform-runtime": "6.23.0", | ||
"babel-preset-es2015": "6.24.1", | ||
"babel-preset-stage-0": "6.24.1", | ||
"eslint": "4.19.1", | ||
"mocha": "5.1.1", | ||
"graphql": "0.13.2", | ||
"graphql-tools": "3.0.0", | ||
"test-all-versions": "3.3.2" | ||
"eslint": "5.9.0", | ||
"graphql": "14.0.2", | ||
"graphql-tools": "4.0.3", | ||
"mocha": "5.2.0", | ||
"test-all-versions": "3.3.3" | ||
}, | ||
@@ -48,4 +49,4 @@ "babel": { | ||
"peerDependencies": { | ||
"graphql": "^0.12.0 || ^0.13.0" | ||
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0" | ||
} | ||
} |
@@ -19,2 +19,3 @@ # eslint-plugin-graphql | ||
3. [Lokka](https://github.com/kadirahq/lokka) | ||
3. [FraQL](https://github.com/smooth-code/fraql) | ||
@@ -35,5 +36,5 @@ If you want to lint your GraphQL schema, rather than queries, check out [cjoudrey/graphql-schema-linter](https://github.com/cjoudrey/graphql-schema-linter). | ||
All of the rules provided by this plugin have a few options in common. There are examples of how to use these with Apollo, Relay, Lokka and literal files further down. | ||
All of the rules provided by this plugin have a few options in common. There are examples of how to use these with Apollo, Relay, Lokka, FraQL and literal files further down. | ||
- `env`: Import default settings for your GraphQL client. Supported values: `'apollo'`, `'relay'`, `'lokka'`, `'literal'`. Defaults to `'apollo'`. This is used for the slight parsing differences in the GraphQL syntax between Apollo, Relay and Lokka, as well as giving nice defaults to some other options. | ||
- `env`: Import default settings for your GraphQL client. Supported values: `'apollo'`, `'relay'`, `'lokka'`, `'fraql'` `'literal'`. Defaults to `'apollo'`. This is used for the slight parsing differences in the GraphQL syntax between Apollo, Relay, Lokka and FraQL as well as giving nice defaults to some other options. | ||
@@ -44,3 +45,3 @@ - `tagName`: The name of the template literal tag that this plugin should look for when searching for GraphQL queries. It has different defaults depending on the `env` option: | ||
- `'internal'`: Special automatic value | ||
- others: `'gql'` | ||
- others: `'gql'`, `'graphql'` | ||
@@ -50,3 +51,3 @@ You also have to specify a schema. You can either do it using _one_ of these options: | ||
- `schemaJson`: Your schema as JSON. | ||
- `schemaJsonFilepath`: The absolute path to your schema as a .json file. | ||
- `schemaJsonFilepath`: The absolute path to your schema as a .json file. (Warning: this variant is incompatible with `eslint --cache`.) | ||
- `schemaString`: Your schema in the Schema Language format as a string. | ||
@@ -103,3 +104,3 @@ | ||
// Import default settings for your GraphQL client. Supported values: | ||
// 'apollo', 'relay', 'lokka', 'literal' | ||
// 'apollo', 'relay', 'lokka', 'fraql', 'literal' | ||
env: 'apollo', | ||
@@ -110,3 +111,3 @@ | ||
// OR provide absolute path to your schema JSON | ||
// OR provide absolute path to your schema JSON (but not if using `eslint --cache`!) | ||
// schemaJsonFilepath: path.resolve(__dirname, './schema.json'), | ||
@@ -135,3 +136,3 @@ | ||
// Import default settings for your GraphQL client. Supported values: | ||
// 'apollo', 'relay', 'lokka', 'literal' | ||
// 'apollo', 'relay', 'lokka', 'fraql', 'literal' | ||
env: 'relay', | ||
@@ -142,3 +143,3 @@ | ||
// OR provide absolute path to your schema JSON | ||
// OR provide absolute path to your schema JSON (but not if using `eslint --cache`!) | ||
// schemaJsonFilepath: path.resolve(__dirname, './schema.json'), | ||
@@ -167,3 +168,3 @@ | ||
// Import default settings for your GraphQL client. Supported values: | ||
// 'apollo', 'relay', 'lokka', 'literal' | ||
// 'apollo', 'relay', 'lokka', 'fraql', 'literal' | ||
env: 'lokka', | ||
@@ -174,2 +175,33 @@ | ||
// OR provide absolute path to your schema JSON (but not if using `eslint --cache`!) | ||
// schemaJsonFilepath: path.resolve(__dirname, './schema.json'), | ||
// OR provide the schema in the Schema Language format | ||
// schemaString: printSchema(schema), | ||
// Optional, the name of the template tag, defaults to 'gql' | ||
tagName: 'gql' | ||
}] | ||
}, | ||
plugins: [ | ||
'graphql' | ||
] | ||
} | ||
``` | ||
### Example config for FraQL | ||
```js | ||
// In a file called .eslintrc.js | ||
module.exports = { | ||
parser: "babel-eslint", | ||
rules: { | ||
"graphql/template-strings": ['error', { | ||
// Import default settings for your GraphQL client. Supported values: | ||
// 'apollo', 'relay', 'lokka', 'fraql', 'literal' | ||
env: 'fraql', | ||
// Import your schema JSON here | ||
schemaJson: require('./schema.json'), | ||
// OR provide absolute path to your schema JSON | ||
@@ -200,3 +232,3 @@ // schemaJsonFilepath: path.resolve(__dirname, './schema.json'), | ||
// Import default settings for your GraphQL client. Supported values: | ||
// 'apollo', 'relay', 'lokka', 'literal' | ||
// 'apollo', 'relay', 'lokka', 'fraql', 'literal' | ||
env: 'literal', | ||
@@ -207,3 +239,3 @@ | ||
// OR provide absolute path to your schema JSON | ||
// OR provide absolute path to your schema JSON (but not if using `eslint --cache`!) | ||
// schemaJsonFilepath: path.resolve(__dirname, './schema.json'), | ||
@@ -259,3 +291,3 @@ | ||
// Import default settings for your GraphQL client. Supported values: | ||
// 'apollo', 'relay', 'lokka', 'literal' | ||
// 'apollo', 'relay', 'lokka', 'fraql', 'literal' | ||
env: 'literal' | ||
@@ -334,3 +366,3 @@ // no need to specify schema here, it will be automatically determined using .graphqlconfig | ||
- `PossibleFragmentSpreads` | ||
- `ProvidedNonNullArguments` (*disabled by default in `relay`*) | ||
- `ProvidedRequiredArguments` (*disabled by default in `relay`*) | ||
- `ScalarLeafs` (*disabled by default in `relay`*) | ||
@@ -337,0 +369,0 @@ - `SingleFieldSubscriptions` |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
103611
10.3%11
-8.33%16
33.33%765
26.03%584
5.8%