Skip to content

Commit 9b9b976

Browse files
committed
Initial commit
0 parents  commit 9b9b976

9 files changed

+1538
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

CONTRIBUTING.md

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Contributing to eslint-plugin-relay
2+
3+
`eslint-plugin-relay` is one of Facebook's open source projects that is both under very active development and is also being used to ship code to everybody on [facebook.com](https://www.facebook.com). We're still working out the kinks to make contributing to this project as easy and transparent as possible, but we're not quite there yet. Hopefully this document makes the process for contributing clear and answers some questions that you may have.
4+
5+
## [Code of Conduct](https://code.facebook.com/codeofconduct)
6+
7+
Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please read [the full text](https://code.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated.
8+
9+
## Our Development Process
10+
11+
Unlike Relay, this project is developed directly and exclusively on GitHub. We intend to release updates quickly after changes are merged.
12+
13+
### Pull Requests
14+
15+
*Before* submitting a pull request, please make sure the following is done…
16+
17+
1. Fork the repo and create your branch from `master`.
18+
2. If you've added code that should be tested, add tests.
19+
3. Ensure the test suite passes (`yarn test` or `npm test`).
20+
4. Auto-format the code by running `yarn run prettier` or `npm run prettier`.
21+
5. If you haven't already, complete the CLA.
22+
23+
### Contributor License Agreement (CLA)
24+
25+
In order to accept your pull request, we need you to submit a CLA. You only need to do this once, so if you've done this for another Facebook open source project, you're good to go. If you are submitting a pull request for the first time, just let us know that you have completed the CLA and we can cross-check with your GitHub username.
26+
27+
[Complete your CLA here.](https://code.facebook.com/cla)
28+
29+
## Bugs & Questions
30+
31+
We will be using GitHub Issues bugs and feature requests. Before filing a new issue, make sure an issue for your problem doesn't already exist.
32+
33+
## License
34+
35+
By contributing to `eslint-plugin-relay`, you agree that your contributions will be licensed under its BSD license.

LICENSE

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
BSD License
2+
3+
For eslint-plugin-relay software
4+
5+
Copyright (c) 2013-present, Facebook, Inc.
6+
All rights reserved.
7+
8+
Redistribution and use in source and binary forms, with or without modification,
9+
are permitted provided that the following conditions are met:
10+
11+
* Redistributions of source code must retain the above copyright notice, this
12+
list of conditions and the following disclaimer.
13+
14+
* Redistributions in binary form must reproduce the above copyright notice,
15+
this list of conditions and the following disclaimer in the documentation
16+
and/or other materials provided with the distribution.
17+
18+
* Neither the name Facebook nor the names of its contributors may be used to
19+
endorse or promote products derived from this software without specific
20+
prior written permission.
21+
22+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
23+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
24+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
26+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
27+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
29+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
31+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

PATENTS

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
Additional Grant of Patent Rights Version 2
2+
3+
"Software" means the eslint-plugin-relay software distributed by Facebook, Inc.
4+
5+
Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software
6+
("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable
7+
(subject to the termination provision below) license under any Necessary
8+
Claims, to make, have made, use, sell, offer to sell, import, and otherwise
9+
transfer the Software. For avoidance of doubt, no license is granted under
10+
Facebook's rights in any patent claims that are infringed by (i) modifications
11+
to the Software made by you or any third party or (ii) the Software in
12+
combination with any software or other technology.
13+
14+
The license granted hereunder will terminate, automatically and without notice,
15+
if you (or any of your subsidiaries, corporate affiliates or agents) initiate
16+
directly or indirectly, or take a direct financial interest in, any Patent
17+
Assertion: (i) against Facebook or any of its subsidiaries or corporate
18+
affiliates, (ii) against any party if such Patent Assertion arises in whole or
19+
in part from any software, technology, product or service of Facebook or any of
20+
its subsidiaries or corporate affiliates, or (iii) against any party relating
21+
to the Software. Notwithstanding the foregoing, if Facebook or any of its
22+
subsidiaries or corporate affiliates files a lawsuit alleging patent
23+
infringement against you in the first instance, and you respond by filing a
24+
patent infringement counterclaim in that lawsuit against that party that is
25+
unrelated to the Software, the license granted hereunder will not terminate
26+
under section (i) of this paragraph due to such counterclaim.
27+
28+
A "Necessary Claim" is a claim of a patent owned by Facebook that is
29+
necessarily infringed by the Software standing alone.
30+
31+
A "Patent Assertion" is any lawsuit or other action alleging direct, indirect,
32+
or contributory infringement or inducement to infringe any patent, including a
33+
cross-claim or counterclaim.

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# eslint-plugin-relay
2+
3+
`eslint-plugin-relay` is a plugin for [ESLint](http://eslint.org/) to catch common problems in code using [Relay](https://facebook.github.io/relay/) early.
4+
5+
## Contribute
6+
7+
We actively welcome pull requests, learn how to [contribute](./CONTRIBUTING.md).
8+
9+
## License
10+
11+
eslint-plugin-relay is [BSD licensed](./LICENSE). We also provide an additional [patent grant](./PATENTS).

eslint-plugin-relay.js

+279
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
'use strict';
11+
12+
const graphql = require('graphql');
13+
14+
function getGraphQLTagName(tag) {
15+
if (tag.type === 'Identifier' && tag.name === 'graphql') {
16+
return 'graphql';
17+
} else if (
18+
tag.type === 'MemberExpression' &&
19+
tag.object.type === 'Identifier' &&
20+
tag.object.name === 'graphql' &&
21+
tag.property.type === 'Identifier' &&
22+
tag.property.name === 'experimental'
23+
) {
24+
return 'graphql.experimental';
25+
} else {
26+
return null;
27+
}
28+
}
29+
30+
function getGraphQLAST(taggedTemplateExpression) {
31+
if (!getGraphQLTagName(taggedTemplateExpression.tag)) {
32+
return null;
33+
}
34+
if (taggedTemplateExpression.quasi.quasis.length !== 1) {
35+
// has substitutions, covered by graphql-syntax rule
36+
return null;
37+
}
38+
const quasi = taggedTemplateExpression.quasi.quasis[0];
39+
try {
40+
return graphql.parse(quasi.value.cooked);
41+
} catch (error) {
42+
// Invalid syntax, covered by graphql-syntax rule
43+
return null;
44+
}
45+
}
46+
47+
const MODULE_NAME_REGEX = /(?:\/|^)(\w+)(?:\.[\w\.]+|\/index\.\w+)$/;
48+
function getModuleName(context) {
49+
const match = context.getFilename().match(MODULE_NAME_REGEX);
50+
if (match) {
51+
return match[1];
52+
}
53+
return null;
54+
}
55+
56+
/**
57+
* Returns a loc object for error reporting.
58+
*/
59+
function getLoc(context, templateNode, graphQLNode) {
60+
const [start, end] = getRange(context, templateNode, graphQLNode);
61+
return {
62+
start: context.getSourceCode().getLocFromIndex(start),
63+
end: context.getSourceCode().getLocFromIndex(end),
64+
};
65+
}
66+
67+
/**
68+
* Returns a range object for auto fixers.
69+
*/
70+
function getRange(context, templateNode, graphQLNode) {
71+
const graphQLStart = templateNode.quasi.quasis[0].start;
72+
return [
73+
graphQLStart + graphQLNode.loc.start,
74+
graphQLStart + graphQLNode.loc.end,
75+
];
76+
}
77+
78+
const CREATE_CONTAINER_FUNCTIONS = new Set([
79+
'createFragmentContainer',
80+
'createPaginationContainer',
81+
'createRefetchContainer',
82+
]);
83+
84+
function isCreateContainerCall(node) {
85+
const callee = node.callee;
86+
// prettier-ignore
87+
return (
88+
callee.type === 'Identifier' &&
89+
CREATE_CONTAINER_FUNCTIONS.has(callee.name)
90+
) || (
91+
callee.kind === 'MemberExpression' &&
92+
callee.object.type === 'Identifier' &&
93+
// Relay, relay, RelayCompat, etc.
94+
/relay/i.test(callee.object.value) &&
95+
callee.property.type === 'Identifier' &&
96+
CREATE_CONTAINER_FUNCTIONS.has(callee.property.name)
97+
);
98+
}
99+
100+
function calleeToString(callee) {
101+
if (callee.type) {
102+
return callee.name;
103+
}
104+
if (
105+
callee.kind === 'MemberExpression' &&
106+
callee.object.type === 'Identifier' &&
107+
callee.property.type === 'Identifier'
108+
) {
109+
return callee.object.value + '.' + callee.property.name;
110+
}
111+
return null;
112+
}
113+
114+
function validateTemplate(context, taggedTemplateExpression, keyName) {
115+
const ast = getGraphQLAST(taggedTemplateExpression);
116+
if (!ast) {
117+
return;
118+
}
119+
const moduleName = getModuleName(context);
120+
ast.definitions.forEach(def => {
121+
if (!def.name) {
122+
// no name, covered by graphql-naming/TaggedTemplateExpression
123+
return;
124+
}
125+
const definitionName = def.name.value;
126+
if (def.kind === 'FragmentDefinition') {
127+
if (keyName) {
128+
const expectedName = moduleName + '_' + keyName;
129+
if (definitionName !== expectedName) {
130+
context.report({
131+
loc: getLoc(context, taggedTemplateExpression, def.name),
132+
message:
133+
'Container fragment names must be `<ModuleName>_<propName>`. Got `{{actual}}`, expected `{{expected}}`.',
134+
data: {
135+
actual: definitionName,
136+
expected: expectedName,
137+
},
138+
fix: fixer =>
139+
fixer.replaceTextRange(
140+
getRange(context, taggedTemplateExpression, def.name),
141+
expectedName
142+
),
143+
});
144+
}
145+
}
146+
}
147+
});
148+
}
149+
150+
module.exports.rules = {
151+
'graphql-syntax': {
152+
meta: {
153+
docs: {
154+
description:
155+
'Validates the syntax of all graphql`...` and ' +
156+
'graphql.experimental`...` templates.',
157+
},
158+
},
159+
create(context) {
160+
return {
161+
TaggedTemplateExpression(node) {
162+
if (!getGraphQLTagName(node.tag)) {
163+
return;
164+
}
165+
const quasi = node.quasi.quasis[0];
166+
if (node.quasi.quasis.length !== 1) {
167+
context.report({
168+
node: node,
169+
message:
170+
'graphql tagged templates do not support ${...} substitutions.',
171+
});
172+
return;
173+
}
174+
try {
175+
const ast = graphql.parse(quasi.value.cooked);
176+
ast.definitions.forEach(definition => {
177+
if (!definition.name) {
178+
context.report({
179+
message: 'Operations in graphql tags require a name.',
180+
loc: getLoc(context, node, definition),
181+
});
182+
}
183+
});
184+
} catch (error) {
185+
context.report({
186+
node: node,
187+
message: '{{message}}',
188+
data: {message: error.message},
189+
});
190+
}
191+
},
192+
};
193+
},
194+
},
195+
'graphql-naming': {
196+
meta: {
197+
fixable: 'code',
198+
docs: {
199+
description: 'Validates naming conventions of graphql tags',
200+
},
201+
},
202+
create(context) {
203+
return {
204+
TaggedTemplateExpression(node) {
205+
const ast = getGraphQLAST(node);
206+
if (!ast) {
207+
return;
208+
}
209+
210+
ast.definitions.forEach(definition => {
211+
switch (definition.kind) {
212+
case 'OperationDefinition':
213+
const moduleName = getModuleName(context);
214+
const name = definition.name;
215+
if (!name) {
216+
return;
217+
}
218+
const operationName = name.value;
219+
220+
if (operationName.indexOf(moduleName) !== 0) {
221+
context.report({
222+
message:
223+
'Operations should start with the module name. Expected prefix `{{expected}}`, got `{{actual}}`.',
224+
data: {
225+
expected: moduleName,
226+
actual: operationName,
227+
},
228+
loc: getLoc(context, node, name),
229+
});
230+
}
231+
break;
232+
default:
233+
}
234+
});
235+
},
236+
CallExpression(node) {
237+
if (!isCreateContainerCall(node)) {
238+
return;
239+
}
240+
const fragments = node.arguments[1];
241+
if (fragments.type === 'ObjectExpression') {
242+
fragments.properties.forEach(property => {
243+
if (
244+
property.type === 'Property' &&
245+
property.key.type === 'Identifier' &&
246+
property.computed === false &&
247+
property.value.type === 'TaggedTemplateExpression'
248+
) {
249+
const tagName = getGraphQLTagName(property.value.tag);
250+
251+
if (!tagName) {
252+
context.report({
253+
node: property.value.tag,
254+
message:
255+
'`{{callee}}` expects GraphQL to be tagged with graphql`...` or graphql.experimental`...`.',
256+
data: {
257+
callee: calleeToString(node.callee),
258+
},
259+
});
260+
return;
261+
}
262+
validateTemplate(context, property.value, property.key.name);
263+
} else {
264+
context.report({
265+
node: property,
266+
message:
267+
'`{{callee}}` expects fragment definitions to be `key: graphql`.',
268+
data: {
269+
callee: calleeToString(node.callee),
270+
},
271+
});
272+
}
273+
});
274+
}
275+
},
276+
};
277+
},
278+
},
279+
};

0 commit comments

Comments
 (0)