├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── eslint-plugin-relay.js ├── eslint.config.mjs ├── package.json ├── src ├── rule-function-required-argument.js ├── rule-generated-typescript-types.js ├── rule-graphql-naming.js ├── rule-graphql-syntax.js ├── rule-hook-required-argument.js ├── rule-must-colocate-fragment-spreads.js ├── rule-no-future-added-value.js ├── rule-unused-fields.js └── utils.js ├── test ├── function-required-argument.js ├── future-added-value.js ├── generated-typescript-types.js ├── hook-required-argument.js ├── must-colocate-fragment-spreads.js ├── test.js └── unused-fields.js └── yarn.lock /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | name: CI 7 | 8 | on: [push, pull_request] 9 | 10 | jobs: 11 | build: 12 | name: Tests (Node ${{ matrix.node-version }}) 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [18.x, 20.x, 22.x] 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | cache: 'yarn' 23 | - name: Install dependencies 24 | run: yarn install --frozen-lockfile 25 | - name: Run tests 26 | run: yarn run test 27 | 28 | lint: 29 | name: Lint 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: actions/setup-node@v4 34 | with: 35 | node-version: 22.x 36 | cache: 'yarn' 37 | - name: Install dependencies 38 | run: yarn install --frozen-lockfile 39 | - name: Lint 40 | run: yarn run lint 41 | - name: Prettier 42 | run: yarn run prettier-check 43 | 44 | release: 45 | name: Publish to NPM 46 | runs-on: ubuntu-latest 47 | if: github.event_name == 'push' && github.repository == 'relayjs/eslint-plugin-relay' 48 | needs: [build, lint] 49 | steps: 50 | - uses: actions/checkout@v4 51 | - uses: actions/setup-node@v4 52 | with: 53 | node-version: 22.x 54 | registry-url: https://registry.npmjs.org/ 55 | cache: 'yarn' 56 | - name: Build latest (main) version 57 | if: github.ref == 'refs/heads/main' 58 | run: yarn version --no-git-tag-version --new-version 0.0.0-main-${GITHUB_SHA} 59 | - name: Check release version matches tag 60 | if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v') 61 | run: | 62 | if [ $(cat package.json | jq -r '.version') != "${GITHUB_REF_NAME:1}" ]; then 63 | echo "Version in package.json does not match tag. Did you forget to commit the package.json version bump?" 64 | exit 1 65 | fi 66 | - name: Publish to npm 67 | if: github.ref == 'refs/heads/main' || github.ref_type == 'tag' && startsWith(github.ref_name, 'v') 68 | run: npm publish --verbose ${TAG} 69 | env: 70 | TAG: ${{ github.ref == 'refs/heads/main' && '--tag=main' || ((contains(github.ref_name, '-rc.') && '--tag=dev') || '' )}} 71 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "bracketSameLine": true, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.0 2 | 3 | - Remove `relay/compat-uses-vars` and `relay/generated-flow-types` rules [493346f](https://github.com/relayjs/eslint-plugin-relay/commit/493346f), [5c313c6](https://github.com/relayjs/eslint-plugin-relay/commit/5c313c6) 4 | - Clean up legacy lint rules [b2251d6](https://github.com/relayjs/eslint-plugin-relay/commit/b2251d6) 5 | - Fix compatibility issues with ESLint 9 [2274f07](https://github.com/relayjs/eslint-plugin-relay/commit/2274f07) 6 | - Make compatible with typescript-eslint 8 [3203497](https://github.com/relayjs/eslint-plugin-relay/commit/3203497) 7 | - Add `generated-typescript-types` rule [31bfd44](https://github.com/relayjs/eslint-plugin-relay/commit/31bfd44), [f29444d](https://github.com/relayjs/eslint-plugin-relay/commit/f29444d), [0c91b68](https://github.com/relayjs/eslint-plugin-relay/commit/0c91b68), [ead1352](https://github.com/relayjs/eslint-plugin-relay/commit/ead1352), [105cf7f](https://github.com/relayjs/eslint-plugin-relay/commit/105cf7f) 8 | - Remove RelayCompat support [8b9beb1](https://github.com/relayjs/eslint-plugin-relay/commit/8b9beb1) 9 | - Require passing generated flow type for useMutation [3d1ebcd](https://github.com/relayjs/eslint-plugin-relay/commit/3d1ebcd) 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Facebook has adopted a Code of Conduct that we expect project participants to adhere to. 4 | Please read the [full text](https://code.fb.com/codeofconduct/) 5 | so that you can understand what actions will and will not be tolerated. 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 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 | ### Package Publishing 24 | 25 | - Every change that gets pushed to the `main` branch will be published as `0.0.0-main-SHA`. 26 | - For stable releases, the release author is expected to update the version in `package.json`, commit that, and create an accompanying tag. Once this is pushed a package will be published following that version. The workflow would look something like this: 27 | 28 | ```bash 29 | $ yarn version --minor 30 | $ git push --follow-tags 31 | ``` 32 | 33 | ### Contributor License Agreement (CLA) 34 | 35 | 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. 36 | 37 | [Complete your CLA here.](https://code.facebook.com/cla) 38 | 39 | ## Bugs & Questions 40 | 41 | 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. 42 | 43 | ## License 44 | 45 | By contributing to `eslint-plugin-relay`, you agree that your contributions will be licensed under its MIT license. 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-relay [![Build Status](https://travis-ci.org/relayjs/eslint-plugin-relay.svg?branch=master)](https://travis-ci.org/relayjs/eslint-plugin-relay) [![npm version](https://badge.fury.io/js/eslint-plugin-relay.svg)](http://badge.fury.io/js/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 | ## Install 6 | 7 | `npm i --save-dev eslint-plugin-relay` 8 | 9 | ## How To Use 10 | 11 | 1. Add `"relay"` to your eslint `plugins` section. 12 | 2. Add the relay rules such as `"relay/graphql-syntax": "error"` to your eslint `rules` section, see the example for all rules. 13 | 14 | Example .eslintrc.js: 15 | 16 | ```js 17 | module.exports = { 18 | // Other eslint properties here 19 | rules: { 20 | 'relay/graphql-syntax': 'error', 21 | 'relay/graphql-naming': 'error', 22 | 'relay/must-colocate-fragment-spreads': 'warn', 23 | 'relay/no-future-added-value': 'warn', 24 | 'relay/unused-fields': 'warn', 25 | 'relay/function-required-argument': 'warn', 26 | 'relay/hook-required-argument': 'warn' 27 | }, 28 | plugins: ['relay'] 29 | }; 30 | ``` 31 | 32 | You can also enable all the recommended or strict rules at once. 33 | Add `plugin:relay/recommended` or `plugin:relay/strict` in `extends`: 34 | 35 | ```js 36 | { 37 | "extends": [ 38 | "plugin:relay/recommended" 39 | ] 40 | } 41 | ``` 42 | 43 | ### Rule Descriptions 44 | 45 | Brief descriptions for each rule: 46 | 47 | - `relay/graphql-syntax`: Ensures each `graphql\`\`` tagged template literal contains syntactically valid GraphQL. This is also validated by the Relay Compiler, but the ESLint plugin can often provide faster feedback. 48 | - `relay/graphql-naming`: Ensures GraphQL fragments and queries follow Relay's naming conventions. This is also validated by the Relay Compiler, but the ESLint plugin can often provide faster feedback. 49 | - `relay/no-future-added-value`: Ensures code does not try to explicitly handle the `"%future added value"` enum variant which Relay inserts as a placeholder to ensure you handle the possibility that new enum variants may be added by the server after your application has been deployed. 50 | - `relay/unused-fields`: Ensures that every GraphQL field referenced is used within the module that includes it. This helps enable Relay's [optimal data fetching](https://relay.dev/blog/2023/10/24/how-relay-enables-optimal-data-fetching/) 51 | - `relay/function-required-argument`: Ensures that `readInlineData` is always passed an explicit argument even though that argument is allowed to be `undefined` at runtime. 52 | - `relay/hook-required-argument`: Ensures that Relay hooks are always passed an explicit argument even though that argument is allowed to be `undefined` at runtime. 53 | - `relay/must-colocate-fragment-spreads`: Ensures that when a fragment spread is added within a module, that module directly imports the module which defines that fragment. This prevents the anti-pattern when one component fetches a fragment that is not used by a direct child component. **Note**: This rule leans heavily on Meta's globally unique module names. It likely won't work well in other environments. 54 | 55 | ### Suppressing rules within graphql tags 56 | 57 | The following rules support suppression within graphql tags: 58 | 59 | - relay/unused-fields 60 | - relay/must-colocate-fragment-spreads 61 | 62 | Supported rules can be suppressed by adding `# eslint-disable-next-line relay/name-of-rule` to the preceding line: 63 | 64 | ```js 65 | graphql` 66 | fragment foo on Page { 67 | # eslint-disable-next-line relay/must-colocate-fragment-spreads 68 | ...unused1 69 | } 70 | `; 71 | ``` 72 | 73 | Note that only the `eslint-disable-next-line` form of suppression works. `eslint-disable-line` doesn't currently work until graphql-js provides support for [parsing Comment nodes](https://github.com/graphql/graphql-js/issues/2241) in their AST. 74 | 75 | ## Contribute 76 | 77 | We actively welcome pull requests, learn how to [contribute](./CONTRIBUTING.md). 78 | 79 | ## License 80 | 81 | `eslint-plugin-relay` is [MIT licensed](./LICENSE). 82 | -------------------------------------------------------------------------------- /eslint-plugin-relay.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | module.exports = { 11 | rules: { 12 | 'graphql-syntax': require('./src/rule-graphql-syntax'), 13 | 'graphql-naming': require('./src/rule-graphql-naming'), 14 | 'generated-typescript-types': require('./src/rule-generated-typescript-types'), 15 | 'no-future-added-value': require('./src/rule-no-future-added-value'), 16 | 'unused-fields': require('./src/rule-unused-fields'), 17 | 'must-colocate-fragment-spreads': require('./src/rule-must-colocate-fragment-spreads'), 18 | 'function-required-argument': require('./src/rule-function-required-argument'), 19 | 'hook-required-argument': require('./src/rule-hook-required-argument') 20 | }, 21 | configs: { 22 | recommended: { 23 | rules: { 24 | 'relay/graphql-syntax': 'error', 25 | 'relay/graphql-naming': 'error', 26 | 'relay/no-future-added-value': 'warn', 27 | 'relay/unused-fields': 'warn', 28 | 'relay/must-colocate-fragment-spreads': 'warn', 29 | 'relay/function-required-argument': 'warn', 30 | 'relay/hook-required-argument': 'warn' 31 | } 32 | }, 33 | 'ts-recommended': { 34 | rules: { 35 | 'relay/graphql-syntax': 'error', 36 | 'relay/graphql-naming': 'error', 37 | 'relay/generated-typescript-types': 'warn', 38 | 'relay/no-future-added-value': 'warn', 39 | 'relay/unused-fields': 'warn', 40 | 'relay/must-colocate-fragment-spreads': 'warn', 41 | 'relay/function-required-argument': 'warn', 42 | 'relay/hook-required-argument': 'warn' 43 | } 44 | }, 45 | strict: { 46 | rules: { 47 | 'relay/graphql-syntax': 'error', 48 | 'relay/graphql-naming': 'error', 49 | 'relay/no-future-added-value': 'error', 50 | 'relay/unused-fields': 'error', 51 | 'relay/must-colocate-fragment-spreads': 'error', 52 | 'relay/function-required-argument': 'error', 53 | 'relay/hook-required-argument': 'error' 54 | } 55 | }, 56 | 'ts-strict': { 57 | rules: { 58 | 'relay/graphql-syntax': 'error', 59 | 'relay/graphql-naming': 'error', 60 | 'relay/generated-typescript-types': 'error', 61 | 'relay/no-future-added-value': 'error', 62 | 'relay/unused-fields': 'error', 63 | 'relay/must-colocate-fragment-spreads': 'error', 64 | 'relay/function-required-argument': 'error', 65 | 'relay/hook-required-argument': 'error' 66 | } 67 | } 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'eslint/config'; 2 | import js from '@eslint/js'; 3 | import globals from 'globals'; 4 | 5 | export default defineConfig([ 6 | js.configs.recommended, 7 | { 8 | languageOptions: { 9 | globals: globals.node 10 | }, 11 | rules: { 12 | 'prefer-const': 'error', 13 | 'no-unused-vars': ['error', {argsIgnorePattern: '^_', caughtErrors: 'none'}], 14 | } 15 | } 16 | ]); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-relay", 3 | "version": "2.0.0", 4 | "description": "ESLint plugin for Relay.", 5 | "main": "eslint-plugin-relay", 6 | "repository": "relayjs/eslint-plugin-relay", 7 | "license": "MIT", 8 | "scripts": { 9 | "lint": "eslint eslint-plugin-relay.js src", 10 | "test": "mocha", 11 | "test-watch": "mocha --watch", 12 | "prettier": "prettier --write '**/*.js'", 13 | "prettier-check": "prettier --check '**/*.js'" 14 | }, 15 | "files": [ 16 | "eslint-plugin-relay.js", 17 | "src/", 18 | "LICENSE" 19 | ], 20 | "dependencies": { 21 | "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.26.10", 25 | "@babel/eslint-parser": "^7.27.0", 26 | "@babel/preset-flow": "^7.25.9", 27 | "@babel/preset-react": "^7.26.3", 28 | "@eslint/js": "^9.24.0", 29 | "@typescript-eslint/parser": "^8.29.1", 30 | "eslint": "^9.24.0", 31 | "globals": "^16.0.0", 32 | "mocha": "^9.1.3", 33 | "prettier": "^2.4.1", 34 | "typescript": "^5.8.3" 35 | }, 36 | "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" 37 | } 38 | -------------------------------------------------------------------------------- /src/rule-function-required-argument.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const {shouldLint} = require('./utils'); 11 | 12 | function reportMissingKeyArgument(node, context) { 13 | context.report({ 14 | node: node, 15 | message: `A fragment reference should be passed to the \`readInlineData\` function` 16 | }); 17 | } 18 | 19 | module.exports = { 20 | meta: { 21 | docs: { 22 | description: 23 | 'Validates that the second argument is passed to relay functions.' 24 | } 25 | }, 26 | create(context) { 27 | if (!shouldLint(context)) { 28 | return {}; 29 | } 30 | 31 | return { 32 | 'CallExpression[callee.name=readInlineData][arguments.length < 2]'(node) { 33 | reportMissingKeyArgument(node, context); 34 | } 35 | }; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/rule-generated-typescript-types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const utils = require('./utils'); 11 | const shouldLint = utils.shouldLint; 12 | const getGraphQLAST = utils.getGraphQLAST; 13 | 14 | const DEFAULT_TYPESCRIPT_TYPES_OPTIONS = { 15 | fix: false, 16 | haste: false 17 | }; 18 | 19 | function getOptions(optionValue) { 20 | if (optionValue) { 21 | return { 22 | fix: optionValue.fix || DEFAULT_TYPESCRIPT_TYPES_OPTIONS.fix, 23 | haste: optionValue.haste || DEFAULT_TYPESCRIPT_TYPES_OPTIONS.haste 24 | }; 25 | } 26 | return DEFAULT_TYPESCRIPT_TYPES_OPTIONS; 27 | } 28 | 29 | function getTypeImportName(node) { 30 | return (node.specifiers[0].local || node.specifiers[0].imported).name; 31 | } 32 | 33 | function genImportFixRange(type, imports, requires) { 34 | const typeImports = imports.filter(node => node.importKind === 'type'); 35 | const alreadyHasImport = typeImports.some(node => 36 | node.specifiers.some( 37 | specifier => (specifier.imported || specifier.local).name === type 38 | ) 39 | ); 40 | if (alreadyHasImport) { 41 | return null; 42 | } 43 | if (typeImports.length > 0) { 44 | let precedingImportIndex = 0; 45 | while ( 46 | typeImports[precedingImportIndex + 1] && 47 | getTypeImportName(typeImports[precedingImportIndex + 1]) < type 48 | ) { 49 | precedingImportIndex++; 50 | } 51 | return typeImports[precedingImportIndex].range; 52 | } 53 | if (imports.length > 0) { 54 | return imports[imports.length - 1].range; 55 | } 56 | if (requires.length > 0) { 57 | return requires[requires.length - 1].range; 58 | } 59 | // start of file 60 | return [0, 0]; 61 | } 62 | 63 | function genImportFixer(fixer, importFixRange, type, haste, whitespace) { 64 | if (!importFixRange) { 65 | // HACK: insert nothing 66 | return fixer.replaceTextRange([0, 0], ''); 67 | } 68 | if (haste) { 69 | return fixer.insertTextAfterRange( 70 | importFixRange, 71 | `\n${whitespace}import type {${type}} from '${type}.graphql'` 72 | ); 73 | } else { 74 | return fixer.insertTextAfterRange( 75 | importFixRange, 76 | `\n${whitespace}import type {${type}} from './__generated__/${type}.graphql'` 77 | ); 78 | } 79 | } 80 | 81 | function getPropTypeProperty( 82 | context, 83 | typeAliasMap, 84 | propType, 85 | propName, 86 | visitedProps = new Set() 87 | ) { 88 | if (propType == null || visitedProps.has(propType)) { 89 | return null; 90 | } 91 | visitedProps.add(propType); 92 | const spreadsToVisit = []; 93 | if (propType.type === 'TSTypeReference') { 94 | return getPropTypeProperty( 95 | context, 96 | typeAliasMap, 97 | extractReadOnlyType(resolveTypeAlias(propType, typeAliasMap)), 98 | propName, 99 | visitedProps 100 | ); 101 | } 102 | if (propType.type !== 'TSTypeLiteral') { 103 | return null; 104 | } 105 | for (const property of propType.members) { 106 | if (property.type === 'ObjectTypeSpreadProperty') { 107 | spreadsToVisit.push(property); 108 | } else { 109 | if (property.key.name === propName) { 110 | return property; 111 | } 112 | } 113 | } 114 | for (const property of spreadsToVisit) { 115 | if ( 116 | property.argument && 117 | property.argument.id && 118 | property.argument.id.name 119 | ) { 120 | const nextPropType = typeAliasMap[property.argument.id.name]; 121 | const result = getPropTypeProperty( 122 | context, 123 | typeAliasMap, 124 | nextPropType, 125 | propName, 126 | visitedProps 127 | ); 128 | if (result) { 129 | return result; 130 | } 131 | } 132 | } 133 | return null; 134 | } 135 | 136 | function validateObjectTypeAnnotation( 137 | context, 138 | Component, 139 | type, 140 | propName, 141 | propType, 142 | importFixRange, 143 | typeAliasMap, 144 | onlyVerify 145 | ) { 146 | const options = getOptions(context.options[0]); 147 | const propTypeProperty = getPropTypeProperty( 148 | context, 149 | typeAliasMap, 150 | propType, 151 | propName 152 | ); 153 | 154 | const atleastOnePropertyExists = !!propType.members[0]; 155 | 156 | if (!propTypeProperty) { 157 | if (onlyVerify) { 158 | return false; 159 | } 160 | context.report({ 161 | message: 162 | '`{{prop}}` is not declared in the `props` of the React component or it is not marked with the ' + 163 | 'generated typescript type `{{type}}`. See ' + 164 | 'https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 165 | data: { 166 | prop: propName, 167 | type 168 | }, 169 | fix: options.fix 170 | ? fixer => { 171 | const whitespace = ' '.repeat(Component.parent.loc.start.column); 172 | const fixes = [ 173 | genImportFixer( 174 | fixer, 175 | importFixRange, 176 | type, 177 | options.haste, 178 | whitespace 179 | ) 180 | ]; 181 | if (atleastOnePropertyExists) { 182 | fixes.push( 183 | fixer.insertTextBefore( 184 | propType.members[0], 185 | `${propName}: ${type}, ` 186 | ) 187 | ); 188 | } else { 189 | fixes.push(fixer.replaceText(propType, `{${propName}: ${type}}`)); 190 | } 191 | return fixes; 192 | } 193 | : null, 194 | loc: Component.loc 195 | }); 196 | return false; 197 | } 198 | if ( 199 | propTypeProperty.type === 'TSPropertySignature' && 200 | propTypeProperty.typeAnnotation.type === 'TSTypeAnnotation' 201 | ) { 202 | // If we have a TSTypeAnnotation here, it must be a TSTypeReference to the generated type, otherwise we have an invalid reference here 203 | if ( 204 | propTypeProperty.typeAnnotation.typeAnnotation.type === 205 | 'TSTypeReference' && 206 | propTypeProperty.typeAnnotation.typeAnnotation.typeName.name === type 207 | ) { 208 | return true; 209 | } 210 | 211 | if (onlyVerify) { 212 | return false; 213 | } 214 | 215 | context.report({ 216 | message: 217 | 'Component property `{{prop}}` expects to use the generated ' + 218 | '`{{type}}` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 219 | data: { 220 | prop: propName, 221 | type 222 | }, 223 | fix: options.fix 224 | ? fixer => { 225 | const whitespace = ' '.repeat(Component.parent.loc.start.column); 226 | return [ 227 | genImportFixer( 228 | fixer, 229 | importFixRange, 230 | type, 231 | options.haste, 232 | whitespace 233 | ), 234 | fixer.replaceText( 235 | propTypeProperty.typeAnnotation.typeAnnotation, 236 | type 237 | ) 238 | ]; 239 | } 240 | : null, 241 | loc: Component.loc 242 | }); 243 | return false; 244 | } 245 | if ( 246 | propTypeProperty.type !== 'TSTypeReference' || 247 | propTypeProperty.value.id.name !== type 248 | ) { 249 | if (onlyVerify) { 250 | return false; 251 | } 252 | context.report({ 253 | message: 254 | 'Component property `{{prop}}` expects to use the generated ' + 255 | '`{{type}}` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 256 | data: { 257 | prop: propName, 258 | type 259 | }, 260 | fix: options.fix 261 | ? fixer => { 262 | const whitespace = ' '.repeat(Component.parent.loc.start.column); 263 | return [ 264 | genImportFixer( 265 | fixer, 266 | importFixRange, 267 | type, 268 | options.haste, 269 | whitespace 270 | ), 271 | fixer.replaceText(propTypeProperty.value, type) 272 | ]; 273 | } 274 | : null, 275 | loc: Component.loc 276 | }); 277 | return false; 278 | } 279 | return true; 280 | } 281 | 282 | function extractReadOnlyType(genericType) { 283 | let currentType = genericType; 284 | while ( 285 | currentType != null && 286 | currentType.type === 'TSTypeReference' && 287 | currentType.id.name === '$ReadOnly' && 288 | currentType.typeArguments && 289 | currentType.typeArguments.type === 'TSTypeParameterInstantiation' && 290 | Array.isArray(currentType.typeArguments.params) && 291 | currentType.typeArguments.params.length === 1 292 | ) { 293 | currentType = currentType.typeArguments.params[0]; 294 | } 295 | return currentType; 296 | } 297 | 298 | function resolveTypeAlias(genericType, typeAliasMap) { 299 | let currentType = genericType; 300 | while ( 301 | currentType != null && 302 | currentType.type === 'TSTypeReference' && 303 | typeAliasMap[currentType.typeName.name] != null 304 | ) { 305 | currentType = typeAliasMap[currentType.typeName.name]; 306 | } 307 | return currentType; 308 | } 309 | 310 | module.exports = { 311 | meta: { 312 | fixable: 'code', 313 | docs: { 314 | description: 'Validates usage of RelayModern generated typescript types' 315 | }, 316 | schema: [ 317 | { 318 | type: 'object', 319 | properties: { 320 | fix: { 321 | type: 'boolean' 322 | }, 323 | haste: { 324 | type: 'boolean' 325 | } 326 | }, 327 | additionalProperties: false 328 | } 329 | ] 330 | }, 331 | create(context) { 332 | const sourceCode = context.sourceCode ?? context.getSourceCode(); 333 | if (!shouldLint(context)) { 334 | return {}; 335 | } 336 | const options = getOptions(context.options[0]); 337 | const componentMap = {}; 338 | const expectedTypes = []; 339 | const imports = []; 340 | const requires = []; 341 | const typeAliasMap = {}; 342 | const useFragmentInstances = []; 343 | 344 | /** 345 | * Tries to find a GraphQL definition node for a given argument. 346 | * Supports a graphql`...` literal inline and follows variable definitions. 347 | */ 348 | function getDefinition(arg) { 349 | if (arg == null) { 350 | return null; 351 | } 352 | if (arg.type === 'Identifier') { 353 | const name = arg.name; 354 | let scope = sourceCode.getScope 355 | ? sourceCode.getScope(arg) 356 | : context.getScope(); 357 | while (scope != null) { 358 | for (const variable of scope.variables) { 359 | if (variable.name === name) { 360 | const definition = variable.defs.find( 361 | def => def.node && def.node.type === 'VariableDeclarator' 362 | ); 363 | return definition ? getDefinition(definition.node.init) : null; 364 | } 365 | } 366 | scope = scope.upper; 367 | } 368 | return null; 369 | } 370 | if (arg.type !== 'TaggedTemplateExpression') { 371 | return null; 372 | } 373 | return getGraphQLAST(arg); 374 | } 375 | 376 | function getDefinitionName(arg) { 377 | const ast = getDefinition(arg); 378 | if (ast == null || ast.definitions.length === 0) { 379 | return null; 380 | } 381 | return ast.definitions[0].name.value; 382 | } 383 | 384 | function getRefetchableQueryName(arg) { 385 | const ast = getDefinition(arg); 386 | if (ast == null || ast.definitions.length === 0) { 387 | return null; 388 | } 389 | const refetchable = ast.definitions[0].directives.find( 390 | d => d.name.value === 'refetchable' 391 | ); 392 | if (!refetchable) { 393 | return null; 394 | } 395 | const nameArg = refetchable.arguments.find( 396 | a => a.name.value === 'queryName' 397 | ); 398 | return nameArg && nameArg.value && nameArg.value.value 399 | ? nameArg.value.value 400 | : null; 401 | } 402 | 403 | function trackHookCall(node, hookName) { 404 | const firstArg = node.arguments[0]; 405 | if (firstArg == null) { 406 | return; 407 | } 408 | const fragmentName = getDefinitionName(firstArg); 409 | if (fragmentName == null) { 410 | return; 411 | } 412 | useFragmentInstances.push({ 413 | fragmentName: fragmentName, 414 | node: node, 415 | hookName: hookName 416 | }); 417 | } 418 | 419 | function createTypeImportFixer(node, operationName, typeText) { 420 | return fixer => { 421 | const importFixRange = genImportFixRange( 422 | operationName, 423 | imports, 424 | requires 425 | ); 426 | return [ 427 | genImportFixer( 428 | fixer, 429 | importFixRange, 430 | operationName, 431 | options.haste, 432 | '' 433 | ), 434 | fixer.insertTextAfter(node.callee, `<${typeText}>`) 435 | ]; 436 | }; 437 | } 438 | 439 | function reportAndFixRefetchableType(node, hookName, defaultQueryName) { 440 | const queryName = getRefetchableQueryName(node.arguments[0]); 441 | context.report({ 442 | node: node, 443 | message: `The \`${hookName}\` hook should be used with an explicit generated Typescript type, e.g.: ${hookName}<{{queryName}}>(...)`, 444 | data: { 445 | queryName: queryName || defaultQueryName 446 | }, 447 | fix: 448 | queryName != null && options.fix 449 | ? createTypeImportFixer(node, queryName, `${queryName}`) 450 | : null 451 | }); 452 | } 453 | 454 | return { 455 | ImportDeclaration(node) { 456 | imports.push(node); 457 | }, 458 | VariableDeclarator(node) { 459 | if ( 460 | node.init && 461 | node.init.type === 'CallExpression' && 462 | node.init.callee.name === 'require' 463 | ) { 464 | requires.push(node); 465 | } 466 | }, 467 | TSTypeAliasDeclaration(node) { 468 | typeAliasMap[node.id.name] = node.typeAnnotation; 469 | }, 470 | 471 | /** 472 | * Find useQuery() calls without type arguments. 473 | */ 474 | 'CallExpression[callee.name=useQuery]:not([typeArguments])'(node) { 475 | const firstArg = node.arguments[0]; 476 | if (firstArg == null) { 477 | return; 478 | } 479 | const queryName = getDefinitionName(firstArg); 480 | context.report({ 481 | node: node, 482 | message: 483 | 'The `useQuery` hook should be used with an explicit generated Typescript type, e.g.: useQuery<{{queryName}}>(...)', 484 | data: { 485 | queryName: queryName || 'ExampleQuery' 486 | }, 487 | fix: 488 | queryName != null && options.fix 489 | ? createTypeImportFixer(node, queryName, queryName) 490 | : null 491 | }); 492 | }, 493 | 494 | /** 495 | * Find useLazyLoadQuery() calls without type arguments. 496 | */ 497 | 'CallExpression[callee.name=useLazyLoadQuery]:not([typeArguments])'( 498 | node 499 | ) { 500 | const firstArg = node.arguments[0]; 501 | if (firstArg == null) { 502 | return; 503 | } 504 | const queryName = getDefinitionName(firstArg); 505 | context.report({ 506 | node: node, 507 | message: 508 | 'The `useLazyLoadQuery` hook should be used with an explicit generated Typescript type, e.g.: useLazyLoadQuery<{{queryName}}>(...)', 509 | data: { 510 | queryName: queryName || 'ExampleQuery' 511 | }, 512 | fix: 513 | queryName != null && options.fix 514 | ? createTypeImportFixer(node, queryName, queryName) 515 | : null 516 | }); 517 | }, 518 | 519 | /** 520 | * Find commitMutation() calls without type arguments. 521 | */ 522 | 'CallExpression[callee.name=commitMutation]:not([typeArguments])'(node) { 523 | // Get mutation config. It should be second argument of the `commitMutation` 524 | const mutationConfig = node.arguments && node.arguments[1]; 525 | if ( 526 | mutationConfig == null || 527 | mutationConfig.type !== 'ObjectExpression' 528 | ) { 529 | return; 530 | } 531 | // Find `mutation` property on the `mutationConfig` 532 | const mutationNameProperty = mutationConfig.properties.find( 533 | prop => prop.key != null && prop.key.name === 'mutation' 534 | ); 535 | if ( 536 | mutationNameProperty == null || 537 | mutationNameProperty.value == null 538 | ) { 539 | return; 540 | } 541 | const mutationName = getDefinitionName(mutationNameProperty.value); 542 | context.report({ 543 | node: node, 544 | message: 545 | 'The `commitMutation` must be used with an explicit generated Typescript type, e.g.: commitMutation<{{mutationName}}>(...)', 546 | data: { 547 | mutationName: mutationName || 'ExampleMutation' 548 | }, 549 | fix: 550 | mutationName != null && options.fix 551 | ? createTypeImportFixer(node, mutationName, mutationName) 552 | : null 553 | }); 554 | }, 555 | 556 | /** 557 | * Find requestSubscription() calls without type arguments. 558 | */ 559 | 'CallExpression[callee.name=requestSubscription]:not([typeArguments])'( 560 | node 561 | ) { 562 | const subscriptionConfig = node.arguments && node.arguments[1]; 563 | if ( 564 | subscriptionConfig == null || 565 | subscriptionConfig.type !== 'ObjectExpression' 566 | ) { 567 | return; 568 | } 569 | const subscriptionNameProperty = subscriptionConfig.properties.find( 570 | prop => prop.key != null && prop.key.name === 'subscription' 571 | ); 572 | 573 | if ( 574 | subscriptionNameProperty == null || 575 | subscriptionNameProperty.value == null 576 | ) { 577 | return; 578 | } 579 | const subscriptionName = getDefinitionName( 580 | subscriptionNameProperty.value 581 | ); 582 | context.report({ 583 | node: node, 584 | message: 585 | 'The `requestSubscription` must be used with an explicit generated Typescript type, e.g.: requestSubscription<{{subscriptionName}}>(...)', 586 | data: { 587 | subscriptionName: subscriptionName || 'ExampleSubscription' 588 | }, 589 | fix: 590 | subscriptionName != null && options.fix 591 | ? createTypeImportFixer(node, subscriptionName, subscriptionName) 592 | : null 593 | }); 594 | }, 595 | 596 | /** 597 | * Find useMutation() calls without type arguments. 598 | */ 599 | 'CallExpression[callee.name=useMutation]:not([typeArguments])'(node) { 600 | const queryName = getDefinitionName(node.arguments[0]); 601 | context.report({ 602 | node, 603 | message: `The \`useMutation\` hook should be used with an explicit generated Typescript type, e.g.: useMutation<{{queryName}}>(...)`, 604 | data: { 605 | queryName: queryName 606 | }, 607 | fix: 608 | queryName != null && options.fix 609 | ? createTypeImportFixer(node, queryName, queryName) 610 | : null 611 | }); 612 | }, 613 | /** 614 | * Find usePaginationFragment() calls without type arguments. 615 | */ 616 | 'CallExpression[callee.name=usePaginationFragment]:not([typeArguments])'( 617 | node 618 | ) { 619 | reportAndFixRefetchableType( 620 | node, 621 | 'usePaginationFragment', 622 | 'PaginationQuery' 623 | ); 624 | }, 625 | 626 | /** 627 | * Find useBlockingPaginationFragment() calls without type arguments. 628 | */ 629 | 'CallExpression[callee.name=useBlockingPaginationFragment]:not([typeArguments])'( 630 | node 631 | ) { 632 | reportAndFixRefetchableType( 633 | node, 634 | 'useBlockingPaginationFragment', 635 | 'PaginationQuery' 636 | ); 637 | }, 638 | 639 | /** 640 | * Find useLegacyPaginationFragment() calls without type arguments. 641 | */ 642 | 'CallExpression[callee.name=useLegacyPaginationFragment]:not([typeArguments])'( 643 | node 644 | ) { 645 | reportAndFixRefetchableType( 646 | node, 647 | 'useLegacyPaginationFragment', 648 | 'PaginationQuery' 649 | ); 650 | }, 651 | 652 | /** 653 | * Find useRefetchableFragment() calls without type arguments. 654 | */ 655 | 'CallExpression[callee.name=useRefetchableFragment]:not([typeArguments])'( 656 | node 657 | ) { 658 | reportAndFixRefetchableType( 659 | node, 660 | 'useRefetchableFragment', 661 | 'RefetchableQuery' 662 | ); 663 | }, 664 | 665 | /** 666 | * useFragment() calls 667 | */ 668 | 'CallExpression[callee.name=useFragment]'(node) { 669 | trackHookCall(node, 'useFragment'); 670 | }, 671 | 672 | /** 673 | * usePaginationFragment() calls 674 | */ 675 | 'CallExpression[callee.name=usePaginationFragment]'(node) { 676 | trackHookCall(node, 'usePaginationFragment'); 677 | }, 678 | 679 | /** 680 | * useBlockingPaginationFragment() calls 681 | */ 682 | 'CallExpression[callee.name=useBlockingPaginationFragment]'(node) { 683 | trackHookCall(node, 'useBlockingPaginationFragment'); 684 | }, 685 | 686 | /** 687 | * useLegacyPaginationFragment() calls 688 | */ 689 | 'CallExpression[callee.name=useLegacyPaginationFragment]'(node) { 690 | trackHookCall(node, 'useLegacyPaginationFragment'); 691 | }, 692 | 693 | /** 694 | * useRefetchableFragment() calls 695 | */ 696 | 'CallExpression[callee.name=useRefetchableFragment]'(node) { 697 | trackHookCall(node, 'useRefetchableFragment'); 698 | }, 699 | 700 | ClassDeclaration(node) { 701 | const componentName = node.id.name; 702 | componentMap[componentName] = { 703 | Component: node.id 704 | }; 705 | // new style React.Component accepts 'props' as the first parameter 706 | if (node.superTypeArguments && node.superTypeArguments.params[0]) { 707 | componentMap[componentName].propType = 708 | node.superTypeArguments.params[0]; 709 | } 710 | }, 711 | TaggedTemplateExpression(node) { 712 | const ast = getGraphQLAST(node); 713 | if (!ast) { 714 | return; 715 | } 716 | ast.definitions.forEach(def => { 717 | if (!def.name) { 718 | // no name, covered by graphql-naming/TaggedTemplateExpression 719 | return; 720 | } 721 | if (def.kind === 'FragmentDefinition') { 722 | expectedTypes.push(def.name.value); 723 | } 724 | }); 725 | }, 726 | 'Program:exit': function (_node) { 727 | useFragmentInstances.forEach(useFragmentInstance => { 728 | const fragmentName = useFragmentInstance.fragmentName; 729 | const hookName = useFragmentInstance.hookName; 730 | const node = useFragmentInstance.node; 731 | const foundImport = imports.some(importDeclaration => { 732 | const importedFromModuleName = importDeclaration.source.value; 733 | // `includes()` to allow a suffix like `.js` or path prefixes 734 | if (!importedFromModuleName.includes(fragmentName + '.graphql')) { 735 | return false; 736 | } 737 | // import {...} from '...'; 738 | return importDeclaration.specifiers.some( 739 | specifier => 740 | specifier.type === 'ImportSpecifier' && 741 | specifier.imported.name === fragmentName + '$key' 742 | ); 743 | }); 744 | 745 | if (foundImport) { 746 | return; 747 | } 748 | 749 | // Check if the fragment ref that we're passing to the hook 750 | // comes from a previous useFragment (or variants) hook call. 751 | const fragmentRefArgName = 752 | node.arguments[1] != null ? node.arguments[1].name : null; 753 | const foundFragmentRefDeclaration = useFragmentInstances.some( 754 | _useFragmentInstance => { 755 | if (_useFragmentInstance === useFragmentInstance) { 756 | return false; 757 | } 758 | const variableDeclaratorNode = _useFragmentInstance.node.parent; 759 | if ( 760 | !variableDeclaratorNode || 761 | !variableDeclaratorNode.id || 762 | !variableDeclaratorNode.id.type 763 | ) { 764 | return false; 765 | } 766 | if (variableDeclaratorNode.id.type === 'Identifier') { 767 | return ( 768 | fragmentRefArgName != null && 769 | variableDeclaratorNode.id.name === fragmentRefArgName 770 | ); 771 | } 772 | if ( 773 | variableDeclaratorNode.id.type === 'ObjectPattern' && 774 | variableDeclaratorNode.id.properties != null 775 | ) { 776 | return variableDeclaratorNode.id.properties.some(prop => { 777 | return ( 778 | fragmentRefArgName != null && 779 | prop && 780 | prop.value && 781 | prop.value.name === fragmentRefArgName 782 | ); 783 | }); 784 | } 785 | return false; 786 | } 787 | ); 788 | 789 | if (foundFragmentRefDeclaration) { 790 | return; 791 | } 792 | 793 | context.report({ 794 | node: node, 795 | message: 796 | 'The prop passed to {{hookName}}() should be typed with the ' + 797 | "type '{{name}}$key' imported from '{{name}}.graphql', " + 798 | 'e.g.:\n' + 799 | '\n' + 800 | " import type {{{name}}$key} from '{{name}}.graphql';", 801 | data: { 802 | name: fragmentName, 803 | hookName: hookName 804 | } 805 | }); 806 | }); 807 | expectedTypes.forEach(type => { 808 | const componentName = type.split('_')[0]; 809 | const propName = type.split('_').slice(1).join('_'); 810 | if (!componentName || !propName || !componentMap[componentName]) { 811 | // incorrect name, covered by graphql-naming/CallExpression 812 | return; 813 | } 814 | const Component = componentMap[componentName].Component; 815 | const propType = componentMap[componentName].propType; 816 | 817 | // resolve local type alias 818 | const importedPropType = imports.reduce((acc, node) => { 819 | if (node.specifiers) { 820 | const typeSpecifier = node.specifiers.find(specifier => { 821 | if (specifier.type !== 'ImportSpecifier') { 822 | return false; 823 | } 824 | return specifier.imported.name === type; 825 | }); 826 | if (typeSpecifier) { 827 | return typeSpecifier.local.name; 828 | } 829 | } 830 | return acc; 831 | }, type); 832 | 833 | const importFixRange = genImportFixRange( 834 | importedPropType, 835 | imports, 836 | requires 837 | ); 838 | 839 | if (propType) { 840 | // There exists a prop typeAnnotation. Let's look at how it's 841 | // structured 842 | switch (propType.type) { 843 | case 'TSTypeLiteral': { 844 | validateObjectTypeAnnotation( 845 | context, 846 | Component, 847 | importedPropType, 848 | propName, 849 | propType, 850 | importFixRange, 851 | typeAliasMap 852 | ); 853 | break; 854 | } 855 | case 'TSTypeReference': { 856 | const aliasedObjectType = extractReadOnlyType( 857 | resolveTypeAlias(propType, typeAliasMap) 858 | ); 859 | if (!aliasedObjectType) { 860 | // The type Alias doesn't exist, is invalid, or is being 861 | // imported. Can't do anything. 862 | break; 863 | } 864 | switch (aliasedObjectType.type) { 865 | case 'TSTypeLiteral': { 866 | validateObjectTypeAnnotation( 867 | context, 868 | Component, 869 | importedPropType, 870 | propName, 871 | aliasedObjectType, 872 | importFixRange, 873 | typeAliasMap 874 | ); 875 | break; 876 | } 877 | case 'TSIntersectionType': { 878 | const objectTypes = aliasedObjectType.types 879 | .map(intersectedType => { 880 | if (intersectedType.type === 'TSTypeReference') { 881 | return extractReadOnlyType( 882 | resolveTypeAlias(intersectedType, typeAliasMap) 883 | ); 884 | } 885 | if (intersectedType.type === 'TSTypeLiteral') { 886 | return intersectedType; 887 | } 888 | }) 889 | .filter(maybeObjectType => { 890 | // TSTypeReference may not map to an object type 891 | return ( 892 | maybeObjectType && 893 | maybeObjectType.type === 'TSTypeLiteral' 894 | ); 895 | }); 896 | if (!objectTypes.length) { 897 | // The type Alias is likely being imported. 898 | // Can't do anything. 899 | break; 900 | } 901 | for (const objectType of objectTypes) { 902 | const isValid = validateObjectTypeAnnotation( 903 | context, 904 | Component, 905 | importedPropType, 906 | propName, 907 | objectType, 908 | importFixRange, 909 | typeAliasMap, 910 | true // Return false if invalid instead of reporting 911 | ); 912 | if (isValid) { 913 | break; 914 | } 915 | } 916 | // otherwise report an error at the first object 917 | validateObjectTypeAnnotation( 918 | context, 919 | Component, 920 | importedPropType, 921 | propName, 922 | objectTypes[0], 923 | importFixRange, 924 | typeAliasMap 925 | ); 926 | break; 927 | } 928 | } 929 | break; 930 | } 931 | } 932 | } else { 933 | context.report({ 934 | message: 935 | 'Component property `{{prop}}` expects to use the ' + 936 | 'generated `{{type}}` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 937 | data: { 938 | prop: propName, 939 | type: importedPropType 940 | }, 941 | fix: options.fix 942 | ? fixer => { 943 | const classBodyStart = Component.parent.body.body[0]; 944 | if (!classBodyStart) { 945 | // HACK: There's nothing in the body. Let's not do anything 946 | // When something is added to the body, we'll have a fix 947 | return; 948 | } 949 | const aliasWhitespace = ' '.repeat( 950 | Component.parent.loc.start.column 951 | ); 952 | const propsWhitespace = ' '.repeat( 953 | classBodyStart.loc.start.column 954 | ); 955 | return [ 956 | genImportFixer( 957 | fixer, 958 | importFixRange, 959 | importedPropType, 960 | options.haste, 961 | aliasWhitespace 962 | ), 963 | fixer.insertTextBefore( 964 | Component.parent, 965 | `type Props = {${propName}: ` + 966 | `${importedPropType}};\n\n${aliasWhitespace}` 967 | ), 968 | fixer.insertTextBefore( 969 | classBodyStart, 970 | `props: Props;\n\n${propsWhitespace}` 971 | ) 972 | ]; 973 | } 974 | : null, 975 | loc: Component.loc 976 | }); 977 | } 978 | }); 979 | } 980 | }; 981 | } 982 | }; 983 | -------------------------------------------------------------------------------- /src/rule-graphql-naming.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const utils = require('./utils'); 11 | const getGraphQLAST = utils.getGraphQLAST; 12 | const getLoc = utils.getLoc; 13 | const getModuleName = utils.getModuleName; 14 | const getRange = utils.getRange; 15 | const isGraphQLTag = utils.isGraphQLTag; 16 | const shouldLint = utils.shouldLint; 17 | 18 | const CREATE_CONTAINER_FUNCTIONS = new Set([ 19 | 'createFragmentContainer', 20 | 'createPaginationContainer', 21 | 'createRefetchContainer' 22 | ]); 23 | 24 | function isCreateContainerCall(node) { 25 | const callee = node.callee; 26 | // prettier-ignore 27 | return ( 28 | callee.type === 'Identifier' && 29 | CREATE_CONTAINER_FUNCTIONS.has(callee.name) 30 | ) || ( 31 | callee.kind === 'MemberExpression' && 32 | callee.object.type === 'Identifier' && 33 | // Relay, relay, RelayCompat, etc. 34 | /relay/i.test(callee.object.value) && 35 | callee.property.type === 'Identifier' && 36 | CREATE_CONTAINER_FUNCTIONS.has(callee.property.name) 37 | ); 38 | } 39 | 40 | function calleeToString(callee) { 41 | if (callee.type) { 42 | return callee.name; 43 | } 44 | if ( 45 | callee.kind === 'MemberExpression' && 46 | callee.object.type === 'Identifier' && 47 | callee.property.type === 'Identifier' 48 | ) { 49 | return callee.object.value + '.' + callee.property.name; 50 | } 51 | return null; 52 | } 53 | 54 | function validateTemplate(context, taggedTemplateExpression, keyName) { 55 | const ast = getGraphQLAST(taggedTemplateExpression); 56 | if (!ast) { 57 | return; 58 | } 59 | const moduleName = getModuleName(context.filename ?? context.getFilename()); 60 | ast.definitions.forEach(def => { 61 | if (!def.name) { 62 | // no name, covered by graphql-naming/TaggedTemplateExpression 63 | return; 64 | } 65 | const definitionName = def.name.value; 66 | if (def.kind === 'FragmentDefinition') { 67 | if (keyName) { 68 | const expectedName = moduleName + '_' + keyName; 69 | if (definitionName !== expectedName) { 70 | context.report({ 71 | loc: getLoc(context, taggedTemplateExpression, def.name), 72 | message: 73 | 'Container fragment names must be `_`. ' + 74 | 'Got `{{actual}}`, expected `{{expected}}`.', 75 | data: { 76 | actual: definitionName, 77 | expected: expectedName 78 | }, 79 | fix: fixer => 80 | fixer.replaceTextRange( 81 | getRange(context, taggedTemplateExpression, def.name), 82 | expectedName 83 | ) 84 | }); 85 | } 86 | } 87 | } 88 | }); 89 | } 90 | 91 | module.exports = { 92 | meta: { 93 | fixable: 'code', 94 | docs: { 95 | description: 'Validates naming conventions of graphql tags' 96 | } 97 | }, 98 | create(context) { 99 | if (!shouldLint(context)) { 100 | return {}; 101 | } 102 | return { 103 | TaggedTemplateExpression(node) { 104 | const ast = getGraphQLAST(node); 105 | if (!ast) { 106 | return; 107 | } 108 | 109 | ast.definitions.forEach(definition => { 110 | switch (definition.kind) { 111 | case 'OperationDefinition': { 112 | const moduleName = getModuleName( 113 | context.filename ?? context.getFilename() 114 | ); 115 | const name = definition.name; 116 | if (!name) { 117 | return; 118 | } 119 | const operationName = name.value; 120 | 121 | if (operationName.indexOf(moduleName) !== 0) { 122 | context.report({ 123 | message: 124 | 'Operations should start with the module name. ' + 125 | 'Expected prefix `{{expected}}`, got `{{actual}}`.', 126 | data: { 127 | expected: moduleName, 128 | actual: operationName 129 | }, 130 | loc: getLoc(context, node, name) 131 | }); 132 | } 133 | break; 134 | } 135 | default: 136 | } 137 | }); 138 | }, 139 | CallExpression(node) { 140 | if (!isCreateContainerCall(node)) { 141 | return; 142 | } 143 | const fragments = node.arguments[1]; 144 | if (fragments.type === 'ObjectExpression') { 145 | fragments.properties.forEach(property => { 146 | if ( 147 | property.type === 'Property' && 148 | property.key.type === 'Identifier' && 149 | property.computed === false && 150 | property.value.type === 'TaggedTemplateExpression' 151 | ) { 152 | if (!isGraphQLTag(property.value.tag)) { 153 | context.report({ 154 | node: property.value.tag, 155 | message: 156 | '`{{callee}}` expects GraphQL to be tagged with ' + 157 | 'graphql`...`.', 158 | data: { 159 | callee: calleeToString(node.callee) 160 | } 161 | }); 162 | return; 163 | } 164 | validateTemplate(context, property.value, property.key.name); 165 | } else { 166 | context.report({ 167 | node: property, 168 | message: 169 | '`{{callee}}` expects fragment definitions to be ' + 170 | '`key: graphql`.', 171 | data: { 172 | callee: calleeToString(node.callee) 173 | } 174 | }); 175 | } 176 | }); 177 | } 178 | } 179 | }; 180 | } 181 | }; 182 | -------------------------------------------------------------------------------- /src/rule-graphql-syntax.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const path = require('path'); 11 | 12 | const utils = require('./utils'); 13 | const getLoc = utils.getLoc; 14 | const isGraphQLTag = utils.isGraphQLTag; 15 | const shouldLint = utils.shouldLint; 16 | 17 | const graphql = require('graphql'); 18 | const parse = graphql.parse; 19 | const Source = graphql.Source; 20 | 21 | module.exports = { 22 | meta: { 23 | docs: { 24 | description: 'Validates the syntax of graphql`...` templates.' 25 | } 26 | }, 27 | create(context) { 28 | if (!shouldLint(context)) { 29 | return {}; 30 | } 31 | return { 32 | TaggedTemplateExpression(node) { 33 | if (!isGraphQLTag(node.tag)) { 34 | return; 35 | } 36 | const quasi = node.quasi.quasis[0]; 37 | if (node.quasi.quasis.length !== 1) { 38 | context.report({ 39 | node: node, 40 | message: 41 | 'graphql tagged templates do not support ${...} substitutions.' 42 | }); 43 | return; 44 | } 45 | try { 46 | const filename = path.basename( 47 | context.filename ?? context.getFilename() 48 | ); 49 | const ast = parse(new Source(quasi.value.cooked, filename)); 50 | if (ast.definitions.length !== 1) { 51 | context.report({ 52 | node: node, 53 | message: 54 | 'graphql tagged templates can only contain a single definition.' 55 | }); 56 | } else if (!ast.definitions[0].name) { 57 | context.report({ 58 | message: 'Operations in graphql tags require a name.', 59 | loc: getLoc(context, node, ast.definitions[0]) 60 | }); 61 | } 62 | } catch (error) { 63 | context.report({ 64 | node: node, 65 | message: '{{message}}', 66 | data: {message: error.message} 67 | }); 68 | } 69 | } 70 | }; 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /src/rule-hook-required-argument.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const utils = require('./utils'); 11 | const shouldLint = utils.shouldLint; 12 | 13 | function reportMissingKeyArgument(node, context, hookName) { 14 | context.report({ 15 | node: node, 16 | message: `A fragment reference should be passed to the \`${hookName}\` hook` 17 | }); 18 | } 19 | 20 | module.exports = { 21 | meta: { 22 | docs: { 23 | description: 24 | 'Validates that the second argument is passed to relay hooks.' 25 | } 26 | }, 27 | create(context) { 28 | if (!shouldLint(context)) { 29 | return {}; 30 | } 31 | 32 | return { 33 | 'CallExpression[callee.name=useFragment][arguments.length < 2]'(node) { 34 | reportMissingKeyArgument(node, context, 'useFragment'); 35 | }, 36 | 'CallExpression[callee.name=usePaginationFragment][arguments.length < 2]'( 37 | node 38 | ) { 39 | reportMissingKeyArgument(node, context, 'usePaginationFragment'); 40 | }, 41 | 42 | 'CallExpression[callee.name=useBlockingPaginationFragment][arguments.length < 2]'( 43 | node 44 | ) { 45 | reportMissingKeyArgument( 46 | node, 47 | context, 48 | 'useBlockingPaginationFragment' 49 | ); 50 | }, 51 | 52 | 'CallExpression[callee.name=useLegacyPaginationFragment][arguments.length < 2]'( 53 | node 54 | ) { 55 | reportMissingKeyArgument(node, context, 'useLegacyPaginationFragment'); 56 | }, 57 | 58 | 'CallExpression[callee.name=useRefetchableFragment][arguments.length < 2]'( 59 | node 60 | ) { 61 | reportMissingKeyArgument(node, context, 'useRefetchableFragment'); 62 | } 63 | }; 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /src/rule-must-colocate-fragment-spreads.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * This rule lints for non-colocated fragment spreads within queries or 8 | * fragments. In other words, situations where a fragment is spread in module A, 9 | * but the module (B) that defines that fragment is not imported by module A. 10 | * It does not lint subscriptions or mutations. This catches: 11 | * 12 | * - The anti-pattern of spreading a fragment in a parent module, then passing 13 | * that data down to a child module, or jamming it all in context. This defeats 14 | * the purpose of Relay. From the 15 | * [Relay docs](https://relay.dev/docs/next) – "[Relay] 16 | * allows components to specify what data they need and the Relay framework 17 | * provides the data. This makes the data needs of inner components opaque and 18 | * allows composition of those needs." 19 | * - Instances where fragment spreads are unused, which results in overfetching. 20 | * 21 | * ## When the fragment is unused 22 | * The easiest way to tell if a fragment is unused is to remove the line 23 | * containing the lint error, run Relay compiler, then Flow. If there are no 24 | * type errors, then the fragment was possibly unused. You should still test 25 | * your functionality to see that it's working as expected. 26 | * 27 | * ## When the fragment is being passed to a child component 28 | * If you received Relay or Flow errors after attempting to remove the fragment, 29 | * then it's very likely that you're passing that data down the tree. Our 30 | * recommendation is to have components specify the data they need. In the below 31 | * example, this is an anti-pattern because Component B's data requirements are 32 | * no longer opaque. Component B should not be fetching data on Component C's 33 | * behalf. 34 | * 35 | * function ComponentA(props) { 36 | * const data = useFragment(graphql` 37 | * fragment ComponentA_fragment on User { 38 | * foo 39 | * bar 40 | * some_field { 41 | * ...ComponentC_fragment 42 | * } 43 | * } 44 | * `); 45 | * return ( 46 | *
47 | * {data.foo} {data.baz} 48 | * 49 | *
50 | * ); 51 | * } 52 | * 53 | * To address this, refactor Component C to fetch the data it needs. You'll need 54 | * to update the intermediate components by amending, or adding a fragment to 55 | * each intermediate component between ComponentA and ComponentC. 56 | */ 57 | 58 | 'use strict'; 59 | 60 | const {visit} = require('graphql'); 61 | const utils = require('./utils'); 62 | 63 | const ESLINT_DISABLE_COMMENT = 64 | ' eslint-disable-next-line relay/must-colocate-fragment-spreads'; 65 | 66 | function getGraphQLFragmentSpreads(graphQLAst) { 67 | const fragmentSpreads = {}; 68 | visit(graphQLAst, { 69 | FragmentSpread(node, key, parent, path, ancestors) { 70 | for (const ancestorNode of ancestors) { 71 | if (ancestorNode.kind === 'OperationDefinition') { 72 | if ( 73 | ancestorNode.operation === 'mutation' || 74 | ancestorNode.operation === 'subscription' 75 | ) { 76 | return; 77 | } 78 | } 79 | } 80 | for (const directiveNode of node.directives) { 81 | if (directiveNode.name.value === 'module') { 82 | return; 83 | } 84 | if (directiveNode.name.value === 'relay') { 85 | for (const argumentNode of directiveNode.arguments) { 86 | if ( 87 | argumentNode.name.value === 'mask' && 88 | argumentNode.value.value === false 89 | ) { 90 | return; 91 | } 92 | } 93 | } 94 | } 95 | if ( 96 | utils.hasPrecedingEslintDisableComment(node, ESLINT_DISABLE_COMMENT) 97 | ) { 98 | return; 99 | } 100 | fragmentSpreads[node.name.value] = node; 101 | } 102 | }); 103 | return fragmentSpreads; 104 | } 105 | 106 | function getGraphQLFragmentDefinitionName(graphQLAst) { 107 | let name = null; 108 | visit(graphQLAst, { 109 | FragmentDefinition(node) { 110 | name = node.name.value; 111 | } 112 | }); 113 | return name; 114 | } 115 | 116 | module.exports = { 117 | meta: { 118 | docs: {}, 119 | schema: [] 120 | }, 121 | create(context) { 122 | const foundImportedModules = []; 123 | const graphqlLiterals = []; 124 | 125 | return { 126 | 'Program:exit'(_node) { 127 | const fragmentsInTheSameModule = []; 128 | graphqlLiterals.forEach(({graphQLAst}) => { 129 | const fragmentName = getGraphQLFragmentDefinitionName(graphQLAst); 130 | if (fragmentName) { 131 | fragmentsInTheSameModule.push(fragmentName); 132 | } 133 | }); 134 | graphqlLiterals.forEach(({node, graphQLAst}) => { 135 | const queriedFragments = getGraphQLFragmentSpreads(graphQLAst); 136 | for (const fragment in queriedFragments) { 137 | const matchedModuleName = foundImportedModules.find(name => 138 | fragment.startsWith(name) 139 | ); 140 | if ( 141 | !matchedModuleName && 142 | !fragmentsInTheSameModule.includes(fragment) 143 | ) { 144 | context.report({ 145 | node, 146 | loc: utils.getLoc(context, node, queriedFragments[fragment]), 147 | message: 148 | `This spreads the fragment \`${fragment}\` but ` + 149 | 'this module does not use it directly. If a different module ' + 150 | 'needs this information, that module should directly define a ' + 151 | 'fragment querying for that data, colocated next to where the ' + 152 | 'data is used.\n' 153 | }); 154 | } 155 | } 156 | }); 157 | }, 158 | 159 | ImportDeclaration(node) { 160 | if (node.importKind === 'value') { 161 | foundImportedModules.push(utils.getModuleName(node.source.value)); 162 | } 163 | }, 164 | 165 | ImportExpression(node) { 166 | if (node.source.type === 'Literal') { 167 | // Allow dynamic imports like import(`test/${fileName}`); and (path) => import(path); 168 | // These would have node.source.value undefined 169 | foundImportedModules.push(utils.getModuleName(node.source.value)); 170 | } 171 | }, 172 | 173 | CallExpression(node) { 174 | if (node.callee.name !== 'require') { 175 | return; 176 | } 177 | const [source] = node.arguments; 178 | if (source && source.type === 'Literal') { 179 | foundImportedModules.push(utils.getModuleName(source.value)); 180 | } 181 | }, 182 | 183 | TaggedTemplateExpression(node) { 184 | if (utils.isGraphQLTemplate(node)) { 185 | const graphQLAst = utils.getGraphQLAST(node); 186 | if (!graphQLAst) { 187 | // ignore nodes with syntax errors, they're handled by rule-graphql-syntax 188 | return; 189 | } 190 | graphqlLiterals.push({node, graphQLAst}); 191 | } 192 | } 193 | }; 194 | } 195 | }; 196 | -------------------------------------------------------------------------------- /src/rule-no-future-added-value.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | module.exports = { 11 | meta: { 12 | docs: {}, 13 | schema: [] 14 | }, 15 | create: context => { 16 | function validateValue(node) { 17 | context.report({ 18 | node: node, 19 | message: 20 | "Do not use `'%future added value'`. It represents any potential " + 21 | 'value that the server might return in the future that the code ' + 22 | 'should handle.' 23 | }); 24 | } 25 | return { 26 | "Literal[value='%future added value']": validateValue, 27 | 28 | // StringLiteralTypeAnnotations that are not children of a default case 29 | ":not(SwitchCase[test=null] StringLiteralTypeAnnotation)StringLiteralTypeAnnotation[value='%future added value']": 30 | validateValue 31 | }; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/rule-unused-fields.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const { 11 | getGraphQLAST, 12 | getLoc, 13 | hasPrecedingEslintDisableComment 14 | } = require('./utils'); 15 | 16 | const ESLINT_DISABLE_COMMENT = ' eslint-disable-next-line relay/unused-fields'; 17 | 18 | function getGraphQLFieldNames(graphQLAst) { 19 | const fieldNames = {}; 20 | 21 | function walkAST(node, ignoreLevel) { 22 | if (node.kind === 'Field' && !ignoreLevel) { 23 | if (hasPrecedingEslintDisableComment(node, ESLINT_DISABLE_COMMENT)) { 24 | return; 25 | } 26 | const nameNode = node.alias || node.name; 27 | fieldNames[nameNode.value] = nameNode; 28 | } 29 | if (node.kind === 'OperationDefinition') { 30 | if ( 31 | node.operation === 'mutation' || 32 | node.operation === 'subscription' || 33 | hasPrecedingEslintDisableComment(node, ESLINT_DISABLE_COMMENT) 34 | ) { 35 | return; 36 | } 37 | // Ignore fields that are direct children of query as used in mutation 38 | // or query definitions. 39 | node.selectionSet.selections.forEach(selection => { 40 | walkAST(selection, true); 41 | }); 42 | return; 43 | } 44 | for (const prop in node) { 45 | const value = node[prop]; 46 | if (prop === 'loc') { 47 | continue; 48 | } 49 | if (value && typeof value === 'object') { 50 | walkAST(value); 51 | } else if (Array.isArray(value)) { 52 | value.forEach(child => { 53 | walkAST(child); 54 | }); 55 | } 56 | } 57 | } 58 | 59 | walkAST(graphQLAst); 60 | return fieldNames; 61 | } 62 | 63 | function isGraphQLTemplate(node) { 64 | return ( 65 | node.tag.type === 'Identifier' && 66 | node.tag.name === 'graphql' && 67 | node.quasi.quasis.length === 1 68 | ); 69 | } 70 | 71 | function isStringNode(node) { 72 | return ( 73 | node != null && node.type === 'Literal' && typeof node.value === 'string' 74 | ); 75 | } 76 | 77 | function isPageInfoField(field) { 78 | switch (field) { 79 | case 'pageInfo': 80 | case 'page_info': 81 | case 'hasNextPage': 82 | case 'has_next_page': 83 | case 'hasPreviousPage': 84 | case 'has_previous_page': 85 | case 'startCursor': 86 | case 'start_cursor': 87 | case 'endCursor': 88 | case 'end_cursor': 89 | return true; 90 | default: 91 | return false; 92 | } 93 | } 94 | 95 | module.exports = { 96 | meta: { 97 | docs: {}, 98 | schema: [] 99 | }, 100 | create(context) { 101 | let currentMethod = []; 102 | let foundMemberAccesses = {}; 103 | let templateLiterals = []; 104 | 105 | function visitGetByPathCall(node) { 106 | // The `getByPath` utility accesses nested fields in the form 107 | // `getByPath(thing, ['field', 'nestedField'])`. 108 | const pathArg = node.arguments[1]; 109 | if (!pathArg || pathArg.type !== 'ArrayExpression') { 110 | return; 111 | } 112 | pathArg.elements.forEach(element => { 113 | if (isStringNode(element)) { 114 | foundMemberAccesses[element.value] = true; 115 | } 116 | }); 117 | } 118 | 119 | function visitDotAccessCall(node) { 120 | // The `dotAccess` utility accesses nested fields in the form 121 | // `dotAccess(thing, 'field.nestedField')`. 122 | const pathArg = node.arguments[1]; 123 | if (isStringNode(pathArg)) { 124 | pathArg.value.split('.').forEach(element => { 125 | foundMemberAccesses[element] = true; 126 | }); 127 | } 128 | } 129 | 130 | function visitMemberExpression(node) { 131 | if (node.property.type === 'Identifier') { 132 | foundMemberAccesses[node.property.name] = true; 133 | } 134 | } 135 | 136 | return { 137 | Program(_node) { 138 | currentMethod = []; 139 | foundMemberAccesses = {}; 140 | templateLiterals = []; 141 | }, 142 | 'Program:exit'(_node) { 143 | templateLiterals.forEach(templateLiteral => { 144 | const graphQLAst = getGraphQLAST(templateLiteral); 145 | if (!graphQLAst) { 146 | // ignore nodes with syntax errors, they're handled by rule-graphql-syntax 147 | return; 148 | } 149 | 150 | const queriedFields = getGraphQLFieldNames(graphQLAst); 151 | for (const field in queriedFields) { 152 | if ( 153 | !foundMemberAccesses[field] && 154 | !isPageInfoField(field) && 155 | // Do not warn for unused __typename which can be a workaround 156 | // when only interested in existence of an object. 157 | field !== '__typename' 158 | ) { 159 | context.report({ 160 | node: templateLiteral, 161 | loc: getLoc(context, templateLiteral, queriedFields[field]), 162 | message: 163 | `This queries for the field \`${field}\` but this file does ` + 164 | 'not seem to use it directly. If a different file needs this ' + 165 | 'information that file should export a fragment and colocate ' + 166 | 'the query for the data with the usage.\n' + 167 | 'If only interested in the existence of a record, __typename ' + 168 | 'can be used without this warning.' 169 | }); 170 | } 171 | } 172 | }); 173 | }, 174 | CallExpression(node) { 175 | if (node.callee.type !== 'Identifier') { 176 | return; 177 | } 178 | switch (node.callee.name) { 179 | case 'getByPath': 180 | visitGetByPathCall(node); 181 | break; 182 | case 'dotAccess': 183 | visitDotAccessCall(node); 184 | break; 185 | } 186 | }, 187 | TaggedTemplateExpression(node) { 188 | if (currentMethod[0] === 'getConfigs') { 189 | return; 190 | } 191 | if (isGraphQLTemplate(node)) { 192 | templateLiterals.push(node); 193 | } 194 | }, 195 | MemberExpression: visitMemberExpression, 196 | OptionalMemberExpression: visitMemberExpression, 197 | ObjectPattern(node) { 198 | node.properties.forEach(node => { 199 | if (node.type === 'Property' && !node.computed) { 200 | foundMemberAccesses[node.key.name] = true; 201 | } 202 | }); 203 | }, 204 | MethodDefinition(node) { 205 | currentMethod.unshift(node.key.name); 206 | }, 207 | 'MethodDefinition:exit'(_node) { 208 | currentMethod.shift(); 209 | } 210 | }; 211 | } 212 | }; 213 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const path = require('path'); 11 | 12 | const graphql = require('graphql'); 13 | const parse = graphql.parse; 14 | 15 | function isGraphQLTemplate(node) { 16 | return ( 17 | node.tag.type === 'Identifier' && 18 | node.tag.name === 'graphql' && 19 | node.quasi.quasis.length === 1 20 | ); 21 | } 22 | 23 | function getGraphQLAST(taggedTemplateExpression) { 24 | if (!isGraphQLTag(taggedTemplateExpression.tag)) { 25 | return null; 26 | } 27 | if (taggedTemplateExpression.quasi.quasis.length !== 1) { 28 | // has substitutions, covered by graphql-syntax rule 29 | return null; 30 | } 31 | const quasi = taggedTemplateExpression.quasi.quasis[0]; 32 | try { 33 | return parse(quasi.value.cooked); 34 | } catch (_error) { 35 | // Invalid syntax, covered by graphql-syntax rule 36 | return null; 37 | } 38 | } 39 | 40 | /** 41 | * Returns a loc object for error reporting. 42 | */ 43 | function getLoc(context, templateNode, graphQLNode) { 44 | const startAndEnd = getRange(context, templateNode, graphQLNode); 45 | const start = startAndEnd[0]; 46 | const end = startAndEnd[1]; 47 | return { 48 | start: getLocFromIndex( 49 | context.sourceCode ?? context.getSourceCode(), 50 | start 51 | ), 52 | end: getLocFromIndex(context.sourceCode ?? context.getSourceCode(), end) 53 | }; 54 | } 55 | 56 | // TODO remove after we no longer have to support ESLint 3.5.0 57 | function getLocFromIndex(sourceCode, index) { 58 | if (sourceCode.getLocFromIndex) { 59 | return sourceCode.getLocFromIndex(index); 60 | } 61 | let pos = 0; 62 | for (let line = 0; line < sourceCode.lines.length; line++) { 63 | const lineLength = sourceCode.lines[line].length; 64 | if (index <= pos + lineLength) { 65 | return {line: line + 1, column: index - pos}; 66 | } 67 | pos += lineLength + 1; 68 | } 69 | return null; 70 | } 71 | 72 | // Copied directly from Relay 73 | function getModuleName(filePath) { 74 | // index.js -> index 75 | // index.js.flow -> index.js 76 | let filename = path.basename(filePath, path.extname(filePath)); 77 | 78 | // index.js -> index (when extension has multiple segments) 79 | // index.react -> index (when extension has multiple segments) 80 | filename = filename.replace(/(\.(?!ios|android)[_a-zA-Z0-9\\-]+)+/g, ''); 81 | 82 | // /path/to/button/index.js -> button 83 | let moduleName = 84 | filename === 'index' ? path.basename(path.dirname(filePath)) : filename; 85 | 86 | // foo-bar -> fooBar 87 | // Relay compatibility mode splits on _, so we can't use that here. 88 | moduleName = moduleName.replace(/[^a-zA-Z0-9]+(\w?)/g, (match, next) => 89 | next.toUpperCase() 90 | ); 91 | 92 | return moduleName; 93 | } 94 | 95 | /** 96 | * Returns a range object for auto fixers. 97 | */ 98 | function getRange(context, templateNode, graphQLNode) { 99 | const graphQLStart = templateNode.quasi.quasis[0].range[0] + 1; 100 | return [ 101 | graphQLStart + graphQLNode.loc.start, 102 | graphQLStart + graphQLNode.loc.end 103 | ]; 104 | } 105 | 106 | function isGraphQLTag(tag) { 107 | return tag.type === 'Identifier' && tag.name === 'graphql'; 108 | } 109 | 110 | function shouldLint(context) { 111 | return /graphql|relay/i.test( 112 | (context.sourceCode ?? context.getSourceCode()).text 113 | ); 114 | } 115 | 116 | function hasPrecedingEslintDisableComment(node, commentText) { 117 | const prevNode = node.loc.startToken.prev; 118 | return prevNode.kind === 'Comment' && prevNode.value.startsWith(commentText); 119 | } 120 | 121 | module.exports = { 122 | isGraphQLTemplate: isGraphQLTemplate, 123 | getGraphQLAST: getGraphQLAST, 124 | getLoc: getLoc, 125 | getLocFromIndex: getLocFromIndex, 126 | getModuleName: getModuleName, 127 | getRange: getRange, 128 | hasPrecedingEslintDisableComment: hasPrecedingEslintDisableComment, 129 | isGraphQLTag: isGraphQLTag, 130 | shouldLint: shouldLint 131 | }; 132 | -------------------------------------------------------------------------------- /test/function-required-argument.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const rules = require('..').rules; 11 | const RuleTester = require('eslint').RuleTester; 12 | 13 | const ruleTester = new RuleTester({ 14 | languageOptions: { 15 | ecmaVersion: 6, 16 | parser: require('@typescript-eslint/parser') 17 | } 18 | }); 19 | 20 | ruleTester.run( 21 | 'function-required-argument', 22 | rules['function-required-argument'], 23 | { 24 | valid: [ 25 | { 26 | code: ` 27 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 28 | readInlineData(graphql\`fragment TestFragment_foo on User { id }\`, ref) 29 | ` 30 | } 31 | ], 32 | invalid: [ 33 | { 34 | code: ` 35 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 36 | readInlineData(graphql\`fragment TestFragment_foo on User { id }\`) 37 | `, 38 | errors: [ 39 | { 40 | message: 41 | 'A fragment reference should be passed to the `readInlineData` function', 42 | line: 3 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | ); 49 | -------------------------------------------------------------------------------- /test/future-added-value.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | var eslint = require('eslint'); 11 | 12 | const rules = require('..').rules; 13 | const RuleTester = eslint.RuleTester; 14 | 15 | const ruleTester = new RuleTester({ 16 | languageOptions: { 17 | ecmaVersion: 6, 18 | sourceType: 'module', 19 | parser: require('@babel/eslint-parser'), 20 | parserOptions: { 21 | requireConfigFile: false, 22 | babelOptions: {presets: ['@babel/preset-flow']} 23 | } 24 | } 25 | }); 26 | 27 | const FUTURE_ADDED_VALUE_MESSAGE = 28 | "Do not use `'%future added value'`. It represents any potential " + 29 | 'value that the server might return in the future that the code ' + 30 | 'should handle.'; 31 | 32 | ruleTester.run('no-future-added-value', rules['no-future-added-value'], { 33 | valid: [ 34 | `const response: 'YES' | 'NO' = 'YES';`, 35 | ` 36 | const response: 'YES' | 'NO' = 'YES'; 37 | switch (response) { 38 | case 'YES': 39 | break; 40 | case 'NO': 41 | break; 42 | default: 43 | (response: '%future added value'); 44 | } 45 | ` 46 | ], 47 | invalid: [ 48 | { 49 | // value location 50 | code: `const response: 'YES' | 'NO' = '%future added value';`, 51 | errors: [ 52 | { 53 | message: FUTURE_ADDED_VALUE_MESSAGE 54 | } 55 | ] 56 | }, 57 | { 58 | // type location 59 | code: `function test(x: 'EXAMPLE'|'%future added value'){ }`, 60 | errors: [ 61 | { 62 | message: FUTURE_ADDED_VALUE_MESSAGE 63 | } 64 | ] 65 | }, 66 | { 67 | code: ` 68 | const response: 'YES' | 'NO' = 'YES'; 69 | switch (response) { 70 | case 'YES': 71 | break; 72 | case 'NO': 73 | break; 74 | case '%future added value': 75 | break; 76 | default: 77 | (response: '%future added value'); 78 | } 79 | `, 80 | errors: [ 81 | { 82 | message: FUTURE_ADDED_VALUE_MESSAGE 83 | } 84 | ] 85 | }, 86 | { 87 | // using future added value not in typecasting 88 | code: ` 89 | const response: 'YES' | 'NO' = 'YES'; 90 | switch (response) { 91 | case 'YES': 92 | break; 93 | case 'NO': 94 | break; 95 | default: 96 | const foo = '%future added value'; 97 | } 98 | `, 99 | errors: [ 100 | { 101 | message: FUTURE_ADDED_VALUE_MESSAGE 102 | } 103 | ] 104 | } 105 | ] 106 | }); 107 | -------------------------------------------------------------------------------- /test/generated-typescript-types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const rules = require('..').rules; 11 | const RuleTester = require('eslint').RuleTester; 12 | 13 | const ruleTester = new RuleTester({ 14 | languageOptions: { 15 | ecmaVersion: 6, 16 | parser: require('@typescript-eslint/parser'), 17 | parserOptions: {ecmaFeatures: {jsx: true}} 18 | } 19 | }); 20 | 21 | const HAS_ESLINT_BEEN_UPGRADED_YET = false; 22 | const DEFAULT_OPTIONS = [ 23 | { 24 | fix: true, 25 | haste: false 26 | } 27 | ]; 28 | 29 | ruleTester.run( 30 | 'generated-typescript-types', 31 | rules['generated-typescript-types'], 32 | { 33 | valid: [ 34 | // syntax error, covered by `graphql-syntax` 35 | {code: 'graphql`query {{{`'}, 36 | { 37 | code: ` 38 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 39 | useFragment(graphql\`fragment TestFragment_foo on User { id }\`) 40 | ` 41 | }, 42 | { 43 | code: ` 44 | import type {TestFragment_foo$key} from './path/to/TestFragment_foo.graphql'; 45 | useFragment(graphql\`fragment TestFragment_foo on User { id }\`) 46 | ` 47 | }, 48 | { 49 | code: ` 50 | import {type TestFragment_foo$key} from './path/to/TestFragment_foo.graphql'; 51 | useFragment(graphql\`fragment TestFragment_foo on User { id }\`) 52 | ` 53 | }, 54 | { 55 | code: ` 56 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 57 | useRefetchableFragment(graphql\`fragment TestFragment_foo on User { id }\`) 58 | ` 59 | }, 60 | { 61 | code: ` 62 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 63 | usePaginationFragment(graphql\`fragment TestFragment_foo on User { id }\`) 64 | ` 65 | }, 66 | { 67 | code: ` 68 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 69 | 70 | const ref = useFragment(graphql\`fragment TestFragment_foo on User { id }\`, props.user); 71 | usePaginationFragment(graphql\`fragment TestPaginationFragment_foo on User { id }\`, ref); 72 | ` 73 | }, 74 | { 75 | code: ` 76 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 77 | 78 | const {data: ref} = useFragment(graphql\`fragment TestFragment_foo on User { id }\`, props.user); 79 | usePaginationFragment(graphql\`fragment TestPaginationFragment_foo on User { id }\`, ref); 80 | ` 81 | }, 82 | { 83 | code: ` 84 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 85 | useBlockingPaginationFragment(graphql\`fragment TestFragment_foo on User { id }\`) 86 | ` 87 | }, 88 | { 89 | code: ` 90 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 91 | useLegacyPaginationFragment(graphql\`fragment TestFragment_foo on User { id }\`) 92 | ` 93 | }, 94 | {code: 'useQuery(graphql`query Foo { id }`)'}, 95 | {code: 'useLazyLoadQuery(graphql`query Foo { id }`)'}, 96 | { 97 | code: ` 98 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 99 | class MyComponent extends React.Component<{user: MyComponent_user}> { 100 | render() { 101 | return
; 102 | } 103 | } 104 | 105 | createFragmentContainer(MyComponent, { 106 | user: graphql\`fragment MyComponent_user on User {id}\`, 107 | }); 108 | ` 109 | }, 110 | { 111 | code: ` 112 | import type {MyComponent_user as User} from './__generated__/MyComponent_user.graphql' 113 | class MyComponent extends React.Component<{user: User}> { 114 | render() { 115 | return
; 116 | } 117 | } 118 | 119 | createFragmentContainer(MyComponent, { 120 | user: graphql\`fragment MyComponent_user on User {id}\`, 121 | }); 122 | ` 123 | }, 124 | { 125 | code: ` 126 | import type {MyComponent_user} from 'MyComponent_user.graphql' 127 | type Props = { 128 | user: MyComponent_user, 129 | } 130 | 131 | class MyComponent extends React.Component { 132 | render() { 133 | return
; 134 | } 135 | } 136 | 137 | createFragmentContainer(MyComponent, { 138 | user: graphql\`fragment MyComponent_user on User {id}\`, 139 | }); 140 | ` 141 | }, 142 | { 143 | code: ` 144 | import type {MyComponent_double_underscore} from 'MyComponent_double_underscore.graphql' 145 | type Props = { 146 | double_underscore: MyComponent_double_underscore, 147 | } 148 | 149 | class MyComponent extends React.Component { 150 | render() { 151 | return
; 152 | } 153 | } 154 | 155 | createFragmentContainer(MyComponent, { 156 | double_underscore: graphql\`fragment MyComponent_double_underscore on User {id}\`, 157 | }); 158 | ` 159 | }, 160 | { 161 | code: ` 162 | import type {MyComponent_user} from 'MyComponent_user.graphql' 163 | type Props = { 164 | readonly user: MyComponent_user, 165 | } 166 | 167 | class MyComponent extends React.Component { 168 | render() { 169 | return
; 170 | } 171 | } 172 | 173 | createFragmentContainer(MyComponent, { 174 | user: graphql\`fragment MyComponent_user on User {id}\`, 175 | }); 176 | ` 177 | }, 178 | { 179 | code: ` 180 | import type {MyComponent_user} from 'MyComponent_user.graphql' 181 | type Props = { 182 | user?: MyComponent_user, 183 | } 184 | 185 | class MyComponent extends React.Component { 186 | render() { 187 | return
; 188 | } 189 | } 190 | 191 | createFragmentContainer(MyComponent, { 192 | user: graphql\`fragment MyComponent_user on User {id}\`, 193 | }); 194 | ` 195 | }, 196 | { 197 | code: ` 198 | import type {MyComponent_user} from 'MyComponent_user.graphql' 199 | type Props = { 200 | user: MyComponent_user, 201 | } 202 | type State = { 203 | count: number, 204 | } 205 | 206 | class MyComponent extends React.Component { 207 | render() { 208 | return
; 209 | } 210 | } 211 | 212 | createFragmentContainer(MyComponent, { 213 | user: graphql\`fragment MyComponent_user on User {id}\`, 214 | }); 215 | ` 216 | } 217 | ], 218 | invalid: [ 219 | { 220 | // imports TestFragment_other$key instead of TestFragment_foo$key 221 | code: ` 222 | import type {TestFragment_other$key} from './path/to/TestFragment_other.graphql'; 223 | useFragment(graphql\`fragment TestFragment_foo on User { id }\`) 224 | `, 225 | errors: [ 226 | { 227 | message: ` 228 | The prop passed to useFragment() should be typed with the type 'TestFragment_foo$key' imported from 'TestFragment_foo.graphql', e.g.: 229 | 230 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql';`.trim(), 231 | line: 3, 232 | column: 9 233 | } 234 | ] 235 | }, 236 | { 237 | code: ` 238 | import type {other} from 'TestFragment_foo.graphql'; 239 | useFragment(graphql\`fragment TestFragment_foo on User { id }\`) 240 | `, 241 | errors: [ 242 | { 243 | message: ` 244 | The prop passed to useFragment() should be typed with the type 'TestFragment_foo$key' imported from 'TestFragment_foo.graphql', e.g.: 245 | 246 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql';`.trim(), 247 | line: 3, 248 | column: 9 249 | } 250 | ] 251 | }, 252 | { 253 | code: ` 254 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 255 | useRefetchableFragment(graphql\`fragment TestFragment_foo on User @refetchable(queryName:"TestFragmentQuery") { id }\`) 256 | `, 257 | errors: [ 258 | { 259 | message: 260 | 'The `useRefetchableFragment` hook should be used with an explicit generated Typescript type, e.g.: useRefetchableFragment(...)', 261 | line: 3, 262 | column: 9 263 | } 264 | ], 265 | options: DEFAULT_OPTIONS, 266 | output: ` 267 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 268 | import type {TestFragmentQuery} from './__generated__/TestFragmentQuery.graphql' 269 | useRefetchableFragment(graphql\`fragment TestFragment_foo on User @refetchable(queryName:"TestFragmentQuery") { id }\`) 270 | ` 271 | }, 272 | { 273 | code: ` 274 | useRefetchableFragment(graphql\`fragment TestFragment_foo on User { id }\`) 275 | `, 276 | errors: [ 277 | { 278 | message: ` 279 | The prop passed to useRefetchableFragment() should be typed with the type 'TestFragment_foo$key' imported from 'TestFragment_foo.graphql', e.g.: 280 | 281 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql';`.trim(), 282 | line: 2, 283 | column: 9 284 | } 285 | ] 286 | }, 287 | { 288 | code: ` 289 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 290 | usePaginationFragment(graphql\`fragment TestFragment_foo on User @refetchable(queryName: "TestFragmentQuery") { id }\`) 291 | `, 292 | options: DEFAULT_OPTIONS, 293 | errors: [ 294 | { 295 | message: 296 | 'The `usePaginationFragment` hook should be used with an explicit generated Typescript type, e.g.: usePaginationFragment(...)', 297 | line: 3, 298 | column: 9 299 | } 300 | ], 301 | output: ` 302 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 303 | import type {TestFragmentQuery} from './__generated__/TestFragmentQuery.graphql' 304 | usePaginationFragment(graphql\`fragment TestFragment_foo on User @refetchable(queryName: "TestFragmentQuery") { id }\`) 305 | ` 306 | }, 307 | { 308 | code: ` 309 | usePaginationFragment(graphql\`fragment TestFragment_foo on User { id }\`) 310 | `, 311 | errors: [ 312 | { 313 | message: ` 314 | The prop passed to usePaginationFragment() should be typed with the type 'TestFragment_foo$key' imported from 'TestFragment_foo.graphql', e.g.: 315 | 316 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql';`.trim(), 317 | line: 2, 318 | column: 9 319 | } 320 | ] 321 | }, 322 | { 323 | code: ` 324 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 325 | 326 | const refUnused = useFragment(graphql\`fragment TestFragment_foo on User { id }\`, props.user); 327 | usePaginationFragment(graphql\`fragment TestPaginationFragment_foo on User { id }\`, ref); 328 | `, 329 | errors: [ 330 | { 331 | message: ` 332 | The prop passed to usePaginationFragment() should be typed with the type 'TestPaginationFragment_foo$key' imported from 'TestPaginationFragment_foo.graphql', e.g.: 333 | 334 | import type {TestPaginationFragment_foo$key} from 'TestPaginationFragment_foo.graphql';`.trim(), 335 | line: 5, 336 | column: 9 337 | } 338 | ] 339 | }, 340 | { 341 | code: ` 342 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 343 | 344 | const {data: refUnused }= useFragment(graphql\`fragment TestFragment_foo on User { id }\`, props.user); 345 | usePaginationFragment(graphql\`fragment TestPaginationFragment_foo on User { id }\`, ref); 346 | `, 347 | errors: [ 348 | { 349 | message: ` 350 | The prop passed to usePaginationFragment() should be typed with the type 'TestPaginationFragment_foo$key' imported from 'TestPaginationFragment_foo.graphql', e.g.: 351 | 352 | import type {TestPaginationFragment_foo$key} from 'TestPaginationFragment_foo.graphql';`.trim(), 353 | line: 5, 354 | column: 9 355 | } 356 | ] 357 | }, 358 | { 359 | code: ` 360 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 361 | useBlockingPaginationFragment(graphql\`fragment TestFragment_foo on User @refetchable(queryName: "TestFragmentQuery") { id }\`) 362 | `, 363 | errors: [ 364 | { 365 | message: 366 | 'The `useBlockingPaginationFragment` hook should be used with an explicit generated Typescript type, e.g.: useBlockingPaginationFragment(...)', 367 | line: 3, 368 | column: 9 369 | } 370 | ] 371 | }, 372 | { 373 | code: ` 374 | useBlockingPaginationFragment(graphql\`fragment TestFragment_foo on User { id }\`) 375 | `, 376 | errors: [ 377 | { 378 | message: ` 379 | The prop passed to useBlockingPaginationFragment() should be typed with the type 'TestFragment_foo$key' imported from 'TestFragment_foo.graphql', e.g.: 380 | 381 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql';`.trim(), 382 | line: 2, 383 | column: 9 384 | } 385 | ] 386 | }, 387 | 388 | { 389 | code: ` 390 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 391 | useLegacyPaginationFragment(graphql\`fragment TestFragment_foo on User { id }\`) 392 | `, 393 | options: DEFAULT_OPTIONS, 394 | errors: [ 395 | { 396 | message: 397 | 'The `useLegacyPaginationFragment` hook should be used with an explicit generated Typescript type, e.g.: useLegacyPaginationFragment(...)', 398 | line: 3, 399 | column: 9 400 | } 401 | ], 402 | output: null 403 | }, 404 | { 405 | code: ` 406 | useLegacyPaginationFragment(graphql\`fragment TestFragment_foo on User { id }\`) 407 | `, 408 | errors: [ 409 | { 410 | message: ` 411 | The prop passed to useLegacyPaginationFragment() should be typed with the type 'TestFragment_foo$key' imported from 'TestFragment_foo.graphql', e.g.: 412 | 413 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql';`.trim(), 414 | line: 2, 415 | column: 9 416 | } 417 | ] 418 | }, 419 | 420 | { 421 | code: `\nuseQuery(graphql\`query FooQuery { id }\`)`, 422 | errors: [ 423 | { 424 | message: 425 | 'The `useQuery` hook should be used with an explicit generated Typescript type, e.g.: useQuery(...)', 426 | line: 2, 427 | column: 1 428 | } 429 | ], 430 | options: DEFAULT_OPTIONS, 431 | output: ` 432 | import type {FooQuery} from './__generated__/FooQuery.graphql' 433 | useQuery(graphql\`query FooQuery { id }\`)` 434 | }, 435 | { 436 | code: ` 437 | const query = graphql\`query FooQuery { id }\`; 438 | const query2 = query; 439 | useQuery(query2); 440 | `, 441 | errors: [ 442 | { 443 | message: 444 | 'The `useQuery` hook should be used with an explicit generated Typescript type, e.g.: useQuery(...)', 445 | line: 4 446 | } 447 | ], 448 | options: DEFAULT_OPTIONS, 449 | output: ` 450 | import type {FooQuery} from './__generated__/FooQuery.graphql' 451 | const query = graphql\`query FooQuery { id }\`; 452 | const query2 = query; 453 | useQuery(query2); 454 | ` 455 | }, 456 | { 457 | code: ` 458 | const query = 'graphql'; 459 | useQuery(query); 460 | `, 461 | options: DEFAULT_OPTIONS, 462 | errors: [ 463 | { 464 | message: 465 | 'The `useQuery` hook should be used with an explicit generated Typescript type, e.g.: useQuery(...)', 466 | line: 3 467 | } 468 | ], 469 | output: null 470 | }, 471 | 472 | { 473 | code: `\nuseLazyLoadQuery(graphql\`query FooQuery { id }\`)`, 474 | errors: [ 475 | { 476 | message: 477 | 'The `useLazyLoadQuery` hook should be used with an explicit generated Typescript type, e.g.: useLazyLoadQuery(...)', 478 | line: 2, 479 | column: 1 480 | } 481 | ], 482 | options: DEFAULT_OPTIONS, 483 | output: ` 484 | import type {FooQuery} from './__generated__/FooQuery.graphql' 485 | useLazyLoadQuery(graphql\`query FooQuery { id }\`)` 486 | }, 487 | { 488 | code: ` 489 | const query = graphql\`query FooQuery { id }\`; 490 | const query2 = query; 491 | useLazyLoadQuery(query2); 492 | `, 493 | errors: [ 494 | { 495 | message: 496 | 'The `useLazyLoadQuery` hook should be used with an explicit generated Typescript type, e.g.: useLazyLoadQuery(...)', 497 | line: 4 498 | } 499 | ], 500 | options: DEFAULT_OPTIONS, 501 | output: ` 502 | import type {FooQuery} from './__generated__/FooQuery.graphql' 503 | const query = graphql\`query FooQuery { id }\`; 504 | const query2 = query; 505 | useLazyLoadQuery(query2); 506 | ` 507 | }, 508 | { 509 | code: ` 510 | const query = 'graphql'; 511 | useLazyLoadQuery(query); 512 | `, 513 | options: DEFAULT_OPTIONS, 514 | errors: [ 515 | { 516 | message: 517 | 'The `useLazyLoadQuery` hook should be used with an explicit generated Typescript type, e.g.: useLazyLoadQuery(...)', 518 | line: 3 519 | } 520 | ], 521 | output: null 522 | }, 523 | { 524 | code: `\nconst mutation = graphql\`mutation FooMutation { id }\`;\nconst [commit] = useMutation(mutation);`, 525 | options: DEFAULT_OPTIONS, 526 | errors: [ 527 | { 528 | message: 529 | 'The `useMutation` hook should be used with an explicit generated Typescript type, e.g.: useMutation(...)', 530 | line: 3 531 | } 532 | ], 533 | output: ` 534 | import type {FooMutation} from './__generated__/FooMutation.graphql' 535 | const mutation = graphql\`mutation FooMutation { id }\`; 536 | const [commit] = useMutation(mutation);` 537 | }, 538 | { 539 | code: `\ncommitMutation(environemnt, {mutation: graphql\`mutation FooMutation { id }\`})`, 540 | errors: [ 541 | { 542 | message: 543 | 'The `commitMutation` must be used with an explicit generated Typescript type, e.g.: commitMutation(...)', 544 | line: 2, 545 | column: 1 546 | } 547 | ], 548 | options: DEFAULT_OPTIONS, 549 | output: ` 550 | import type {FooMutation} from './__generated__/FooMutation.graphql' 551 | commitMutation(environemnt, {mutation: graphql\`mutation FooMutation { id }\`})` 552 | }, 553 | { 554 | code: ` 555 | const mutation = graphql\`mutation FooMutation { id }\`; 556 | commitMutation(environment, {mutation}); 557 | `, 558 | errors: [ 559 | { 560 | message: 561 | 'The `commitMutation` must be used with an explicit generated Typescript type, e.g.: commitMutation(...)', 562 | line: 3 563 | } 564 | ], 565 | options: DEFAULT_OPTIONS, 566 | output: ` 567 | import type {FooMutation} from './__generated__/FooMutation.graphql' 568 | const mutation = graphql\`mutation FooMutation { id }\`; 569 | commitMutation(environment, {mutation}); 570 | ` 571 | }, 572 | { 573 | code: ` 574 | const mutation = graphql\`mutation FooMutation { id }\`; 575 | const myMutation = mutation; 576 | commitMutation(environment, {mutation: myMutation}); 577 | `, 578 | errors: [ 579 | { 580 | message: 581 | 'The `commitMutation` must be used with an explicit generated Typescript type, e.g.: commitMutation(...)', 582 | line: 4 583 | } 584 | ], 585 | options: DEFAULT_OPTIONS, 586 | output: ` 587 | import type {FooMutation} from './__generated__/FooMutation.graphql' 588 | const mutation = graphql\`mutation FooMutation { id }\`; 589 | const myMutation = mutation; 590 | commitMutation(environment, {mutation: myMutation}); 591 | ` 592 | }, 593 | { 594 | code: `\nrequestSubscription(environemnt, {subscription: graphql\`subscription FooSubscription { id }\`})`, 595 | errors: [ 596 | { 597 | message: 598 | 'The `requestSubscription` must be used with an explicit generated Typescript type, e.g.: requestSubscription(...)', 599 | line: 2, 600 | column: 1 601 | } 602 | ], 603 | options: DEFAULT_OPTIONS, 604 | output: ` 605 | import type {FooSubscription} from './__generated__/FooSubscription.graphql' 606 | requestSubscription(environemnt, {subscription: graphql\`subscription FooSubscription { id }\`})` 607 | }, 608 | { 609 | code: ` 610 | const subscription = graphql\`subscription FooSubscription { id }\`; 611 | requestSubscription(environment, {subscription}); 612 | `, 613 | errors: [ 614 | { 615 | message: 616 | 'The `requestSubscription` must be used with an explicit generated Typescript type, e.g.: requestSubscription(...)', 617 | line: 3 618 | } 619 | ], 620 | options: DEFAULT_OPTIONS, 621 | output: ` 622 | import type {FooSubscription} from './__generated__/FooSubscription.graphql' 623 | const subscription = graphql\`subscription FooSubscription { id }\`; 624 | requestSubscription(environment, {subscription}); 625 | ` 626 | }, 627 | { 628 | code: ` 629 | const subscription = graphql\`subscription FooSubscription { id }\`; 630 | const mySubscription = subscription; 631 | requestSubscription(environment, {subscription: mySubscription}); 632 | `, 633 | errors: [ 634 | { 635 | message: 636 | 'The `requestSubscription` must be used with an explicit generated Typescript type, e.g.: requestSubscription(...)', 637 | line: 4 638 | } 639 | ], 640 | options: DEFAULT_OPTIONS, 641 | output: ` 642 | import type {FooSubscription} from './__generated__/FooSubscription.graphql' 643 | const subscription = graphql\`subscription FooSubscription { id }\`; 644 | const mySubscription = subscription; 645 | requestSubscription(environment, {subscription: mySubscription}); 646 | ` 647 | }, 648 | { 649 | filename: 'MyComponent.jsx', 650 | code: ` 651 | class MyComponent extends React.Component<{}> { 652 | render() { 653 | return
; 654 | } 655 | } 656 | 657 | createFragmentContainer(MyComponent, { 658 | user: graphql\`fragment MyComponent_user on User {id}\`, 659 | }); 660 | `, 661 | options: DEFAULT_OPTIONS, 662 | output: ` 663 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 664 | class MyComponent extends React.Component<{user: MyComponent_user}> { 665 | render() { 666 | return
; 667 | } 668 | } 669 | 670 | createFragmentContainer(MyComponent, { 671 | user: graphql\`fragment MyComponent_user on User {id}\`, 672 | }); 673 | `, 674 | errors: [ 675 | { 676 | message: 677 | '`user` is not declared in the `props` of the React component or ' + 678 | 'it is not marked with the generated typescript type ' + 679 | '`MyComponent_user`. See ' + 680 | 'https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 681 | line: 2, 682 | column: 15 683 | } 684 | ] 685 | }, 686 | { 687 | filename: 'Profile.js', 688 | code: ` 689 | type Props = { 690 | user: { 691 | id: number, 692 | } | undefined | null, 693 | }; 694 | class Profile extends React.Component {} 695 | createFragmentContainer(Profile, { 696 | user: graphql\` 697 | fragment Profile_user on User { 698 | id 699 | } 700 | \`, 701 | }); 702 | `, 703 | errors: [ 704 | { 705 | message: 706 | 'Component property `user` expects to use the generated ' + 707 | '`Profile_user` typescript type. See ' + 708 | 'https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 709 | line: 7, 710 | column: 15 711 | } 712 | ] 713 | }, 714 | { 715 | filename: 'MyComponent.jsx', 716 | code: ` 717 | class MyComponent extends React.Component<{somethingElse: number}> { 718 | render() { 719 | return
; 720 | } 721 | } 722 | 723 | createFragmentContainer(MyComponent, { 724 | user: graphql\`fragment MyComponent_user on User {id}\`, 725 | }); 726 | `, 727 | options: DEFAULT_OPTIONS, 728 | output: ` 729 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 730 | class MyComponent extends React.Component<{user: MyComponent_user, somethingElse: number}> { 731 | render() { 732 | return
; 733 | } 734 | } 735 | 736 | createFragmentContainer(MyComponent, { 737 | user: graphql\`fragment MyComponent_user on User {id}\`, 738 | }); 739 | `, 740 | errors: [ 741 | { 742 | message: 743 | '`user` is not declared in the `props` of the React component or it is not marked with the generated typescript type `MyComponent_user`. ' + 744 | 'See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 745 | line: 2, 746 | column: 15 747 | } 748 | ] 749 | }, 750 | { 751 | filename: 'MyComponent.jsx', 752 | code: ` 753 | class MyComponent extends React.Component<{user: number}> { 754 | render() { 755 | return
; 756 | } 757 | } 758 | 759 | createFragmentContainer(MyComponent, { 760 | user: graphql\`fragment MyComponent_user on User {id}\`, 761 | }); 762 | `, 763 | options: DEFAULT_OPTIONS, 764 | output: ` 765 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 766 | class MyComponent extends React.Component<{user: MyComponent_user}> { 767 | render() { 768 | return
; 769 | } 770 | } 771 | 772 | createFragmentContainer(MyComponent, { 773 | user: graphql\`fragment MyComponent_user on User {id}\`, 774 | }); 775 | `, 776 | errors: [ 777 | { 778 | message: 779 | 'Component property `user` expects to use the generated ' + 780 | '`MyComponent_user` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 781 | line: 2, 782 | column: 15 783 | } 784 | ] 785 | }, 786 | { 787 | filename: 'path/to/Example.js', 788 | // Test multiple layers of intersection types. 789 | code: ` 790 | type Props = {} & {}; 791 | type MergedProps = {} & Props; 792 | class Example extends React.PureComponent { 793 | } 794 | module.exports = createFragmentContainer(Example, { 795 | user: graphql\`fragment Example_user on User { id }\`, 796 | }); 797 | `, 798 | errors: [ 799 | { 800 | message: 801 | '`user` is not declared in the `props` of the React component or it is not marked with the generated typescript type `Example_user`. ' + 802 | 'See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 803 | line: 4, 804 | column: 15 805 | } 806 | ] 807 | }, 808 | { 809 | filename: 'MyComponent.jsx', 810 | code: ` 811 | class MyComponent extends React.Component<{user: Random_user}> { 812 | render() { 813 | return
; 814 | } 815 | } 816 | 817 | createFragmentContainer(MyComponent, { 818 | user: graphql\`fragment MyComponent_user on User {id}\`, 819 | }); 820 | `, 821 | options: DEFAULT_OPTIONS, 822 | output: ` 823 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 824 | class MyComponent extends React.Component<{user: MyComponent_user}> { 825 | render() { 826 | return
; 827 | } 828 | } 829 | 830 | createFragmentContainer(MyComponent, { 831 | user: graphql\`fragment MyComponent_user on User {id}\`, 832 | }); 833 | `, 834 | errors: [ 835 | { 836 | message: 837 | 'Component property `user` expects to use the generated ' + 838 | '`MyComponent_user` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 839 | line: 2, 840 | column: 15 841 | } 842 | ] 843 | }, 844 | { 845 | filename: 'MyComponent.jsx', 846 | code: ` 847 | type Props = {}; 848 | 849 | class MyComponent extends React.Component { 850 | render() { 851 | return
; 852 | } 853 | } 854 | 855 | createFragmentContainer(MyComponent, { 856 | user: graphql\`fragment MyComponent_user on User {id}\`, 857 | }); 858 | `, 859 | options: DEFAULT_OPTIONS, 860 | output: ` 861 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 862 | type Props = {user: MyComponent_user}; 863 | 864 | class MyComponent extends React.Component { 865 | render() { 866 | return
; 867 | } 868 | } 869 | 870 | createFragmentContainer(MyComponent, { 871 | user: graphql\`fragment MyComponent_user on User {id}\`, 872 | }); 873 | `, 874 | errors: [ 875 | { 876 | message: 877 | '`user` is not declared in the `props` of the React component or it is not marked with the generated typescript type `MyComponent_user`. ' + 878 | 'See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 879 | line: 4, 880 | column: 15 881 | } 882 | ] 883 | }, 884 | { 885 | filename: 'MyComponent.jsx', 886 | code: ` 887 | type Props = {somethingElse: number}; 888 | 889 | class MyComponent extends React.Component { 890 | render() { 891 | return
; 892 | } 893 | } 894 | 895 | createFragmentContainer(MyComponent, { 896 | user: graphql\`fragment MyComponent_user on User {id}\`, 897 | }); 898 | `, 899 | options: DEFAULT_OPTIONS, 900 | output: ` 901 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 902 | type Props = {user: MyComponent_user, somethingElse: number}; 903 | 904 | class MyComponent extends React.Component { 905 | render() { 906 | return
; 907 | } 908 | } 909 | 910 | createFragmentContainer(MyComponent, { 911 | user: graphql\`fragment MyComponent_user on User {id}\`, 912 | }); 913 | `, 914 | errors: [ 915 | { 916 | message: 917 | '`user` is not declared in the `props` of the React component or it is not marked with the generated typescript type `MyComponent_user`. ' + 918 | 'See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 919 | line: 4, 920 | column: 15 921 | } 922 | ] 923 | }, 924 | { 925 | filename: 'MyComponent.jsx', 926 | code: ` 927 | type Props = {user: number}; 928 | 929 | class MyComponent extends React.Component { 930 | render() { 931 | return
; 932 | } 933 | } 934 | 935 | createFragmentContainer(MyComponent, { 936 | user: graphql\`fragment MyComponent_user on User {id}\`, 937 | }); 938 | `, 939 | options: DEFAULT_OPTIONS, 940 | output: ` 941 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 942 | type Props = {user: MyComponent_user}; 943 | 944 | class MyComponent extends React.Component { 945 | render() { 946 | return
; 947 | } 948 | } 949 | 950 | createFragmentContainer(MyComponent, { 951 | user: graphql\`fragment MyComponent_user on User {id}\`, 952 | }); 953 | `, 954 | errors: [ 955 | { 956 | message: 957 | 'Component property `user` expects to use the generated ' + 958 | '`MyComponent_user` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 959 | line: 4, 960 | column: 15 961 | } 962 | ] 963 | }, 964 | { 965 | filename: 'MyComponent.jsx', 966 | code: ` 967 | type Props = {user: Random_user}; 968 | 969 | class MyComponent extends React.Component { 970 | render() { 971 | return
; 972 | } 973 | } 974 | 975 | createFragmentContainer(MyComponent, { 976 | user: graphql\`fragment MyComponent_user on User {id}\`, 977 | }); 978 | `, 979 | options: DEFAULT_OPTIONS, 980 | output: ` 981 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 982 | type Props = {user: MyComponent_user}; 983 | 984 | class MyComponent extends React.Component { 985 | render() { 986 | return
; 987 | } 988 | } 989 | 990 | createFragmentContainer(MyComponent, { 991 | user: graphql\`fragment MyComponent_user on User {id}\`, 992 | }); 993 | `, 994 | errors: [ 995 | { 996 | message: 997 | 'Component property `user` expects to use the generated ' + 998 | '`MyComponent_user` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 999 | line: 4, 1000 | column: 15 1001 | } 1002 | ] 1003 | }, 1004 | { 1005 | filename: 'MyComponent.jsx', 1006 | code: ` 1007 | class MyComponent extends React.Component { 1008 | render() { 1009 | return
; 1010 | } 1011 | } 1012 | 1013 | createFragmentContainer(MyComponent, { 1014 | user: graphql\`fragment MyComponent_user on User {id}\`, 1015 | }); 1016 | `, 1017 | output: HAS_ESLINT_BEEN_UPGRADED_YET 1018 | ? ` 1019 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 1020 | type Props = {user: MyComponent_user}; 1021 | 1022 | class MyComponent extends React.Component { 1023 | render() { 1024 | return
; 1025 | } 1026 | } 1027 | 1028 | createFragmentContainer(MyComponent, { 1029 | user: graphql\`fragment MyComponent_user on User {id}\`, 1030 | }); 1031 | ` 1032 | : null, 1033 | errors: [ 1034 | { 1035 | message: 1036 | 'Component property `user` expects to use the generated ' + 1037 | '`MyComponent_user` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 1038 | line: 2, 1039 | column: 15 1040 | } 1041 | ] 1042 | }, 1043 | { 1044 | filename: 'MyComponent.jsx', 1045 | code: ` 1046 | class MyComponent extends React.Component { 1047 | render() { 1048 | return
; 1049 | } 1050 | } 1051 | 1052 | createFragmentContainer(MyComponent, { 1053 | user: graphql\`fragment MyComponent_user on User {id}\`, 1054 | }); 1055 | `, 1056 | options: [{haste: true}], 1057 | output: HAS_ESLINT_BEEN_UPGRADED_YET 1058 | ? ` 1059 | import type {MyComponent_user} from 'MyComponent_user.graphql' 1060 | type Props = {user: MyComponent_user}; 1061 | 1062 | class MyComponent extends React.Component { 1063 | render() { 1064 | return
; 1065 | } 1066 | } 1067 | 1068 | createFragmentContainer(MyComponent, { 1069 | user: graphql\`fragment MyComponent_user on User {id}\`, 1070 | }); 1071 | ` 1072 | : null, 1073 | errors: [ 1074 | { 1075 | message: 1076 | 'Component property `user` expects to use the generated ' + 1077 | '`MyComponent_user` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 1078 | line: 2, 1079 | column: 15 1080 | } 1081 | ] 1082 | }, 1083 | { 1084 | filename: 'MyComponent.jsx', 1085 | code: ` 1086 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 1087 | 1088 | class MyComponent extends React.Component { 1089 | render() { 1090 | return
; 1091 | } 1092 | } 1093 | 1094 | createFragmentContainer(MyComponent, { 1095 | user: graphql\`fragment MyComponent_user on User {id}\`, 1096 | }); 1097 | `, 1098 | output: HAS_ESLINT_BEEN_UPGRADED_YET 1099 | ? ` 1100 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 1101 | 1102 | type Props = {user: MyComponent_user}; 1103 | 1104 | class MyComponent extends React.Component { 1105 | render() { 1106 | return
; 1107 | } 1108 | } 1109 | 1110 | createFragmentContainer(MyComponent, { 1111 | user: graphql\`fragment MyComponent_user on User {id}\`, 1112 | }); 1113 | ` 1114 | : null, 1115 | errors: [ 1116 | { 1117 | message: 1118 | 'Component property `user` expects to use the generated ' + 1119 | '`MyComponent_user` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 1120 | line: 4, 1121 | column: 15 1122 | } 1123 | ] 1124 | }, 1125 | { 1126 | filename: 'MyComponent.jsx', 1127 | code: ` 1128 | import type aaa from 'aaa' 1129 | import type zzz from 'zzz' 1130 | 1131 | class MyComponent extends React.Component { 1132 | render() { 1133 | return
; 1134 | } 1135 | } 1136 | 1137 | createFragmentContainer(MyComponent, { 1138 | user: graphql\`fragment MyComponent_user on User {id}\`, 1139 | }); 1140 | `, 1141 | output: HAS_ESLINT_BEEN_UPGRADED_YET 1142 | ? ` 1143 | import type aaa from 'aaa' 1144 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 1145 | import type zzz from 'zzz' 1146 | 1147 | type Props = {user: MyComponent_user}; 1148 | 1149 | class MyComponent extends React.Component { 1150 | render() { 1151 | return
; 1152 | } 1153 | } 1154 | 1155 | createFragmentContainer(MyComponent, { 1156 | user: graphql\`fragment MyComponent_user on User {id}\`, 1157 | }); 1158 | ` 1159 | : null, 1160 | errors: [ 1161 | { 1162 | message: 1163 | 'Component property `user` expects to use the generated ' + 1164 | '`MyComponent_user` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 1165 | line: 5, 1166 | column: 15 1167 | } 1168 | ] 1169 | }, 1170 | { 1171 | filename: 'MyComponent.jsx', 1172 | code: ` 1173 | import type {aaa} from 'aaa' 1174 | import type zzz from 'zzz' 1175 | 1176 | class MyComponent extends React.Component { 1177 | render() { 1178 | return
; 1179 | } 1180 | } 1181 | 1182 | createFragmentContainer(MyComponent, { 1183 | user: graphql\`fragment MyComponent_user on User {id}\`, 1184 | }); 1185 | `, 1186 | output: HAS_ESLINT_BEEN_UPGRADED_YET 1187 | ? ` 1188 | import type {aaa} from 'aaa' 1189 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 1190 | import type zzz from 'zzz' 1191 | 1192 | type Props = {user: MyComponent_user}; 1193 | 1194 | class MyComponent extends React.Component { 1195 | render() { 1196 | return
; 1197 | } 1198 | } 1199 | 1200 | createFragmentContainer(MyComponent, { 1201 | user: graphql\`fragment MyComponent_user on User {id}\`, 1202 | }); 1203 | ` 1204 | : null, 1205 | errors: [ 1206 | { 1207 | message: 1208 | 'Component property `user` expects to use the generated ' + 1209 | '`MyComponent_user` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 1210 | line: 5, 1211 | column: 15 1212 | } 1213 | ] 1214 | }, 1215 | { 1216 | filename: 'MyComponent.jsx', 1217 | code: ` 1218 | import {aaa} from 'aaa' 1219 | import zzz from 'zzz' 1220 | 1221 | class MyComponent extends React.Component { 1222 | render() { 1223 | return
; 1224 | } 1225 | } 1226 | 1227 | createFragmentContainer(MyComponent, { 1228 | user: graphql\`fragment MyComponent_user on User {id}\`, 1229 | }); 1230 | `, 1231 | output: HAS_ESLINT_BEEN_UPGRADED_YET 1232 | ? ` 1233 | import {aaa} from 'aaa' 1234 | import zzz from 'zzz' 1235 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 1236 | 1237 | type Props = {user: MyComponent_user}; 1238 | 1239 | class MyComponent extends React.Component { 1240 | render() { 1241 | return
; 1242 | } 1243 | } 1244 | 1245 | createFragmentContainer(MyComponent, { 1246 | user: graphql\`fragment MyComponent_user on User {id}\`, 1247 | }); 1248 | ` 1249 | : null, 1250 | errors: [ 1251 | { 1252 | message: 1253 | 'Component property `user` expects to use the generated ' + 1254 | '`MyComponent_user` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 1255 | line: 5, 1256 | column: 15 1257 | } 1258 | ] 1259 | }, 1260 | { 1261 | filename: 'MyComponent.jsx', 1262 | code: ` 1263 | const aaa = require('aaa') 1264 | const zzz = require('zzz') 1265 | 1266 | class MyComponent extends React.Component { 1267 | render() { 1268 | return
; 1269 | } 1270 | } 1271 | 1272 | createFragmentContainer(MyComponent, { 1273 | user: graphql\`fragment MyComponent_user on User {id}\`, 1274 | }); 1275 | `, 1276 | output: HAS_ESLINT_BEEN_UPGRADED_YET 1277 | ? ` 1278 | const aaa = require('aaa') 1279 | const zzz = require('zzz') 1280 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 1281 | 1282 | type Props = {user: MyComponent_user}; 1283 | 1284 | class MyComponent extends React.Component { 1285 | render() { 1286 | return
; 1287 | } 1288 | } 1289 | 1290 | createFragmentContainer(MyComponent, { 1291 | user: graphql\`fragment MyComponent_user on User {id}\`, 1292 | }); 1293 | ` 1294 | : null, 1295 | errors: [ 1296 | { 1297 | message: 1298 | 'Component property `user` expects to use the generated ' + 1299 | '`MyComponent_user` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 1300 | line: 5, 1301 | column: 15 1302 | } 1303 | ] 1304 | }, 1305 | { 1306 | filename: 'MyComponent.jsx', 1307 | code: ` 1308 | const aaa = require('aaa') 1309 | 1310 | import zzz from 'zzz' 1311 | 1312 | import type ccc from 'ccc' 1313 | import type {xxx} from 'xxx' 1314 | 1315 | class MyComponent extends React.Component { 1316 | render() { 1317 | return
; 1318 | } 1319 | } 1320 | 1321 | createFragmentContainer(MyComponent, { 1322 | user: graphql\`fragment MyComponent_user on User {id}\`, 1323 | }); 1324 | `, 1325 | output: HAS_ESLINT_BEEN_UPGRADED_YET 1326 | ? ` 1327 | const aaa = require('aaa') 1328 | 1329 | import zzz from 'zzz' 1330 | 1331 | import type ccc from 'ccc' 1332 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 1333 | import type {xxx} from 'xxx' 1334 | 1335 | type Props = {user: MyComponent_user}; 1336 | 1337 | class MyComponent extends React.Component { 1338 | render() { 1339 | return
; 1340 | } 1341 | } 1342 | 1343 | createFragmentContainer(MyComponent, { 1344 | user: graphql\`fragment MyComponent_user on User {id}\`, 1345 | }); 1346 | ` 1347 | : null, 1348 | errors: [ 1349 | { 1350 | message: 1351 | 'Component property `user` expects to use the generated ' + 1352 | '`MyComponent_user` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 1353 | line: 9, 1354 | column: 15 1355 | } 1356 | ] 1357 | }, 1358 | { 1359 | filename: 'MyComponent.jsx', 1360 | code: ` 1361 | import type {MyComponent_user as User} from 'aaa' 1362 | 1363 | class MyComponent extends React.Component<{ user: MyComponent_user }> { 1364 | render() { 1365 | return
; 1366 | } 1367 | } 1368 | 1369 | createFragmentContainer(MyComponent, { 1370 | user: graphql\`fragment MyComponent_user on User {id}\`, 1371 | }); 1372 | `, 1373 | output: HAS_ESLINT_BEEN_UPGRADED_YET 1374 | ? ` 1375 | import type {MyComponent_user as User} from 'aaa' 1376 | 1377 | class MyComponent extends React.Component<{ user: User }> { 1378 | render() { 1379 | return
; 1380 | } 1381 | } 1382 | 1383 | createFragmentContainer(MyComponent, { 1384 | user: graphql\`fragment MyComponent_user on User {id}\`, 1385 | }); 1386 | ` 1387 | : null, 1388 | errors: [ 1389 | { 1390 | message: 1391 | 'Component property `user` expects to use the generated ' + 1392 | '`User` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 1393 | line: 4, 1394 | column: 15 1395 | } 1396 | ] 1397 | }, 1398 | { 1399 | filename: 'MyComponent.jsx', 1400 | code: ` 1401 | type OtherProps = { 1402 | other: string 1403 | } 1404 | 1405 | type Props = { 1406 | user: any | null | undefined, 1407 | } & OtherProps; 1408 | 1409 | class MyComponent extends React.Component { 1410 | render() { 1411 | return
; 1412 | } 1413 | } 1414 | 1415 | createFragmentContainer(MyComponent, { 1416 | user: graphql\`fragment MyComponent_user on User {id}\`, 1417 | }); 1418 | `, 1419 | output: HAS_ESLINT_BEEN_UPGRADED_YET 1420 | ? ` 1421 | import type {MyComponent_user} from './__generated__/MyComponent_user.graphql' 1422 | class MyComponent extends React.Component<{user: MyComponent_user}> { 1423 | render() { 1424 | return
; 1425 | } 1426 | } 1427 | 1428 | createFragmentContainer(MyComponent, { 1429 | user: graphql\`fragment MyComponent_user on User {id}\`, 1430 | }); 1431 | ` 1432 | : null, 1433 | errors: [ 1434 | { 1435 | message: 1436 | 'Component property `user` expects to use the generated ' + 1437 | '`MyComponent_user` typescript type. See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 1438 | line: 10, 1439 | column: 15 1440 | } 1441 | ] 1442 | }, 1443 | { 1444 | filename: 'MyComponent.jsx', 1445 | code: ` 1446 | type RelayProps = { 1447 | user: string 1448 | } 1449 | 1450 | type Props = { 1451 | other: any | null | undefined, 1452 | } & RelayProps; 1453 | 1454 | class MyComponent extends React.Component { 1455 | render() { 1456 | return
; 1457 | } 1458 | } 1459 | 1460 | createFragmentContainer(MyComponent, { 1461 | user: graphql\`fragment MyComponent_user on User {id}\`, 1462 | }); 1463 | `, 1464 | errors: [ 1465 | { 1466 | message: 1467 | '`user` is not declared in the `props` of the React component or it is not marked with the generated typescript type `MyComponent_user`. ' + 1468 | 'See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 1469 | line: 10, 1470 | column: 15 1471 | } 1472 | ] 1473 | }, 1474 | { 1475 | filename: 'MyComponent.jsx', 1476 | code: ` 1477 | type RelayProps = { 1478 | users: MyComponent_user 1479 | } 1480 | 1481 | type Props = { 1482 | other: any | string | undefined, 1483 | } & RelayProps 1484 | 1485 | class MyComponent extends React.Component { 1486 | render() { 1487 | return
; 1488 | } 1489 | } 1490 | 1491 | createFragmentContainer(MyComponent, { 1492 | user: graphql\`fragment MyComponent_user on User {id}\`, 1493 | }); 1494 | `, 1495 | errors: [ 1496 | { 1497 | message: 1498 | '`user` is not declared in the `props` of the React component or it is not marked with the generated typescript type `MyComponent_user`. ' + 1499 | 'See https://facebook.github.io/relay/docs/en/graphql-in-relay.html#importing-generated-definitions', 1500 | line: 10, 1501 | column: 15 1502 | } 1503 | ] 1504 | } 1505 | ] 1506 | } 1507 | ); 1508 | -------------------------------------------------------------------------------- /test/hook-required-argument.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const rules = require('..').rules; 11 | const RuleTester = require('eslint').RuleTester; 12 | 13 | const ruleTester = new RuleTester({ 14 | languageOptions: { 15 | ecmaVersion: 6, 16 | parser: require('@typescript-eslint/parser') 17 | } 18 | }); 19 | 20 | ruleTester.run('hook-required-argument', rules['hook-required-argument'], { 21 | valid: [ 22 | { 23 | code: ` 24 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 25 | useFragment(graphql\`fragment TestFragment_foo on User { id }\`, ref) 26 | ` 27 | }, 28 | { 29 | code: ` 30 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 31 | useRefetchableFragment(graphql\`fragment TestFragment_foo on User { id }\`, ref) 32 | ` 33 | }, 34 | { 35 | code: ` 36 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 37 | usePaginationFragment(graphql\`fragment TestFragment_foo on User { id }\`, ref) 38 | ` 39 | }, 40 | { 41 | code: ` 42 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 43 | useBlockingPaginationFragment(graphql\`fragment TestFragment_foo on User { id }\`, ref) 44 | ` 45 | }, 46 | { 47 | code: ` 48 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 49 | useLegacyPaginationFragment(graphql\`fragment TestFragment_foo on User { id }\`, ref) 50 | ` 51 | } 52 | ], 53 | invalid: [ 54 | { 55 | code: ` 56 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 57 | useFragment(graphql\`fragment TestFragment_foo on User { id }\`) 58 | `, 59 | errors: [ 60 | { 61 | message: 62 | 'A fragment reference should be passed to the `useFragment` hook', 63 | line: 3 64 | } 65 | ] 66 | }, 67 | { 68 | code: ` 69 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 70 | usePaginationFragment(graphql\`fragment TestFragment_foo on User { id }\`) 71 | `, 72 | errors: [ 73 | { 74 | message: 75 | 'A fragment reference should be passed to the `usePaginationFragment` hook', 76 | line: 3 77 | } 78 | ] 79 | }, 80 | { 81 | code: ` 82 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 83 | useBlockingPaginationFragment(graphql\`fragment TestFragment_foo on User { id }\`) 84 | `, 85 | errors: [ 86 | { 87 | message: 88 | 'A fragment reference should be passed to the `useBlockingPaginationFragment` hook', 89 | line: 3 90 | } 91 | ] 92 | }, 93 | { 94 | code: ` 95 | import type {TestFragment_foo$key} from 'TestFragment_foo.graphql'; 96 | useLegacyPaginationFragment(graphql\`fragment TestFragment_foo on User { id }\`) 97 | `, 98 | errors: [ 99 | { 100 | message: 101 | 'A fragment reference should be passed to the `useLegacyPaginationFragment` hook', 102 | line: 3 103 | } 104 | ] 105 | } 106 | ] 107 | }); 108 | -------------------------------------------------------------------------------- /test/must-colocate-fragment-spreads.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | var eslint = require('eslint'); 11 | 12 | const rules = require('..').rules; 13 | const RuleTester = eslint.RuleTester; 14 | 15 | const ruleTester = new RuleTester({ 16 | languageOptions: { 17 | ecmaVersion: 6, 18 | parser: require('@typescript-eslint/parser') 19 | } 20 | }); 21 | 22 | function unusedFieldsWarning(fragment) { 23 | return ( 24 | `This spreads the fragment \`${fragment}\` but ` + 25 | 'this module does not use it directly. If a different module ' + 26 | 'needs this information, that module should directly define a ' + 27 | 'fragment querying for that data, colocated next to where the ' + 28 | 'data is used.\n' 29 | ); 30 | } 31 | 32 | ruleTester.run( 33 | 'must-colocate-fragment-spreads', 34 | rules['must-colocate-fragment-spreads'], 35 | { 36 | valid: [ 37 | ` 38 | import { Component } from '../shared/component.js'; 39 | graphql\`fragment foo on Page { 40 | ...component_fragment 41 | }\`; 42 | `, 43 | ` 44 | const Component = require('../shared/component.js'); 45 | graphql\`fragment foo on Page { 46 | ...component_fragment 47 | }\`; 48 | `, 49 | ` 50 | const Component = import('../shared/component.js'); 51 | graphql\`fragment foo on Page { 52 | ...component_fragment 53 | }\`; 54 | `, 55 | ` 56 | import { Component } from './nested/componentModule.js'; 57 | graphql\`fragment foo on Page { 58 | ...componentModule_fragment 59 | }\`; 60 | `, 61 | ` 62 | import { Component } from './component-module.js'; 63 | graphql\`fragment foo on Page { 64 | ...componentModuleFragment 65 | }\`; 66 | `, 67 | ` 68 | import { Component } from './component-module.js'; 69 | graphql\`query Root { 70 | ...componentModuleFragment 71 | }\`; 72 | `, 73 | ` 74 | graphql\`fragment foo1 on Page { 75 | name 76 | }\`; 77 | graphql\`fragment foo2 on Page { 78 | ...foo1 79 | }\`; 80 | `, 81 | ` 82 | graphql\`mutation { 83 | page_unlike(data: $input) { 84 | ...component_fragment 85 | ...componentFragment 86 | ...component 87 | } 88 | }\` 89 | `, 90 | ` 91 | graphql\`fragment foo on Page { ...Fragment @relay(mask: false) }\`; 92 | `, 93 | ` 94 | graphql\`fragment foo on Page { ...Fragment @module(name: "ComponentName.react") }\`; 95 | `, 96 | '\ 97 | const getOperation = (reference) => {\ 98 | return import(`./src/__generated__/${reference}`);\ 99 | };\ 100 | ', 101 | '\ 102 | const getOperation = (reference) => {\ 103 | return import(reference);\ 104 | };\ 105 | ', 106 | ` 107 | graphql\`fragment foo on Page { 108 | # eslint-disable-next-line relay/must-colocate-fragment-spreads 109 | ...unused1 110 | }\`; 111 | ` 112 | ], 113 | invalid: [ 114 | { 115 | code: ` 116 | graphql\`fragment foo on Page { ...unused1 }\`; 117 | `, 118 | errors: [ 119 | { 120 | message: unusedFieldsWarning('unused1'), 121 | line: 2 122 | } 123 | ] 124 | }, 125 | { 126 | code: ` 127 | graphql\`fragment Test on Page { ...unused1, ...unused2 }\`; 128 | `, 129 | errors: [unusedFieldsWarning('unused1'), unusedFieldsWarning('unused2')] 130 | }, 131 | { 132 | code: ` 133 | graphql\`query Root { ...unused1 }\`; 134 | `, 135 | errors: [ 136 | { 137 | message: unusedFieldsWarning('unused1'), 138 | line: 2 139 | } 140 | ] 141 | }, 142 | { 143 | code: ` 144 | import { Component } from './used1.js'; 145 | graphql\`fragment foo on Page { ...used1 ...unused1 }\`; 146 | `, 147 | errors: [unusedFieldsWarning('unused1')] 148 | }, 149 | { 150 | code: ` 151 | graphql\`fragment foo on Page { ...unused1 @relay(mask: true) }\`; 152 | `, 153 | errors: [unusedFieldsWarning('unused1')] 154 | }, 155 | { 156 | code: ` 157 | import type { MyType } from '../shared/component.js'; 158 | graphql\`fragment foo on Page { 159 | ...component_fragment 160 | }\`; 161 | `, 162 | errors: [unusedFieldsWarning('component_fragment')] 163 | } 164 | ] 165 | } 166 | ); 167 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const rules = require('..').rules; 11 | const RuleTester = require('eslint').RuleTester; 12 | 13 | const HAS_ESLINT_BEEN_UPGRADED_YET = false; 14 | const DEFAULT_OPTIONS = [ 15 | { 16 | fix: true, 17 | haste: false 18 | } 19 | ]; 20 | 21 | const ruleTester = new RuleTester({ 22 | languageOptions: { 23 | ecmaVersion: 6, 24 | sourceType: 'module', 25 | parser: require('@babel/eslint-parser'), 26 | parserOptions: { 27 | requireConfigFile: false, 28 | babelOptions: {presets: ['@babel/preset-flow', '@babel/preset-react']} 29 | } 30 | } 31 | }); 32 | 33 | const valid = [ 34 | {code: 'hello();'}, 35 | {code: 'graphql`fragment Foo on Node { id }`'}, 36 | { 37 | filename: 'path/to/Example.react.js', 38 | code: ` 39 | createFragmentContainer(Component, { 40 | user: graphql\`fragment Example_user on User {id}\`, 41 | }); 42 | ` 43 | }, 44 | { 45 | filename: 'path/to/MyComponent.react.js', 46 | code: 'graphql`query MyComponent { me { name }}`;' 47 | }, 48 | { 49 | filename: 'path/to/MyComponent.react.js', 50 | code: 'graphql`query MyComponentBla { me { name }}`;' 51 | }, 52 | { 53 | filename: 'path/to/MyComponent.jsx', 54 | code: `createFragmentContainer(Component, { 55 | user: graphql\`fragment MyComponent_user on User {id}\`, 56 | });` 57 | } 58 | ]; 59 | 60 | ruleTester.run('graphql-syntax', rules['graphql-syntax'], { 61 | valid: valid, 62 | invalid: [ 63 | // missing name on query 64 | { 65 | filename: 'path/to/Example.react.js', 66 | code: [ 67 | 'graphql`query{test}`;', 68 | 'graphql`{test}`;', 69 | 'graphql`subscription {test}`;', 70 | 'graphql`mutation {test}`;' 71 | ].join('\n'), 72 | errors: [ 73 | { 74 | message: 'Operations in graphql tags require a name.', 75 | line: 1, 76 | column: 9 77 | }, 78 | { 79 | message: 'Operations in graphql tags require a name.', 80 | line: 2, 81 | column: 9 82 | }, 83 | { 84 | message: 'Operations in graphql tags require a name.', 85 | line: 3, 86 | column: 9 87 | }, 88 | { 89 | message: 'Operations in graphql tags require a name.', 90 | line: 4, 91 | column: 9 92 | } 93 | ] 94 | }, 95 | { 96 | code: 'test;\ngraphql`fragment Test on User { ${x} }`;', 97 | errors: [ 98 | { 99 | message: 100 | 'graphql tagged templates do not support ${...} substitutions.' 101 | } 102 | ] 103 | }, 104 | { 105 | code: 'graphql`fragment Test on User { id } fragment Test2 on User { id }`;', 106 | errors: [ 107 | { 108 | message: 109 | 'graphql tagged templates can only contain a single definition.' 110 | } 111 | ] 112 | }, 113 | { 114 | filename: '/path/to/test.js', 115 | code: 'graphql`fragment F on User {\n id()\n}`;', 116 | errors: [ 117 | { 118 | message: `Syntax Error: Expected Name, found ")".` 119 | } 120 | ] 121 | } 122 | ] 123 | }); 124 | 125 | ruleTester.run('graphql-naming', rules['graphql-naming'], { 126 | valid: valid.concat([ 127 | // syntax error, covered by `graphql-syntax` 128 | {code: 'graphql`query {{{`'} 129 | ]), 130 | invalid: [ 131 | { 132 | filename: 'path/to/Example.react.js', 133 | code: ' graphql` query RandomName { me { name }}`;', 134 | errors: [ 135 | { 136 | message: 137 | 'Operations should start with the module name. Expected prefix ' + 138 | '`Example`, got `RandomName`.', 139 | line: 1, 140 | column: 28, 141 | endLine: 1, 142 | endColumn: 38 143 | } 144 | ] 145 | }, 146 | { 147 | filename: 'path/to/Example.react.js', 148 | code: ` 149 | const createFragmentContainer = require('relay-runtime'); 150 | var UserFragment; 151 | createFragmentContainer(Component, { 152 | user: junk\`fragment Example_user on User { id }\`, 153 | }); 154 | `, 155 | errors: [ 156 | { 157 | message: 158 | '`createFragmentContainer` expects GraphQL to be tagged with ' + 159 | 'graphql`...`.' 160 | } 161 | ] 162 | }, 163 | { 164 | filename: 'path/to/Example.react.js', 165 | code: ` 166 | const createFragmentContainer = require('relay-runtime'); 167 | var UserFragment; 168 | createFragmentContainer(Component, { 169 | user: UserFragment, 170 | }); 171 | `, 172 | errors: [ 173 | { 174 | message: 175 | '`createFragmentContainer` expects fragment definitions to be ' + 176 | '`key: graphql`.' 177 | } 178 | ] 179 | }, 180 | { 181 | filename: 'MyComponent.jsx', 182 | code: ` 183 | createFragmentContainer(Component, { 184 | user: graphql\`fragment Random on User {id}\`, 185 | }); 186 | `, 187 | output: ` 188 | createFragmentContainer(Component, { 189 | user: graphql\`fragment MyComponent_user on User {id}\`, 190 | }); 191 | `, 192 | errors: [ 193 | { 194 | message: 195 | 'Container fragment names must be `_`. Got ' + 196 | '`Random`, expected `MyComponent_user`.' 197 | } 198 | ] 199 | }, 200 | { 201 | code: ` 202 | createFragmentContainer(Component, { 203 | [user]: graphql\`fragment Random on User {id}\`, 204 | }); 205 | `, 206 | errors: [ 207 | { 208 | message: 209 | '`createFragmentContainer` expects fragment definitions to be ' + 210 | '`key: graphql`.' 211 | } 212 | ] 213 | } 214 | ] 215 | }); 216 | -------------------------------------------------------------------------------- /test/unused-fields.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | var eslint = require('eslint'); 11 | 12 | const rules = require('..').rules; 13 | const RuleTester = eslint.RuleTester; 14 | 15 | const ruleTester = new RuleTester({ 16 | languageOptions: { 17 | ecmaVersion: 6, 18 | parser: require('@typescript-eslint/parser') 19 | } 20 | }); 21 | 22 | function unusedFieldsWarning(field) { 23 | return ( 24 | `This queries for the field \`${field}\` but this file does ` + 25 | 'not seem to use it directly. If a different file needs this ' + 26 | 'information that file should export a fragment and colocate ' + 27 | 'the query for the data with the usage.\n' + 28 | 'If only interested in the existence of a record, __typename ' + 29 | 'can be used without this warning.' 30 | ); 31 | } 32 | 33 | ruleTester.run('unused-fields', rules['unused-fields'], { 34 | valid: [ 35 | ` 36 | graphql\`fragment foo on Page { name2 }\`; 37 | props.page.name; 38 | foo.name2; 39 | `, 40 | 'graphql`fragment foo on Page { __typename }`;', 41 | // Syntax error is ignored by this rule 42 | `graphql\`fragment Test { name2 }\`;`, 43 | ` 44 | graphql\`fragment Test on InternalTask { 45 | owner: task_owner { 46 | name: full_name 47 | } 48 | }\`; 49 | node.owner.name; 50 | `, 51 | 'graphql`fragment Test on Page { ...Other_x }`;', 52 | ` 53 | const { 54 | normal, 55 | aliased: v1, 56 | [computed]: x, 57 | nested: { v2 }, 58 | ...rest 59 | } = foo; 60 | `, 61 | 'graphql`mutation { page_unlike(data: $input) }`', 62 | 'String.raw`foo bar`', 63 | // Facebook naming of page info fields 64 | ` 65 | graphql\` 66 | fragment foo on Page { 67 | page_info { 68 | has_next_page 69 | has_previous_page 70 | end_cursor 71 | start_cursor 72 | } 73 | } 74 | \`; 75 | `, 76 | // OSS naming of page info fields 77 | ` 78 | graphql\` 79 | fragment foo on Page { 80 | pageInfo { 81 | hasNextPage 82 | hasPreviousPage 83 | endCursor 84 | startCursor 85 | } 86 | } 87 | \`; 88 | `, 89 | ` 90 | graphql\`fragment foo on Page { 91 | # eslint-disable-next-line relay/unused-fields 92 | name 93 | }\`; 94 | ` 95 | ], 96 | invalid: [ 97 | { 98 | code: ` 99 | graphql\` 100 | fragment Test on Page { 101 | name 102 | name2 103 | } 104 | \`; 105 | props.page.name; 106 | `, 107 | errors: [ 108 | { 109 | message: unusedFieldsWarning('name2'), 110 | line: 5 111 | } 112 | ] 113 | }, 114 | { 115 | code: ` 116 | graphql\`fragment Test on Page { unused1, unused2 }\`; 117 | `, 118 | errors: [unusedFieldsWarning('unused1'), unusedFieldsWarning('unused2')] 119 | }, 120 | { 121 | code: ` 122 | const getByPath = require('getByPath'); 123 | graphql\`fragment Test on Page { unused1, used1, used2 }\`; 124 | alert(getByPath(obj, ['foo', 'used1', 'used2'])) 125 | `, 126 | errors: [unusedFieldsWarning('unused1')] 127 | }, 128 | { 129 | code: ` 130 | graphql\`fragment Test on Page { unused1, used1, used2 }\`; 131 | obj?.foo?.used1?.used2; 132 | `, 133 | errors: [unusedFieldsWarning('unused1')] 134 | }, 135 | { 136 | code: ` 137 | const dotAccess = require('dotAccess'); 138 | graphql\`fragment Test on Page { unused1, used1, used2 }\`; 139 | alert(dotAccess(obj, 'foo.used1.used2')) 140 | `, 141 | errors: [unusedFieldsWarning('unused1')] 142 | }, 143 | { 144 | code: ` 145 | graphql\`fragment Test on Page { 146 | unused1 147 | unused2 148 | used1 149 | used2 150 | used3 151 | used4 152 | }\`; 153 | var { used1: unused1, used2: {used3} } = node; 154 | function test({used4}) { 155 | return x; 156 | } 157 | `, 158 | errors: [ 159 | { 160 | message: unusedFieldsWarning('unused1'), 161 | line: 3 162 | }, 163 | { 164 | message: unusedFieldsWarning('unused2'), 165 | line: 4 166 | } 167 | ] 168 | } 169 | ] 170 | }); 171 | --------------------------------------------------------------------------------