├── .eslint-doc-generatorrc.json ├── .eslintrc.cjs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── dist ├── index.d.ts ├── index.js ├── rules │ ├── awaitRequiresAsync.d.ts │ ├── awaitRequiresAsync.js │ ├── banDeprecatedIdParams.d.ts │ ├── banDeprecatedIdParams.js │ ├── banDeprecatedSyncMethods.d.ts │ ├── banDeprecatedSyncMethods.js │ ├── banDeprecatedSyncPropGetters.d.ts │ ├── banDeprecatedSyncPropGetters.js │ ├── banDeprecatedSyncPropSetters.d.ts │ ├── banDeprecatedSyncPropSetters.js │ ├── constrainProportionsReplacedByTargetAspectRatioAdvice.d.ts │ ├── constrainProportionsReplacedByTargetAspectRatioAdvice.js │ ├── dynamicPageDocumentchangeEventAdvice.d.ts │ ├── dynamicPageDocumentchangeEventAdvice.js │ ├── dynamicPageFindMethodAdvice.d.ts │ └── dynamicPageFindMethodAdvice.js ├── util.d.ts └── util.js ├── docs └── rules │ ├── await-requires-async.md │ ├── ban-deprecated-id-params.md │ ├── ban-deprecated-sync-methods.md │ ├── ban-deprecated-sync-prop-getters.md │ ├── ban-deprecated-sync-prop-setters.md │ ├── constrain-proportions-replaced-by-target-aspect-ratio-advice.md │ ├── dynamic-page-documentchange-event-advice.md │ └── dynamic-page-find-method-advice.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── index.ts ├── rules │ ├── awaitRequiresAsync.ts │ ├── banDeprecatedIdParams.ts │ ├── banDeprecatedSyncMethods.ts │ ├── banDeprecatedSyncPropGetters.ts │ ├── banDeprecatedSyncPropSetters.ts │ ├── constrainProportionsReplacedByTargetAspectRatioAdvice.ts │ ├── dynamicPageDocumentchangeEventAdvice.ts │ └── dynamicPageFindMethodAdvice.ts └── util.ts ├── test ├── awaitRequiresAsync.test.ts ├── banDeprecatedIdParams.test.ts ├── banDeprecatedSyncMethods.test.ts ├── banDeprecatedSyncPropGetters.test.ts ├── banDeprecatedSyncPropSetters.test.ts ├── constrainProportionsReplacedByTargetAspectRatio.test.ts ├── dynamicPageDocumentchangeEventAdvice.ts ├── dynamicPageFindMethodAdvice.test.ts ├── fixture │ ├── README.md │ ├── file.ts │ ├── file.tsx │ └── tsconfig.json └── testUtil.ts ├── tsconfig.build.json ├── tsconfig.json └── vscode-quickfix.gif /.eslint-doc-generatorrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "configEmoji": [ 3 | ["recommended", "👍"], 4 | ["recommended-problems-only", "🔦"] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended-type-checked', 6 | 'plugin:@typescript-eslint/stylistic-type-checked', 7 | ], 8 | ignorePatterns: ['dist/', 'node_modules/', 'test/fixture', '**/*.js'], 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | project: './tsconfig.json', 12 | }, 13 | plugins: ['@typescript-eslint'], 14 | root: true, 15 | rules: { 16 | '@typescript-eslint/no-unused-vars': [ 17 | 'error', // or "error" 18 | { 19 | argsIgnorePattern: '^_', 20 | varsIgnorePattern: '^_', 21 | caughtErrorsIgnorePattern: '^_', 22 | }, 23 | ], 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI checks 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | checks: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: 18 17 | cache: 'npm' 18 | - run: npm ci 19 | - run: npm run typecheck 20 | - run: npm run lint 21 | - run: npm run lint:docs 22 | - run: npm run test 23 | - name: Check that commit contains build artifacts 24 | run: | 25 | npm run build 26 | git diff --exit-code || (echo "Error: changes detected after build." && exit 1) 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | .pnpm-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # Snowpack dependency directory (https://snowpack.dev/) 48 | web_modules/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional stylelint cache 60 | .stylelintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variable files 78 | .env 79 | .env.development.local 80 | .env.test.local 81 | .env.production.local 82 | .env.local 83 | 84 | # parcel-bundler cache (https://parceljs.org/) 85 | .cache 86 | .parcel-cache 87 | 88 | # Next.js build output 89 | .next 90 | out 91 | 92 | # Nuxt.js build / generate output 93 | .nuxt 94 | 95 | # Gatsby files 96 | .cache/ 97 | # Comment in the public line in if your project uses Gatsby and not Next.js 98 | # https://nextjs.org/blog/next-9-1#public-directory-support 99 | # public 100 | 101 | # vuepress build output 102 | .vuepress/dist 103 | 104 | # vuepress v2.x temp and cache directory 105 | .temp 106 | .cache 107 | 108 | # Docusaurus cache and generated files 109 | .docusaurus 110 | 111 | # Serverless directories 112 | .serverless/ 113 | 114 | # FuseBox cache 115 | .fusebox/ 116 | 117 | # DynamoDB Local files 118 | .dynamodb/ 119 | 120 | # TernJS port file 121 | .tern-port 122 | 123 | # Stores VSCode versions used for testing VSCode extensions 124 | .vscode-test 125 | 126 | # yarn v2 127 | .yarn/cache 128 | .yarn/unplugged 129 | .yarn/build-state.yml 130 | .yarn/install-state.gz 131 | .pnp.* 132 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "arrowParens": "always" 8 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | Any changes that haven't been included in a published version will be listed here. 6 | 7 | ## 0.16.1 8 | 9 | - Ships changelog updates that were missing in 0.16.1. 10 | 11 | ## 0.16.0 12 | 13 | - Add the `constrain-proportions-replaced-by-target-aspect-ratio-advice` rule, which adds a warning that advises using `targetAspectRatio` in favor of `constrainProportions`. 14 | 15 | ## 0.15.0 16 | 17 | - package.json: restrict package files ([@andrii-bodnar](https://github.com/andrii-bodnar)) 18 | - package.json: explicitly specify the "types" field in package.json. This fixes incompatibilities with TypeScript 4 and/or Webpack. ([@andrii-bodnar](https://github.com/andrii-bodnar)) 19 | 20 | ## 0.14.0 21 | 22 | - Added `node.reactions = ...` to the list of banned property setters under `ban-deprecated-sync-prop-setters`. Use `node.setReactionsAsync` instead. 23 | 24 | ## 0.13.0 25 | 26 | - Updated `ban-deprecated-sync-prop-getters` rule to allow _assignment_ to properties. This allows for statements such as `instance.mainComponent = ...`. 27 | 28 | ## 0.12.0 29 | 30 | - Initial published version 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you notice any problems with this linter, please [file an issue](https://github.com/figma/rest-api-spec/issues). We welcome PRs! 4 | 5 | For bug reports and feature requests for Figma or the Figma Plugin API itself, please [contact Figma support](https://help.figma.com/hc/en-us/requests/new). 6 | 7 | ## Local development 8 | 9 | ### Building the package 10 | 11 | To rebuild both the lint plugin and documentation: 12 | 13 | ``` 14 | npm run build 15 | ``` 16 | 17 | To rebuild the lint plugin when source files change: 18 | 19 | ``` 20 | npm run watch 21 | ``` 22 | 23 | Any consuming repo will need to re-install the package using the process described [above](#install-the-package). 24 | 25 | ### Documentation 26 | 27 | This plugin uses [eslint-doc-generator](https://github.com/bmish/eslint-doc-generator) to produce docs for each rule, as well as parts of the [README](./README.md). 28 | 29 | To automatically re-build documentation for rules: 30 | 31 | ``` 32 | npm run update:eslint-docs 33 | ``` 34 | 35 | ### Tests 36 | 37 | Tests are implemented in the [test/](./test) directory using [@typescript-eslint/RuleTester](https://typescript-eslint.io/packages/rule-tester/). The test harness is [ts-jest](). 38 | 39 | To run tests, run: 40 | 41 | ``` 42 | npm run tests 43 | ``` 44 | 45 | To run an individual test, you can run Jest with the `-t` parameter, followed by the string handle for the test. The handle is declared in each test file. Example: 46 | 47 | ``` 48 | npx jest -t 'await-requires-async' 49 | ``` 50 | 51 | Jest has an issue with printing errors emitted from eslint rules [due to a bug](https://github.com/jestjs/jest/issues/10577). If you are seeing errors like `TypeError: Converting circular structure to JSON`, then run this instead: 52 | 53 | ``` 54 | npm run test-workaround 55 | ``` 56 | 57 | This enables the `--detect-open-handles` Jest option. Tests will run slower, but you'll see the real cause of the errors. 58 | 59 | ### Manual testing 60 | 61 | You may want to run a local version of this plugin against your own plugin code. 62 | 63 | First, clone this repo. Then add the following to your Figma plugin's `package.json`, replacing `/path/to/local/clone` with an actual filesystem path: 64 | 65 | ``` 66 | { 67 | ... 68 | "devDependencies": { 69 | "@figma/eslint-plugin-figma-plugins": "file:/path/to/local/clone", 70 | ... 71 | } 72 | } 73 | ``` 74 | 75 | #### Update node_modules 76 | 77 | Once you've updated your `package.json`, run `npm install` to pull down the latest changes. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Figma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-figma-plugins 2 | 3 | This repository defines [typescript-eslint](https://typescript-eslint.io/) rules for [Figma plugin development](https://www.figma.com/plugin-docs/). 4 | 5 | This tool helps you stay up to date with best practices and deprecations in the Figma Plugin API. You can use it to help identify, and in many cases automatically fix, issues in your plugin code. Like any ESLint plugin, it integrates with IDEs like VSCode to provide inline warnings and quick-fix functionality. 6 | 7 | ### A quick look 8 | 9 | ![An animation of VSCode quick fixes enabled by this plugin](./vscode-quickfix.gif) 10 | 11 | ## Installation 12 | 13 | ### Dependencies 14 | 15 | This linter requires TypeScript, ESLint, typescript-eslint, and the Figma Plugin API type definitions. To install all of these, run: 16 | 17 | ``` 18 | npm install -D typescript eslint@8 @typescript-eslint/parser@6 @typescript-eslint/eslint-plugin@6 @figma/plugin-typings 19 | ``` 20 | 21 | #### Notes on peer dependency versions 22 | 23 | - This plugin is not yet compatible with ESLint 9. Once [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint) has been [upgraded to support ESLint 9](https://github.com/typescript-eslint/typescript-eslint/pull/9002), we'll update this README with example configurations that use the new ESLint 9 flat configs. 24 | - This plugin has only been tested with typescript-eslint version 6. 25 | 26 | ### Install the ESLint plugin package 27 | 28 | ``` 29 | npm install -D @figma/eslint-plugin-figma-plugins 30 | ``` 31 | 32 | ### Configure eslint 33 | 34 | Configure typescript-eslint as normal using [these instructions](https://typescript-eslint.io/getting-started#step-1-installation). 35 | 36 | Next, update your ESLint config's `extends` array to include the `plugin:@figma/figma-plugins/recommended` ruleset. We also recommend the following rulesets: 37 | 38 | - `eslint:recommended`, 39 | - `plugin:@typescript-eslint/recommended` 40 | 41 | To work with TypeScript code, ESLint also requires the following parser settings: 42 | 43 | ``` 44 | { 45 | ... 46 | parser: '@typescript-eslint/parser', 47 | parserOptions: { 48 | project: './tsconfig.json', 49 | }, 50 | ... 51 | } 52 | ``` 53 | 54 | Here's a full example of `.eslintrc.js`: 55 | 56 | ``` 57 | /* eslint-env node */ 58 | module.exports = { 59 | extends: [ 60 | 'eslint:recommended', 61 | 'plugin:@typescript-eslint/recommended', 62 | 'plugin:@figma/figma-plugins/recommended', 63 | ], 64 | parser: '@typescript-eslint/parser', 65 | parserOptions: { 66 | project: './tsconfig.json', 67 | }, 68 | root: true 69 | } 70 | ``` 71 | 72 | ### Restart the ESLint server 73 | 74 | If you've run `npm install` and updated to a newer version of this package, remember to restart your IDE. In VSCode, you can restart the ESLint server independently by opening the command palette and choosing "Restart ESLint Server". 75 | 76 | ## Usage 77 | 78 | ### Linting and autofixing 79 | 80 | You can lint your project using these rules by running 81 | 82 | ``` 83 | npx eslint ./path/to/source 84 | ``` 85 | 86 | Some rules provide autofixes, which you can run using `--fix`. 87 | 88 | ``` 89 | npx eslint --fix ./path/to/source 90 | ``` 91 | 92 | Autofixes are also available via some IDEs. 93 | 94 | ### VSCode 95 | 96 | To use ESLint with VSCode, see the [ESLint VSCode extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint). This extension will show rule violations inline, as well as provide opportunities to run autofixes directly in the IDE. 97 | 98 | ## Rules 99 | 100 | 101 | 102 | 💼 Configurations enabled in.\ 103 | ⚠️ Configurations set to warn in.\ 104 | 👍 Set in the `recommended` configuration.\ 105 | 🔦 Set in the `recommended-problems-only` configuration.\ 106 | 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). 107 | 108 | | Name                                                         | Description | 💼 | ⚠️ | 🔧 | 109 | | :----------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------- | :---- | :- | :- | 110 | | [await-requires-async](docs/rules/await-requires-async.md) | Require functions that contain `await` to be `async` | 👍 🔦 | | 🔧 | 111 | | [ban-deprecated-id-params](docs/rules/ban-deprecated-id-params.md) | Ban use of deprecated string ID parameters | 👍 🔦 | | 🔧 | 112 | | [ban-deprecated-sync-methods](docs/rules/ban-deprecated-sync-methods.md) | Ban use of deprecated synchronous methods | 👍 🔦 | | 🔧 | 113 | | [ban-deprecated-sync-prop-getters](docs/rules/ban-deprecated-sync-prop-getters.md) | Ban use of deprecated synchronous property getters | 👍 🔦 | | 🔧 | 114 | | [ban-deprecated-sync-prop-setters](docs/rules/ban-deprecated-sync-prop-setters.md) | Ban use of deprecated synchronous property getters | 👍 🔦 | | 🔧 | 115 | | [constrain-proportions-replaced-by-target-aspect-ratio-advice](docs/rules/constrain-proportions-replaced-by-target-aspect-ratio-advice.md) | Warns against using constrainProportions in favor of targetAspectRatio | | 👍 | | 116 | | [dynamic-page-documentchange-event-advice](docs/rules/dynamic-page-documentchange-event-advice.md) | Advice on using the `documentchange` event | | 👍 | | 117 | | [dynamic-page-find-method-advice](docs/rules/dynamic-page-find-method-advice.md) | Advice on using the find*() family of methods | | 👍 | | 118 | 119 | 120 | 121 | ### Contributing 122 | 123 | Please see [CONTRIBUTING.md](./CONTRIBUTING.md) -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare const rules: unknown; 2 | export declare const configs: unknown; 3 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.configs = exports.rules = void 0; 4 | const awaitRequiresAsync_1 = require("./rules/awaitRequiresAsync"); 5 | const dynamicPageDocumentchangeEventAdvice_1 = require("./rules/dynamicPageDocumentchangeEventAdvice"); 6 | const banDeprecatedIdParams_1 = require("./rules/banDeprecatedIdParams"); 7 | const banDeprecatedSyncMethods_1 = require("./rules/banDeprecatedSyncMethods"); 8 | const banDeprecatedSyncPropGetters_1 = require("./rules/banDeprecatedSyncPropGetters"); 9 | const banDeprecatedSyncPropSetters_1 = require("./rules/banDeprecatedSyncPropSetters"); 10 | const dynamicPageFindMethodAdvice_1 = require("./rules/dynamicPageFindMethodAdvice"); 11 | const constrainProportionsReplacedByTargetAspectRatioAdvice_1 = require("./rules/constrainProportionsReplacedByTargetAspectRatioAdvice"); 12 | function rulesetWithSeverity(severity, rules) { 13 | return Object.keys(rules).reduce((acc, name) => { 14 | acc[`@figma/figma-plugins/${name}`] = severity; 15 | return acc; 16 | }, {}); 17 | } 18 | const errRules = { 19 | 'await-requires-async': awaitRequiresAsync_1.awaitRequiresAsync, 20 | 'ban-deprecated-id-params': banDeprecatedIdParams_1.banDeprecatedIdParams, 21 | 'ban-deprecated-sync-methods': banDeprecatedSyncMethods_1.banDeprecatedSyncMethods, 22 | 'ban-deprecated-sync-prop-getters': banDeprecatedSyncPropGetters_1.banDeprecatedSyncPropGetters, 23 | 'ban-deprecated-sync-prop-setters': banDeprecatedSyncPropSetters_1.banDeprecatedSyncPropSetters, 24 | }; 25 | const dynamicePageAdvice = { 26 | 'dynamic-page-documentchange-event-advice': dynamicPageDocumentchangeEventAdvice_1.dynamicPageDocumentchangeEventAdvice, 27 | 'dynamic-page-find-method-advice': dynamicPageFindMethodAdvice_1.dynamicPageFindMethodAdvice, 28 | }; 29 | const warnRules = Object.assign(Object.assign({}, dynamicePageAdvice), { 'constrain-proportions-replaced-by-target-aspect-ratio-advice': constrainProportionsReplacedByTargetAspectRatioAdvice_1.constrainProportionsReplacedByTargetAspectRatioAdvice }); 30 | // The exported type annotations in this file are somewhat arbitrary; we do NOT 31 | // expect anyone to actually consume these types. We include them because we use 32 | // @figma as a type root, and all packages under a type root must emit a type 33 | // declaration file. 34 | exports.rules = Object.assign(Object.assign({}, errRules), warnRules); 35 | exports.configs = { 36 | recommended: { 37 | plugins: ['@figma/figma-plugins'], 38 | rules: Object.assign(Object.assign({}, rulesetWithSeverity('error', errRules)), rulesetWithSeverity('warn', warnRules)), 39 | }, 40 | 'recommended-problems-only': { 41 | plugins: ['@figma/figma-plugins'], 42 | rules: Object.assign({}, rulesetWithSeverity('error', errRules)), 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /dist/rules/awaitRequiresAsync.d.ts: -------------------------------------------------------------------------------- 1 | import type { TSESLint } from '@typescript-eslint/utils'; 2 | /** 3 | * This rule requires that functions containing the `await` keyword be marked 4 | * `async`. It's quite a bit more generic than we want for this rule package, 5 | * and overlaps with a feature already present in the VSCode TypeScript 6 | * extension. Nevertheless, we offer it so that we can add `async` modifiers to 7 | * functions via a full-file autofix (e.g. eslint --fix). 8 | * 9 | * Note that this rule covers all cases where `await` is present without 10 | * `async`. Ideally, the fix in this rule would be restricted to cases where 11 | * another fix in this package creates an `await` inside of a function that is 12 | * not async. However, these two fixes cannot co-exist in the same eslint report; 13 | * adding an `async` modifier applies to the entire function, and is considered 14 | * "overlapping" with the fix that adds `await`. eslint reports do not permit 15 | * overlapping fixes. 16 | */ 17 | export declare const awaitRequiresAsync: TSESLint.RuleModule<"requiresAsync", never[], TSESLint.RuleListener>; 18 | -------------------------------------------------------------------------------- /dist/rules/awaitRequiresAsync.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.awaitRequiresAsync = void 0; 4 | const typescript_estree_1 = require("@typescript-eslint/typescript-estree"); 5 | const util_1 = require("../util"); 6 | /** 7 | * This rule requires that functions containing the `await` keyword be marked 8 | * `async`. It's quite a bit more generic than we want for this rule package, 9 | * and overlaps with a feature already present in the VSCode TypeScript 10 | * extension. Nevertheless, we offer it so that we can add `async` modifiers to 11 | * functions via a full-file autofix (e.g. eslint --fix). 12 | * 13 | * Note that this rule covers all cases where `await` is present without 14 | * `async`. Ideally, the fix in this rule would be restricted to cases where 15 | * another fix in this package creates an `await` inside of a function that is 16 | * not async. However, these two fixes cannot co-exist in the same eslint report; 17 | * adding an `async` modifier applies to the entire function, and is considered 18 | * "overlapping" with the fix that adds `await`. eslint reports do not permit 19 | * overlapping fixes. 20 | */ 21 | exports.awaitRequiresAsync = (0, util_1.createPluginRule)({ 22 | name: 'await-requires-async', 23 | meta: { 24 | docs: { 25 | description: 'Require functions that contain `await` to be `async`', 26 | }, 27 | fixable: 'code', 28 | messages: { 29 | requiresAsync: 'Functions containing the await keyword should be marked async.', 30 | }, 31 | schema: [], 32 | type: 'problem', 33 | }, 34 | defaultOptions: [], 35 | create(context) { 36 | return { 37 | ArrowFunctionExpression(node) { 38 | runRule(context, node); 39 | }, 40 | FunctionDeclaration(node) { 41 | runRule(context, node); 42 | }, 43 | FunctionExpression(node) { 44 | runRule(context, node); 45 | }, 46 | }; 47 | }, 48 | }); 49 | function containsAwait(containingNode) { 50 | let found = false; 51 | (0, util_1.traverseTree)(containingNode, (node) => { 52 | if (node.type === typescript_estree_1.AST_NODE_TYPES.AwaitExpression) { 53 | found = true; 54 | return util_1.TraverseTreeResult.Done; 55 | } 56 | // Ignore `await` in nested functions 57 | if (node.type === typescript_estree_1.AST_NODE_TYPES.ArrowFunctionExpression || 58 | node.type === typescript_estree_1.AST_NODE_TYPES.FunctionDeclaration || 59 | node.type === typescript_estree_1.AST_NODE_TYPES.FunctionExpression) { 60 | return util_1.TraverseTreeResult.SkipChildren; 61 | } 62 | return util_1.TraverseTreeResult.Continue; 63 | }); 64 | return found; 65 | } 66 | function runRule(context, funcNode) { 67 | if (funcNode.async) { 68 | return; 69 | } 70 | if (!containsAwait(funcNode.body)) { 71 | return; 72 | } 73 | context.report({ 74 | node: funcNode, 75 | messageId: 'requiresAsync', 76 | fix(fixer) { 77 | const src = context.sourceCode.getText(funcNode); 78 | return fixer.replaceText(funcNode, `async ${src}`); 79 | }, 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /dist/rules/banDeprecatedIdParams.d.ts: -------------------------------------------------------------------------------- 1 | import type { TSESLint as _ } from '@typescript-eslint/utils'; 2 | export declare const banDeprecatedIdParams: _.RuleModule<"useReplacement", never[], _.RuleListener>; 3 | -------------------------------------------------------------------------------- /dist/rules/banDeprecatedIdParams.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.banDeprecatedIdParams = void 0; 4 | const typescript_estree_1 = require("@typescript-eslint/typescript-estree"); 5 | const util_1 = require("../util"); 6 | const deprecatedIdParams = [ 7 | { 8 | receiverType: 'VariablesAPI', 9 | method: 'createVariable', 10 | paramIndex: 1, 11 | wantParamType: 'VariableCollection', 12 | asyncObjectFetch: 'figma.variables.getVariableCollectionByIdAsync', 13 | }, 14 | { 15 | receiverType: 'ExplicitVariableModesMixin', 16 | method: 'setExplicitVariableModeForCollection', 17 | paramIndex: 0, 18 | wantParamType: 'VariableCollection', 19 | asyncObjectFetch: 'figma.variables.getVariableCollectionByIdAsync', 20 | }, 21 | { 22 | receiverType: 'ExplicitVariableModesMixin', 23 | method: 'clearExplicitVariableModeForCollection', 24 | paramIndex: 0, 25 | wantParamType: 'VariableCollection', 26 | asyncObjectFetch: 'figma.variables.getVariableCollectionByIdAsync', 27 | }, 28 | { 29 | receiverType: 'SceneNodeMixin', 30 | method: 'setBoundVariable', 31 | paramIndex: 1, 32 | wantParamType: 'Variable', 33 | asyncObjectFetch: 'figma.variables.getVariableByIdAsync', 34 | }, 35 | ]; 36 | exports.banDeprecatedIdParams = (0, util_1.createPluginRule)({ 37 | name: 'ban-deprecated-id-params', 38 | meta: { 39 | docs: { 40 | description: 'Ban use of deprecated string ID parameters', 41 | }, 42 | fixable: 'code', 43 | messages: { 44 | useReplacement: 'Passing a string ID for parameter {{humanReadableParamIndex}} to {{receiverType}}.{{method}} is deprecated. Please pass a {{wantParamType}} instead.', 45 | }, 46 | schema: [], 47 | type: 'problem', 48 | }, 49 | defaultOptions: [], 50 | create(context) { 51 | return { 52 | CallExpression(node) { 53 | const callee = node.callee; 54 | if (callee.type !== typescript_estree_1.AST_NODE_TYPES.MemberExpression) { 55 | return; 56 | } 57 | const calleeProp = callee.property; 58 | if (calleeProp.type !== typescript_estree_1.AST_NODE_TYPES.Identifier) { 59 | return; 60 | } 61 | const deprecation = deprecatedIdParams.find((p) => p.method === calleeProp.name); 62 | if (!deprecation) { 63 | return; 64 | } 65 | const receiver = callee.object; 66 | const match = (0, util_1.matchAncestorTypes)(context, receiver, [deprecation.receiverType]); 67 | if (!match) { 68 | return; 69 | } 70 | const arg = node.arguments[deprecation.paramIndex]; 71 | if (!arg) { 72 | return; 73 | } 74 | if (!(0, util_1.isStringNode)(context, arg)) { 75 | return; 76 | } 77 | context.report({ 78 | node, 79 | messageId: 'useReplacement', 80 | data: { 81 | humanReadableParamIndex: deprecation.paramIndex + 1, 82 | receiverType: (0, util_1.getTypeName)(match.nodeType, match.matchedAncestorType), 83 | method: deprecation.method, 84 | wantParamType: deprecation.wantParamType, 85 | }, 86 | fix(fixer) { 87 | const argText = context.sourceCode.getText(arg); 88 | return fixer.replaceText(arg, `await ${deprecation.asyncObjectFetch}(${argText})`); 89 | }, 90 | }); 91 | }, 92 | }; 93 | }, 94 | }); 95 | -------------------------------------------------------------------------------- /dist/rules/banDeprecatedSyncMethods.d.ts: -------------------------------------------------------------------------------- 1 | import type { TSESLint as _ } from '@typescript-eslint/utils'; 2 | export declare const banDeprecatedSyncMethods: _.RuleModule<"useReplacement", never[], _.RuleListener>; 3 | -------------------------------------------------------------------------------- /dist/rules/banDeprecatedSyncMethods.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.banDeprecatedSyncMethods = void 0; 4 | const typescript_estree_1 = require("@typescript-eslint/typescript-estree"); 5 | const util_1 = require("../util"); 6 | const deprecatedSyncMethods = [ 7 | { 8 | method: 'getFileThumbnailNode', 9 | replacement: 'getFileThumbnailNodeAsync', 10 | receiverTypes: ['PluginAPI'], 11 | }, 12 | { 13 | method: 'getLocalTextStyles', 14 | replacement: 'getLocalTextStylesAsync', 15 | receiverTypes: ['PluginAPI'], 16 | }, 17 | { 18 | method: 'getLocalPaintStyles', 19 | replacement: 'getLocalPaintStylesAsync', 20 | receiverTypes: ['PluginAPI'], 21 | }, 22 | { 23 | method: 'getLocalEffectStyles', 24 | replacement: 'getLocalEffectStylesAsync', 25 | receiverTypes: ['PluginAPI'], 26 | }, 27 | { 28 | method: 'getLocalGridStyles', 29 | replacement: 'getLocalGridStylesAsync', 30 | receiverTypes: ['PluginAPI'], 31 | }, 32 | { 33 | method: 'getLocalVariableCollections', 34 | replacement: 'getLocalVariableCollectionsAsync', 35 | receiverTypes: ['VariablesAPI'], 36 | }, 37 | { 38 | method: 'getLocalVariables', 39 | replacement: 'getLocalVariablesAsync', 40 | receiverTypes: ['VariablesAPI'], 41 | }, 42 | { 43 | method: 'getNodeById', 44 | replacement: 'getNodeByIdAsync', 45 | receiverTypes: ['PluginAPI'], 46 | }, 47 | { 48 | method: 'getStyleById', 49 | replacement: 'getStyleByIdAsync', 50 | receiverTypes: ['PluginAPI'], 51 | }, 52 | { 53 | method: 'getVariableById', 54 | replacement: 'getVariableByIdAsync', 55 | receiverTypes: ['VariablesAPI'], 56 | }, 57 | { 58 | method: 'getVariableCollectionById', 59 | replacement: 'getVariableCollectionByIdAsync', 60 | receiverTypes: ['VariablesAPI'], 61 | }, 62 | { 63 | method: 'setRangeFillStyle', 64 | replacement: 'setRangeFillStyleIdAsync', 65 | receiverTypes: ['NonResizableTextMixin'], 66 | }, 67 | { 68 | method: 'setRangeTextStyle', 69 | replacement: 'setRangeTextStyleIdAsync', 70 | receiverTypes: ['NonResizableTextMixin'], 71 | }, 72 | ]; 73 | exports.banDeprecatedSyncMethods = (0, util_1.createPluginRule)({ 74 | name: 'ban-deprecated-sync-methods', 75 | meta: { 76 | docs: { 77 | description: 'Ban use of deprecated synchronous methods', 78 | }, 79 | fixable: 'code', 80 | messages: { 81 | useReplacement: '{{receiverType}}.{{method}} is deprecated. Please use {{replacement}} instead.', 82 | }, 83 | schema: [], 84 | type: 'problem', 85 | }, 86 | defaultOptions: [], 87 | create(context) { 88 | return { 89 | CallExpression(node) { 90 | const callee = node.callee; 91 | if (callee.type !== typescript_estree_1.AST_NODE_TYPES.MemberExpression) { 92 | return; 93 | } 94 | const calleeProp = callee.property; 95 | if (calleeProp.type !== typescript_estree_1.AST_NODE_TYPES.Identifier) { 96 | return; 97 | } 98 | const deprecation = deprecatedSyncMethods.find((m) => m.method === calleeProp.name); 99 | if (!deprecation) { 100 | return; 101 | } 102 | const receiver = callee.object; 103 | const match = (0, util_1.matchAncestorTypes)(context, receiver, deprecation.receiverTypes); 104 | if (!match) { 105 | return; 106 | } 107 | context.report({ 108 | node, 109 | messageId: 'useReplacement', 110 | data: { 111 | receiverType: (0, util_1.getTypeName)(match.nodeType, match.matchedAncestorType), 112 | method: deprecation.method, 113 | replacement: deprecation.replacement, 114 | }, 115 | fix(fixer) { 116 | return (0, util_1.addAsyncCallFix)({ 117 | context, 118 | fixer, 119 | expression: node, 120 | receiver: receiver, 121 | asyncIdentifier: deprecation.replacement, 122 | args: node.arguments, 123 | }); 124 | }, 125 | }); 126 | }, 127 | }; 128 | }, 129 | }); 130 | -------------------------------------------------------------------------------- /dist/rules/banDeprecatedSyncPropGetters.d.ts: -------------------------------------------------------------------------------- 1 | import type { TSESLint as _ } from '@typescript-eslint/utils'; 2 | export declare const banDeprecatedSyncPropGetters: _.RuleModule<"useReplacement", never[], _.RuleListener>; 3 | -------------------------------------------------------------------------------- /dist/rules/banDeprecatedSyncPropGetters.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.banDeprecatedSyncPropGetters = void 0; 4 | const typescript_estree_1 = require("@typescript-eslint/typescript-estree"); 5 | const util_1 = require("../util"); 6 | const deprecatedSyncPropGetters = [ 7 | { 8 | property: 'instances', 9 | replacement: 'getInstancesAsync', 10 | receiverTypes: ['ComponentNode'], 11 | }, 12 | { 13 | property: 'consumers', 14 | replacement: 'getConsumersAsync', 15 | receiverTypes: ['BaseStyle'], 16 | }, 17 | { 18 | property: 'mainComponent', 19 | replacement: 'getMainComponentAsync', 20 | receiverTypes: ['InstanceNode'], 21 | }, 22 | ]; 23 | exports.banDeprecatedSyncPropGetters = (0, util_1.createPluginRule)({ 24 | name: 'ban-deprecated-sync-prop-getters', 25 | meta: { 26 | docs: { 27 | description: 'Ban use of deprecated synchronous property getters', 28 | }, 29 | fixable: 'code', 30 | messages: { 31 | useReplacement: 'Reading from {{receiverType}}.{{property}} is deprecated. Please use {{replacement}} instead.', 32 | }, 33 | schema: [], 34 | type: 'problem', 35 | }, 36 | defaultOptions: [], 37 | create(context) { 38 | return { 39 | MemberExpression(node) { 40 | // allow the expression to be used in an assignment 41 | const parent = node.parent; 42 | if (parent && parent.type === typescript_estree_1.AST_NODE_TYPES.AssignmentExpression && parent.left === node) { 43 | return; 44 | } 45 | const prop = node.property; 46 | if (prop.type !== typescript_estree_1.AST_NODE_TYPES.Identifier) { 47 | return; 48 | } 49 | const deprecation = deprecatedSyncPropGetters.find((g) => g.property === prop.name); 50 | if (!deprecation) { 51 | return; 52 | } 53 | const receiver = node.object; 54 | const match = (0, util_1.matchAncestorTypes)(context, receiver, deprecation.receiverTypes); 55 | if (!match) { 56 | return; 57 | } 58 | context.report({ 59 | node, 60 | messageId: 'useReplacement', 61 | data: { 62 | receiverType: (0, util_1.getTypeName)(match.nodeType, match.matchedAncestorType), 63 | property: deprecation.property, 64 | replacement: deprecation.replacement, 65 | }, 66 | fix(fixer) { 67 | return (0, util_1.addAsyncCallFix)({ 68 | context, 69 | fixer, 70 | expression: node, 71 | receiver, 72 | asyncIdentifier: deprecation.replacement, 73 | args: [], 74 | }); 75 | }, 76 | }); 77 | }, 78 | }; 79 | }, 80 | }); 81 | -------------------------------------------------------------------------------- /dist/rules/banDeprecatedSyncPropSetters.d.ts: -------------------------------------------------------------------------------- 1 | import type { TSESLint as _ } from '@typescript-eslint/utils'; 2 | export declare const banDeprecatedSyncPropSetters: _.RuleModule<"useReplacement", never[], _.RuleListener>; 3 | -------------------------------------------------------------------------------- /dist/rules/banDeprecatedSyncPropSetters.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.banDeprecatedSyncPropSetters = void 0; 4 | const typescript_estree_1 = require("@typescript-eslint/typescript-estree"); 5 | const util_1 = require("../util"); 6 | const DeprecatedSyncPropSetters = [ 7 | { 8 | property: 'currentPage', 9 | replacement: 'setCurrentPageAsync', 10 | receiverTypes: ['PluginAPI'], 11 | }, 12 | { 13 | property: 'effectStyleId', 14 | replacement: 'setEffectStyleIdAsync', 15 | receiverTypes: ['BlendMixin'], 16 | }, 17 | { 18 | property: 'fillStyleId', 19 | replacement: 'setFillStyleIdAsync', 20 | receiverTypes: ['MinimalFillsMixin'], 21 | }, 22 | { 23 | property: 'gridStyleId', 24 | replacement: 'setGridStyleIdAsync', 25 | receiverTypes: ['BaseFrameMixin'], 26 | }, 27 | { 28 | property: 'strokeStyleId', 29 | replacement: 'setStrokeStyleIdAsync', 30 | receiverTypes: ['MinimalStrokesMixin'], 31 | }, 32 | { 33 | property: 'textStyleId', 34 | replacement: 'setTextStyleIdAsync', 35 | receiverTypes: ['TextNode'], 36 | }, 37 | { 38 | property: 'backgroundStyleId', 39 | replacement: 'setFillStyleIdAsync', 40 | receiverTypes: ['DeprecatedBackgroundMixin'], 41 | }, 42 | { 43 | property: 'vectorNetwork', 44 | replacement: 'setVectorNetworkAsync', 45 | receiverTypes: ['VectorLikeMixin'], 46 | }, 47 | { 48 | property: 'reactions', 49 | replacement: 'setReactionsAsync', 50 | receiverTypes: ['ReactionMixin'], 51 | }, 52 | ]; 53 | exports.banDeprecatedSyncPropSetters = (0, util_1.createPluginRule)({ 54 | name: 'ban-deprecated-sync-prop-setters', 55 | meta: { 56 | docs: { 57 | description: 'Ban use of deprecated synchronous property getters', 58 | }, 59 | fixable: 'code', 60 | messages: { 61 | useReplacement: 'Assigning to {{receiverType}}.{{property}} is deprecated. Please use {{replacement}} instead.', 62 | }, 63 | schema: [], 64 | type: 'problem', 65 | }, 66 | defaultOptions: [], 67 | create(context) { 68 | return { 69 | AssignmentExpression(node) { 70 | if (node.left.type !== typescript_estree_1.AST_NODE_TYPES.MemberExpression) { 71 | return; 72 | } 73 | const prop = node.left.property; 74 | if (prop.type !== typescript_estree_1.AST_NODE_TYPES.Identifier) { 75 | return; 76 | } 77 | const deprecation = DeprecatedSyncPropSetters.find((s) => s.property === prop.name); 78 | if (!deprecation) { 79 | return; 80 | } 81 | const receiver = node.left.object; 82 | const match = (0, util_1.matchAncestorTypes)(context, receiver, deprecation.receiverTypes); 83 | if (!match) { 84 | return; 85 | } 86 | context.report({ 87 | node, 88 | messageId: 'useReplacement', 89 | data: { 90 | receiverType: (0, util_1.getTypeName)(match.nodeType, match.matchedAncestorType), 91 | property: deprecation.property, 92 | replacement: deprecation.replacement, 93 | }, 94 | fix(fixer) { 95 | return (0, util_1.addAsyncCallFix)({ 96 | context, 97 | fixer, 98 | expression: node, 99 | receiver, 100 | asyncIdentifier: deprecation.replacement, 101 | args: [node.right], 102 | }); 103 | }, 104 | }); 105 | }, 106 | }; 107 | }, 108 | }); 109 | -------------------------------------------------------------------------------- /dist/rules/constrainProportionsReplacedByTargetAspectRatioAdvice.d.ts: -------------------------------------------------------------------------------- 1 | import type { TSESLint as _ } from '@typescript-eslint/utils'; 2 | export declare const constrainProportionsReplacedByTargetAspectRatioAdvice: _.RuleModule<"readAdvice" | "writeAdvice", never[], _.RuleListener>; 3 | -------------------------------------------------------------------------------- /dist/rules/constrainProportionsReplacedByTargetAspectRatioAdvice.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.constrainProportionsReplacedByTargetAspectRatioAdvice = void 0; 4 | const typescript_estree_1 = require("@typescript-eslint/typescript-estree"); 5 | const util_1 = require("../util"); 6 | exports.constrainProportionsReplacedByTargetAspectRatioAdvice = (0, util_1.createPluginRule)({ 7 | name: 'constrain-proportions-replaced-by-target-aspect-ratio-advice', 8 | meta: { 9 | docs: { 10 | description: 'Warns against using constrainProportions in favor of targetAspectRatio', 11 | }, 12 | messages: { 13 | readAdvice: 'Please use targetAspectRatio instead of constrainProportions for determining if a node will resize proportinally.', 14 | writeAdvice: 'Please use lockAspectRatio() or unlockAspectRatio() instead of setting constrainProportions.', 15 | }, 16 | schema: [], 17 | type: 'suggestion', 18 | }, 19 | defaultOptions: [], 20 | create(context) { 21 | return { 22 | MemberExpression(node) { 23 | const property = node.property; 24 | if (property.type === typescript_estree_1.AST_NODE_TYPES.Identifier && 25 | property.name === 'constrainProportions') { 26 | // Check if the receiver is a LayoutMixin, since that's what constrainProportions lives on 27 | const match = (0, util_1.matchAncestorTypes)(context, node.object, ['LayoutMixin']); 28 | if (!match) { 29 | return; 30 | } 31 | // Check if it's being read or written to 32 | const parent = node.parent; 33 | if ((parent === null || parent === void 0 ? void 0 : parent.type) === typescript_estree_1.AST_NODE_TYPES.AssignmentExpression && 34 | parent.left === node) { 35 | // It's being written to 36 | context.report({ 37 | node, 38 | messageId: 'writeAdvice', 39 | }); 40 | } 41 | else { 42 | // It's being read 43 | context.report({ 44 | node, 45 | messageId: 'readAdvice', 46 | }); 47 | } 48 | } 49 | }, 50 | }; 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /dist/rules/dynamicPageDocumentchangeEventAdvice.d.ts: -------------------------------------------------------------------------------- 1 | import type { TSESLint as _ } from '@typescript-eslint/utils'; 2 | export declare const dynamicPageDocumentchangeEventAdvice: _.RuleModule<"advice", never[], _.RuleListener>; 3 | -------------------------------------------------------------------------------- /dist/rules/dynamicPageDocumentchangeEventAdvice.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.dynamicPageDocumentchangeEventAdvice = void 0; 4 | const typescript_estree_1 = require("@typescript-eslint/typescript-estree"); 5 | const util_1 = require("../util"); 6 | exports.dynamicPageDocumentchangeEventAdvice = (0, util_1.createPluginRule)({ 7 | name: 'dynamic-page-documentchange-event-advice', 8 | meta: { 9 | docs: { 10 | description: 'Advice on using the `documentchange` event', 11 | }, 12 | messages: { 13 | advice: `When using the dynamic-page manifest field, remember to call figma.loadAllPagesAsync() before using DocumentNode.{{method}}(). loadAllPagesAsync() only needs to be called once.`, 14 | }, 15 | schema: [], 16 | type: 'suggestion', 17 | }, 18 | defaultOptions: [], 19 | create(context) { 20 | return { 21 | CallExpression(node) { 22 | // Check that we're calling one of on(), once(), or off() 23 | const callee = node.callee; 24 | if (callee.type !== typescript_estree_1.AST_NODE_TYPES.MemberExpression) { 25 | return; 26 | } 27 | const calleeProp = callee.property; 28 | if (calleeProp.type !== typescript_estree_1.AST_NODE_TYPES.Identifier) { 29 | return; 30 | } 31 | if (calleeProp.name !== 'on' && calleeProp.name !== 'once' && calleeProp.name !== 'off') { 32 | return; 33 | } 34 | // Check that the first argument is 'documentchange' 35 | const args = node.arguments; 36 | if (args.length < 1) { 37 | return; 38 | } 39 | const eventName = args[0]; 40 | if (eventName.type !== typescript_estree_1.AST_NODE_TYPES.Literal) { 41 | return; 42 | } 43 | if (eventName.value !== 'documentchange') { 44 | return; 45 | } 46 | // Ensure that we're calling the event handler method on a PluginAPI instance 47 | if (!(0, util_1.matchAncestorTypes)(context, callee.object, ['PluginAPI'])) { 48 | return; 49 | } 50 | context.report({ node, messageId: 'advice' }); 51 | }, 52 | }; 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /dist/rules/dynamicPageFindMethodAdvice.d.ts: -------------------------------------------------------------------------------- 1 | import type { TSESLint as _ } from '@typescript-eslint/utils'; 2 | export declare const dynamicPageFindMethodAdvice: _.RuleModule<"advice", never[], _.RuleListener>; 3 | -------------------------------------------------------------------------------- /dist/rules/dynamicPageFindMethodAdvice.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.dynamicPageFindMethodAdvice = void 0; 4 | const typescript_estree_1 = require("@typescript-eslint/typescript-estree"); 5 | const util_1 = require("../util"); 6 | const findMethods = ['findAll', 'findAllWithCriteria', 'findOne']; 7 | exports.dynamicPageFindMethodAdvice = (0, util_1.createPluginRule)({ 8 | name: 'dynamic-page-find-method-advice', 9 | meta: { 10 | docs: { 11 | description: 'Advice on using the find*() family of methods', 12 | }, 13 | messages: { 14 | advice: 'When using the dynamic-page manifest field, remember to call figma.loadAllPagesAsync() before using DocumentNode.{{method}}(). loadAllPagesAsync() only needs to be called once.', 15 | }, 16 | schema: [], 17 | type: 'suggestion', 18 | }, 19 | defaultOptions: [], 20 | create(context) { 21 | return { 22 | CallExpression(node) { 23 | const callee = node.callee; 24 | if (callee.type !== typescript_estree_1.AST_NODE_TYPES.MemberExpression) { 25 | return; 26 | } 27 | const calleeProp = callee.property; 28 | if (calleeProp.type !== typescript_estree_1.AST_NODE_TYPES.Identifier) { 29 | return; 30 | } 31 | if (!findMethods.includes(calleeProp.name)) { 32 | return; 33 | } 34 | const receiver = callee.object; 35 | const match = (0, util_1.matchAncestorTypes)(context, receiver, ['DocumentNode']); 36 | if (!match) { 37 | return; 38 | } 39 | context.report({ 40 | node, 41 | messageId: 'advice', 42 | data: { method: calleeProp.name }, 43 | }); 44 | }, 45 | }; 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /dist/util.d.ts: -------------------------------------------------------------------------------- 1 | import { ESLintUtils, TSESLint, TSESTree } from '@typescript-eslint/utils'; 2 | import ts from 'typescript'; 3 | export declare const createPluginRule: ({ name, meta, ...rule }: Readonly>) => ESLintUtils.RuleModule; 4 | export declare function addAsyncCallFix({ context, fixer, expression, receiver, asyncIdentifier, args, argsPostProcessor, }: { 5 | context: TSESLint.RuleContext; 6 | fixer: TSESLint.RuleFixer; 7 | expression: TSESTree.Node; 8 | receiver: TSESTree.Node; 9 | asyncIdentifier: string; 10 | args: TSESTree.Node[]; 11 | argsPostProcessor?: (s: string, index: number) => string; 12 | }): TSESLint.RuleFix; 13 | export interface MatchAncestorTypeResult { 14 | nodeType: ts.Type; 15 | matchedAncestorType: string; 16 | } 17 | export declare function matchAncestorTypes(context: TSESLint.RuleContext, node: TSESTree.Node, ancestorTypes: string[]): MatchAncestorTypeResult | undefined; 18 | export declare enum TraverseTreeResult { 19 | Continue = 0, 20 | SkipChildren = 1, 21 | Done = 2 22 | } 23 | /** 24 | * Traverse a TSESTree.Node tree in depth-first order. The visitor function can 25 | * indicate whether to continue traversing the node's children, skip the node's 26 | * children, or stop traversing altogether. 27 | */ 28 | export declare function traverseTree(root: TSESTree.Node, visitor: (node: TSESTree.Node) => TraverseTreeResult): void; 29 | /** 30 | * When running these rules from tests, sometimes a TypeScript Type object's 31 | * symbol property is undefined, contrary to the type declaration. This seems to 32 | * happen when an expression has a named type, but the type does not to resolve 33 | * to anything that the typechecker knows about. 34 | * 35 | * The discrepancy between the compiler API and its type definitions may be due 36 | * to this bug: https://github.com/microsoft/TypeScript/issues/13165 37 | * 38 | * As a workaround, we use two fallbacks, in order of priority: 39 | * - aliasSymbol.escapedName 40 | * - the fallback argument, which should be the type we searched for in 41 | * matchAncestorTypes() 42 | */ 43 | export declare function getTypeName(t: ts.Type, fallback: string): string; 44 | export declare function isStringNode(context: TSESLint.RuleContext, node: TSESTree.Node): boolean; 45 | -------------------------------------------------------------------------------- /dist/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.isStringNode = exports.getTypeName = exports.traverseTree = exports.TraverseTreeResult = exports.matchAncestorTypes = exports.addAsyncCallFix = exports.createPluginRule = void 0; 7 | const utils_1 = require("@typescript-eslint/utils"); 8 | const typescript_1 = __importDefault(require("typescript")); 9 | exports.createPluginRule = utils_1.ESLintUtils.RuleCreator((name) => `https://github.com/figma/eslint-plugin-figma-plugins/blob/main/docs/rules/${name}.md`); 10 | function mapIdentity(val, _index) { 11 | return val; 12 | } 13 | function addAsyncCallFix({ context, fixer, expression, receiver, asyncIdentifier, args, argsPostProcessor, }) { 14 | const doParens = receiver.type !== utils_1.AST_NODE_TYPES.Identifier && 15 | receiver.type !== utils_1.AST_NODE_TYPES.MemberExpression && 16 | receiver.type !== utils_1.AST_NODE_TYPES.CallExpression; 17 | let rcvSrc = context.sourceCode.getText(receiver); 18 | rcvSrc = doParens ? `(${rcvSrc})` : rcvSrc; 19 | const paramsSrc = args 20 | .map((a) => context.sourceCode.getText(a)) 21 | .map(argsPostProcessor !== null && argsPostProcessor !== void 0 ? argsPostProcessor : mapIdentity) 22 | .join(', '); 23 | return fixer.replaceText(expression, `await ${rcvSrc}.${asyncIdentifier}(${paramsSrc})`); 24 | } 25 | exports.addAsyncCallFix = addAsyncCallFix; 26 | function matchAncestorTypes(context, node, ancestorTypes) { 27 | const type = utils_1.ESLintUtils.getParserServices(context).getTypeAtLocation(node); 28 | const match = ancestorTypes.find((name) => composedOfTypeWithName(type, name)); 29 | return match ? { nodeType: type, matchedAncestorType: match } : undefined; 30 | } 31 | exports.matchAncestorTypes = matchAncestorTypes; 32 | var TraverseTreeResult; 33 | (function (TraverseTreeResult) { 34 | TraverseTreeResult[TraverseTreeResult["Continue"] = 0] = "Continue"; 35 | TraverseTreeResult[TraverseTreeResult["SkipChildren"] = 1] = "SkipChildren"; 36 | TraverseTreeResult[TraverseTreeResult["Done"] = 2] = "Done"; 37 | })(TraverseTreeResult || (exports.TraverseTreeResult = TraverseTreeResult = {})); 38 | /** 39 | * Traverse a TSESTree.Node tree in depth-first order. The visitor function can 40 | * indicate whether to continue traversing the node's children, skip the node's 41 | * children, or stop traversing altogether. 42 | */ 43 | function traverseTree(root, visitor) { 44 | traverseTreeRecursive(root, visitor); 45 | } 46 | exports.traverseTree = traverseTree; 47 | function traverseTreeRecursive(node, visitor) { 48 | // This algorithm is provided by: 49 | // github.com/typescript-eslint/typescript-eslint/blob/705370ac0d9c54081657b8855b398e57d6ea4ddb/packages/typescript-estree/src/simple-traverse.ts 50 | const result = visitor(node); 51 | if (result === TraverseTreeResult.Done) { 52 | return TraverseTreeResult.Done; 53 | } 54 | if (result === TraverseTreeResult.SkipChildren) { 55 | return; 56 | } 57 | for (const [k, childOrChildren] of Object.entries(node)) { 58 | // Avoid cycles. Ideally, we could restrict this to an even narrower set of 59 | // keys, but it's a lot of work to inventory all possible keys containing 60 | // child nodes, and it wouldn't be future-proof. 61 | if (k === 'parent') { 62 | continue; 63 | } 64 | if (isValidNode(childOrChildren)) { 65 | if (traverseTreeRecursive(childOrChildren, visitor) === TraverseTreeResult.Done) { 66 | return TraverseTreeResult.Done; 67 | } 68 | } 69 | else if (Array.isArray(childOrChildren)) { 70 | for (const child of childOrChildren) { 71 | if (!isValidNode(child)) { 72 | // We're not in an array of children, so let's just skip this key 73 | break; 74 | } 75 | if (traverseTreeRecursive(child, visitor) === TraverseTreeResult.Done) { 76 | return TraverseTreeResult.Done; 77 | } 78 | } 79 | } 80 | } 81 | } 82 | /** 83 | * This is a duck-type test to determine whether a value is a TSESTree.Node. It 84 | * is not particularly bulletproof, and I'd suggest not using it unless you can 85 | * guarantee that the input value is either a node or comes from a node. 86 | */ 87 | function isValidNode(x) { 88 | return typeof x === 'object' && x != null && 'type' in x && typeof x.type === 'string'; 89 | } 90 | function composedOfTypeWithName(t, typeName) { 91 | if (t.symbol && t.symbol.name === typeName) { 92 | return true; 93 | } 94 | if (t.aliasSymbol && t.aliasSymbol.name === typeName) { 95 | return true; 96 | } 97 | if (t.isUnion()) { 98 | return t.types.some((t) => composedOfTypeWithName(t, typeName)); 99 | } 100 | if (t.isIntersection()) { 101 | return t.types.some((t) => composedOfTypeWithName(t, typeName)); 102 | } 103 | const baseTypes = t.getBaseTypes(); 104 | if (baseTypes) { 105 | return baseTypes.some((t) => composedOfTypeWithName(t, typeName)); 106 | } 107 | return false; 108 | } 109 | /** 110 | * When running these rules from tests, sometimes a TypeScript Type object's 111 | * symbol property is undefined, contrary to the type declaration. This seems to 112 | * happen when an expression has a named type, but the type does not to resolve 113 | * to anything that the typechecker knows about. 114 | * 115 | * The discrepancy between the compiler API and its type definitions may be due 116 | * to this bug: https://github.com/microsoft/TypeScript/issues/13165 117 | * 118 | * As a workaround, we use two fallbacks, in order of priority: 119 | * - aliasSymbol.escapedName 120 | * - the fallback argument, which should be the type we searched for in 121 | * matchAncestorTypes() 122 | */ 123 | function getTypeName(t, fallback) { 124 | var _a, _b, _c, _d; 125 | return (_d = (_b = (_a = t.symbol) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : (_c = t.aliasSymbol) === null || _c === void 0 ? void 0 : _c.escapedName) !== null && _d !== void 0 ? _d : fallback; 126 | } 127 | exports.getTypeName = getTypeName; 128 | function isStringNode(context, node) { 129 | const type = utils_1.ESLintUtils.getParserServices(context).getTypeAtLocation(node); 130 | return !!(type.flags & typescript_1.default.TypeFlags.StringLike); 131 | } 132 | exports.isStringNode = isStringNode; 133 | -------------------------------------------------------------------------------- /docs/rules/await-requires-async.md: -------------------------------------------------------------------------------- 1 | # Require functions that contain `await` to be `async` (`@figma/figma-plugins/await-requires-async`) 2 | 3 | 💼 This rule is enabled in the following configs: 👍 `recommended`, 🔦 `recommended-problems-only`. 4 | 5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). 6 | 7 | 8 | 9 | This rule requires that functions containing the `await` keyword be marked 10 | `async`. It's quite a bit more generic than we want for this rule package, and 11 | overlaps with a feature already present in the VSCode TypeScript extension. 12 | Nevertheless, we offer it so that we can add `async` modifiers to functions via 13 | a full-file autofix (e.g. eslint --fix). 14 | 15 | Note that this rule covers all cases where `await` is present without `async`. 16 | Ideally, the fix in this rule would be restricted to cases where another fix in 17 | this package creates an `await` inside of a function that is not async. However, 18 | these two fixes cannot co-exist in the same eslint report; adding an `async` 19 | modifier applies to the entire function, and is considered "overlapping" with 20 | the fix that adds `await`. eslint reports do not permit overlapping fixes. 21 | -------------------------------------------------------------------------------- /docs/rules/ban-deprecated-id-params.md: -------------------------------------------------------------------------------- 1 | # Ban use of deprecated string ID parameters (`@figma/figma-plugins/ban-deprecated-id-params`) 2 | 3 | 💼 This rule is enabled in the following configs: 👍 `recommended`, 🔦 `recommended-problems-only`. 4 | 5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). 6 | 7 | 8 | 9 | For some methods, passing a string object identifier as an argument has been 10 | deprecated. In these cases, you can pass an instance of the relevant object 11 | instead. 12 | 13 | This rule provides a fix that fetches the relevant object by ID using an async 14 | fetch function. 15 | 16 | Note that the fix may produce an expression that doesn't fully satisfy the 17 | typechecker. For example, the fix will transform this: 18 | 19 | ``` 20 | node.clearExplicitVariableModeForCollection("foo"); 21 | ``` 22 | 23 | into this: 24 | 25 | ``` 26 | node.clearExplicitVariableModeForCollection( 27 | await figma.variables.getVariableCollectionByIdAsync("foo") 28 | ); 29 | ``` 30 | 31 | The type of the argument for `clearExplicitVariableModeForCollection` is 32 | `VariableCollection`, whereas the type of the `await` expression is 33 | `VariableCollection | null`. In other words, `getVariableCollectionByIdAsync` 34 | can return null. 35 | 36 | You can handle this situation in one of two ways. Ideally, you should check to 37 | see if the variable collection is null, and handle that case explicitly, such 38 | as: 39 | 40 | ``` 41 | const collection = await figma.variables.getVariableCollectionByIdAsync("foo") 42 | if (collection === null) { 43 | // log an error, show a message to the user, etc. 44 | return; 45 | } 46 | 47 | node.clearExplicitVariableModeForCollection(collection) 48 | ``` 49 | 50 | A quick-and-dirty workaround is to silence the typechecker using the `!` operator: 51 | 52 | ``` 53 | node.clearExplicitVariableModeForCollection( 54 | (await figma.variables.getVariableCollectionByIdAsync("foo"))! 55 | ); 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/rules/ban-deprecated-sync-methods.md: -------------------------------------------------------------------------------- 1 | # Ban use of deprecated synchronous methods (`@figma/figma-plugins/ban-deprecated-sync-methods`) 2 | 3 | 💼 This rule is enabled in the following configs: 👍 `recommended`, 🔦 `recommended-problems-only`. 4 | 5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). 6 | 7 | 8 | 9 | Some synchronous methods are deprecated in favor of `async` alternatives. This 10 | rule provides a fix to automatically convert callsites into their `async` 11 | equivalents. 12 | -------------------------------------------------------------------------------- /docs/rules/ban-deprecated-sync-prop-getters.md: -------------------------------------------------------------------------------- 1 | # Ban use of deprecated synchronous property getters (`@figma/figma-plugins/ban-deprecated-sync-prop-getters`) 2 | 3 | 💼 This rule is enabled in the following configs: 👍 `recommended`, 🔦 `recommended-problems-only`. 4 | 5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). 6 | 7 | 8 | 9 | Reading certain object properties synchronously has been deprecated. For each of 10 | these, the API contains an `async` getter method that should be used instead. 11 | This rule provides a fix that automatically converts read accesses of these 12 | properties into their `async` equivalents. 13 | -------------------------------------------------------------------------------- /docs/rules/ban-deprecated-sync-prop-setters.md: -------------------------------------------------------------------------------- 1 | # Ban use of deprecated synchronous property getters (`@figma/figma-plugins/ban-deprecated-sync-prop-setters`) 2 | 3 | 💼 This rule is enabled in the following configs: 👍 `recommended`, 🔦 `recommended-problems-only`. 4 | 5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). 6 | 7 | 8 | 9 | Assigning to certain object properties synchronously has been deprecated. For 10 | each of these, the API contains an `async` setter method that should be used 11 | instead. This rule provides a fix that automatically converts writes to these 12 | properties into their `async` equivalents. 13 | -------------------------------------------------------------------------------- /docs/rules/constrain-proportions-replaced-by-target-aspect-ratio-advice.md: -------------------------------------------------------------------------------- 1 | # Warns against using constrainProportions in favor of targetAspectRatio (`@figma/figma-plugins/constrain-proportions-replaced-by-target-aspect-ratio-advice`) 2 | 3 | ⚠️ This rule _warns_ in the 👍 `recommended` config. 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/rules/dynamic-page-documentchange-event-advice.md: -------------------------------------------------------------------------------- 1 | # Advice on using the `documentchange` event (`@figma/figma-plugins/dynamic-page-documentchange-event-advice`) 2 | 3 | ⚠️ This rule _warns_ in the 👍 `recommended` config. 4 | 5 | 6 | 7 | The `documentchange` event is not compatible with the `dynamic-page` manifest option. Please use `PageNode.on('nodechange')` or `figma.on('stylechange')` instead. 8 | -------------------------------------------------------------------------------- /docs/rules/dynamic-page-find-method-advice.md: -------------------------------------------------------------------------------- 1 | # Advice on using the find*() family of methods (`@figma/figma-plugins/dynamic-page-find-method-advice`) 2 | 3 | ⚠️ This rule _warns_ in the 👍 `recommended` config. 4 | 5 | 6 | 7 | The Figma API contains several methods, such as `DocumentNode.findAll()`, that 8 | require special handling when used with the `dynamic-page` manifest field. 9 | Before using any of these methods, your plugin code should include an `await`-ed 10 | call to `figma.loadPagesAsync()`. This call only needs to happen once per plugin 11 | run, so we recommend performing it somewhere in your plugin's setup code. 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@figma/eslint-plugin-figma-plugins", 3 | "version": "0.16.1", 4 | "description": "typescript-eslint rules for Figma plugin development", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "npm run clean && tsc -p tsconfig.build.json && npm run update:eslint-docs", 9 | "watch": "tsc -p tsconfig.build.json -w", 10 | "clean": "rm -rf dist", 11 | "test": "jest test/", 12 | "test-workaround": "jest --detect-open-handles test/", 13 | "lint": "eslint .", 14 | "lint:docs": "npm run update:eslint-docs -- --check", 15 | "typecheck": "tsc --noEmit", 16 | "update:eslint-docs": "eslint-doc-generator" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/figma/eslint-plugin-figma-plugins.git" 21 | }, 22 | "author": "", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/figma/eslint-plugin-figma-plugins/issues" 26 | }, 27 | "homepage": "https://github.com/figma/eslint-plugin-figma-plugins#readme", 28 | "files": [ 29 | "dist/", 30 | "CHANGELOG.md", 31 | "CONTRIBUTING.md", 32 | "LICENSE", 33 | "README.md" 34 | ], 35 | "dependencies": { 36 | "@typescript-eslint/typescript-estree": "^6.13.2", 37 | "@typescript-eslint/utils": "^6.12.0", 38 | "typescript": "^5.3.2" 39 | }, 40 | "devDependencies": { 41 | "@tsconfig/recommended": "^1.0.3", 42 | "@types/jest": "^29.5.11", 43 | "@types/node": "^20.9.4", 44 | "@typescript-eslint/eslint-plugin": "^6.12.0", 45 | "@typescript-eslint/parser": "^6.12.0", 46 | "@typescript-eslint/rule-tester": "^6.13.2", 47 | "eslint": "^8.54.0", 48 | "eslint-doc-generator": "^1.6.1", 49 | "jest": "^29.7.0", 50 | "ts-jest": "^29.1.1", 51 | "tsx": "^4.6.2" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { awaitRequiresAsync } from './rules/awaitRequiresAsync' 2 | import { dynamicPageDocumentchangeEventAdvice } from './rules/dynamicPageDocumentchangeEventAdvice' 3 | import { banDeprecatedIdParams } from './rules/banDeprecatedIdParams' 4 | import { banDeprecatedSyncMethods } from './rules/banDeprecatedSyncMethods' 5 | import { banDeprecatedSyncPropGetters } from './rules/banDeprecatedSyncPropGetters' 6 | import { banDeprecatedSyncPropSetters } from './rules/banDeprecatedSyncPropSetters' 7 | import { dynamicPageFindMethodAdvice } from './rules/dynamicPageFindMethodAdvice' 8 | import { constrainProportionsReplacedByTargetAspectRatioAdvice } from './rules/constrainProportionsReplacedByTargetAspectRatioAdvice' 9 | 10 | function rulesetWithSeverity( 11 | severity: 'error' | 'warn', 12 | rules: Record, 13 | ): Record { 14 | return Object.keys(rules).reduce((acc, name) => { 15 | acc[`@figma/figma-plugins/${name}`] = severity 16 | return acc 17 | }, {} as Record) 18 | } 19 | 20 | const errRules: Record = { 21 | 'await-requires-async': awaitRequiresAsync, 22 | 'ban-deprecated-id-params': banDeprecatedIdParams, 23 | 'ban-deprecated-sync-methods': banDeprecatedSyncMethods, 24 | 'ban-deprecated-sync-prop-getters': banDeprecatedSyncPropGetters, 25 | 'ban-deprecated-sync-prop-setters': banDeprecatedSyncPropSetters, 26 | } 27 | 28 | const dynamicePageAdvice: Record = { 29 | 'dynamic-page-documentchange-event-advice': dynamicPageDocumentchangeEventAdvice, 30 | 'dynamic-page-find-method-advice': dynamicPageFindMethodAdvice, 31 | } 32 | 33 | const warnRules: Record = { 34 | ...dynamicePageAdvice, 35 | 'constrain-proportions-replaced-by-target-aspect-ratio-advice': constrainProportionsReplacedByTargetAspectRatioAdvice 36 | } 37 | 38 | // The exported type annotations in this file are somewhat arbitrary; we do NOT 39 | // expect anyone to actually consume these types. We include them because we use 40 | // @figma as a type root, and all packages under a type root must emit a type 41 | // declaration file. 42 | 43 | export const rules: unknown = { 44 | ...errRules, 45 | ...warnRules 46 | } 47 | 48 | export const configs: unknown = { 49 | recommended: { 50 | plugins: ['@figma/figma-plugins'], 51 | rules: { 52 | ...rulesetWithSeverity('error', errRules), 53 | ...rulesetWithSeverity('warn', warnRules), 54 | }, 55 | }, 56 | 'recommended-problems-only': { 57 | plugins: ['@figma/figma-plugins'], 58 | rules: { 59 | ...rulesetWithSeverity('error', errRules), 60 | }, 61 | }, 62 | } 63 | -------------------------------------------------------------------------------- /src/rules/awaitRequiresAsync.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' 2 | import { TraverseTreeResult, createPluginRule, traverseTree } from '../util' 3 | 4 | // Calls to createPluginRule() cause typechecker errors without this import. 5 | // This is a TypeScript bug; cf https://github.com/microsoft/TypeScript/issues/47663 6 | import type { TSESLint, TSESLint as _ } from '@typescript-eslint/utils' 7 | 8 | /** 9 | * This rule requires that functions containing the `await` keyword be marked 10 | * `async`. It's quite a bit more generic than we want for this rule package, 11 | * and overlaps with a feature already present in the VSCode TypeScript 12 | * extension. Nevertheless, we offer it so that we can add `async` modifiers to 13 | * functions via a full-file autofix (e.g. eslint --fix). 14 | * 15 | * Note that this rule covers all cases where `await` is present without 16 | * `async`. Ideally, the fix in this rule would be restricted to cases where 17 | * another fix in this package creates an `await` inside of a function that is 18 | * not async. However, these two fixes cannot co-exist in the same eslint report; 19 | * adding an `async` modifier applies to the entire function, and is considered 20 | * "overlapping" with the fix that adds `await`. eslint reports do not permit 21 | * overlapping fixes. 22 | */ 23 | export const awaitRequiresAsync = createPluginRule({ 24 | name: 'await-requires-async', 25 | meta: { 26 | docs: { 27 | description: 'Require functions that contain `await` to be `async`', 28 | }, 29 | fixable: 'code', 30 | messages: { 31 | requiresAsync: 'Functions containing the await keyword should be marked async.', 32 | }, 33 | schema: [], 34 | type: 'problem', 35 | }, 36 | defaultOptions: [], 37 | create(context) { 38 | return { 39 | ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression) { 40 | runRule(context, node) 41 | }, 42 | FunctionDeclaration(node: TSESTree.FunctionDeclaration) { 43 | runRule(context, node) 44 | }, 45 | FunctionExpression(node: TSESTree.FunctionExpression) { 46 | runRule(context, node) 47 | }, 48 | } 49 | }, 50 | }) 51 | 52 | function containsAwait(containingNode: TSESTree.Node): boolean { 53 | let found = false 54 | 55 | traverseTree(containingNode, (node: TSESTree.Node) => { 56 | if (node.type === AST_NODE_TYPES.AwaitExpression) { 57 | found = true 58 | return TraverseTreeResult.Done 59 | } 60 | 61 | // Ignore `await` in nested functions 62 | if ( 63 | node.type === AST_NODE_TYPES.ArrowFunctionExpression || 64 | node.type === AST_NODE_TYPES.FunctionDeclaration || 65 | node.type === AST_NODE_TYPES.FunctionExpression 66 | ) { 67 | return TraverseTreeResult.SkipChildren 68 | } 69 | 70 | return TraverseTreeResult.Continue 71 | }) 72 | 73 | return found 74 | } 75 | 76 | function runRule( 77 | context: TSESLint.RuleContext<'requiresAsync', TOptions>, 78 | funcNode: 79 | | TSESTree.ArrowFunctionExpression 80 | | TSESTree.FunctionDeclaration 81 | | TSESTree.FunctionExpression, 82 | ): void { 83 | if (funcNode.async) { 84 | return 85 | } 86 | 87 | if (!containsAwait(funcNode.body)) { 88 | return 89 | } 90 | 91 | context.report({ 92 | node: funcNode, 93 | messageId: 'requiresAsync', 94 | fix(fixer) { 95 | const src = context.sourceCode.getText(funcNode) 96 | return fixer.replaceText(funcNode, `async ${src}`) 97 | }, 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /src/rules/banDeprecatedIdParams.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' 2 | import { createPluginRule, getTypeName, isStringNode, matchAncestorTypes } from '../util' 3 | 4 | // Calls to createPluginRule() cause typechecker errors without this import. 5 | // This is a TypeScript bug; cf https://github.com/microsoft/TypeScript/issues/47663 6 | import type { TSESLint as _ } from '@typescript-eslint/utils' 7 | 8 | interface DeprectedIdParam { 9 | receiverType: string 10 | method: string 11 | paramIndex: number 12 | wantParamType: string 13 | asyncObjectFetch: string 14 | } 15 | 16 | const deprecatedIdParams: DeprectedIdParam[] = [ 17 | { 18 | receiverType: 'VariablesAPI', 19 | method: 'createVariable', 20 | paramIndex: 1, 21 | wantParamType: 'VariableCollection', 22 | asyncObjectFetch: 'figma.variables.getVariableCollectionByIdAsync', 23 | }, 24 | { 25 | receiverType: 'ExplicitVariableModesMixin', 26 | method: 'setExplicitVariableModeForCollection', 27 | paramIndex: 0, 28 | wantParamType: 'VariableCollection', 29 | asyncObjectFetch: 'figma.variables.getVariableCollectionByIdAsync', 30 | }, 31 | { 32 | receiverType: 'ExplicitVariableModesMixin', 33 | method: 'clearExplicitVariableModeForCollection', 34 | paramIndex: 0, 35 | wantParamType: 'VariableCollection', 36 | asyncObjectFetch: 'figma.variables.getVariableCollectionByIdAsync', 37 | }, 38 | { 39 | receiverType: 'SceneNodeMixin', 40 | method: 'setBoundVariable', 41 | paramIndex: 1, 42 | wantParamType: 'Variable', 43 | asyncObjectFetch: 'figma.variables.getVariableByIdAsync', 44 | }, 45 | ] 46 | 47 | export const banDeprecatedIdParams = createPluginRule({ 48 | name: 'ban-deprecated-id-params', 49 | meta: { 50 | docs: { 51 | description: 'Ban use of deprecated string ID parameters', 52 | }, 53 | fixable: 'code', 54 | messages: { 55 | useReplacement: 56 | 'Passing a string ID for parameter {{humanReadableParamIndex}} to {{receiverType}}.{{method}} is deprecated. Please pass a {{wantParamType}} instead.', 57 | }, 58 | schema: [], 59 | type: 'problem', 60 | }, 61 | defaultOptions: [], 62 | create(context) { 63 | return { 64 | CallExpression(node: TSESTree.CallExpression) { 65 | const callee = node.callee 66 | if (callee.type !== AST_NODE_TYPES.MemberExpression) { 67 | return 68 | } 69 | 70 | const calleeProp = callee.property 71 | if (calleeProp.type !== AST_NODE_TYPES.Identifier) { 72 | return 73 | } 74 | 75 | const deprecation = deprecatedIdParams.find((p) => p.method === calleeProp.name) 76 | if (!deprecation) { 77 | return 78 | } 79 | 80 | const receiver = callee.object 81 | const match = matchAncestorTypes(context, receiver, [deprecation.receiverType]) 82 | if (!match) { 83 | return 84 | } 85 | 86 | const arg = node.arguments[deprecation.paramIndex] 87 | if (!arg) { 88 | return 89 | } 90 | 91 | if (!isStringNode(context, arg)) { 92 | return 93 | } 94 | 95 | context.report({ 96 | node, 97 | messageId: 'useReplacement', 98 | data: { 99 | humanReadableParamIndex: deprecation.paramIndex + 1, 100 | receiverType: getTypeName(match.nodeType, match.matchedAncestorType), 101 | method: deprecation.method, 102 | wantParamType: deprecation.wantParamType, 103 | }, 104 | fix(fixer) { 105 | const argText = context.sourceCode.getText(arg) 106 | return fixer.replaceText(arg, `await ${deprecation.asyncObjectFetch}(${argText})`) 107 | }, 108 | }) 109 | }, 110 | } 111 | }, 112 | }) 113 | -------------------------------------------------------------------------------- /src/rules/banDeprecatedSyncMethods.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' 2 | import { addAsyncCallFix, createPluginRule, getTypeName, matchAncestorTypes } from '../util' 3 | 4 | // Calls to createPluginRule() cause typechecker errors without this import. 5 | // This is a TypeScript bug; cf https://github.com/microsoft/TypeScript/issues/47663 6 | import type { TSESLint as _ } from '@typescript-eslint/utils' 7 | 8 | interface DeprecatedSyncMethod { 9 | method: string 10 | replacement: string 11 | receiverTypes: string[] 12 | } 13 | 14 | const deprecatedSyncMethods: DeprecatedSyncMethod[] = [ 15 | { 16 | method: 'getFileThumbnailNode', 17 | replacement: 'getFileThumbnailNodeAsync', 18 | receiverTypes: ['PluginAPI'], 19 | }, 20 | { 21 | method: 'getLocalTextStyles', 22 | replacement: 'getLocalTextStylesAsync', 23 | receiverTypes: ['PluginAPI'], 24 | }, 25 | { 26 | method: 'getLocalPaintStyles', 27 | replacement: 'getLocalPaintStylesAsync', 28 | receiverTypes: ['PluginAPI'], 29 | }, 30 | { 31 | method: 'getLocalEffectStyles', 32 | replacement: 'getLocalEffectStylesAsync', 33 | receiverTypes: ['PluginAPI'], 34 | }, 35 | { 36 | method: 'getLocalGridStyles', 37 | replacement: 'getLocalGridStylesAsync', 38 | receiverTypes: ['PluginAPI'], 39 | }, 40 | { 41 | method: 'getLocalVariableCollections', 42 | replacement: 'getLocalVariableCollectionsAsync', 43 | receiverTypes: ['VariablesAPI'], 44 | }, 45 | { 46 | method: 'getLocalVariables', 47 | replacement: 'getLocalVariablesAsync', 48 | receiverTypes: ['VariablesAPI'], 49 | }, 50 | { 51 | method: 'getNodeById', 52 | replacement: 'getNodeByIdAsync', 53 | receiverTypes: ['PluginAPI'], 54 | }, 55 | { 56 | method: 'getStyleById', 57 | replacement: 'getStyleByIdAsync', 58 | receiverTypes: ['PluginAPI'], 59 | }, 60 | { 61 | method: 'getVariableById', 62 | replacement: 'getVariableByIdAsync', 63 | receiverTypes: ['VariablesAPI'], 64 | }, 65 | { 66 | method: 'getVariableCollectionById', 67 | replacement: 'getVariableCollectionByIdAsync', 68 | receiverTypes: ['VariablesAPI'], 69 | }, 70 | { 71 | method: 'setRangeFillStyle', 72 | replacement: 'setRangeFillStyleIdAsync', 73 | receiverTypes: ['NonResizableTextMixin'], 74 | }, 75 | { 76 | method: 'setRangeTextStyle', 77 | replacement: 'setRangeTextStyleIdAsync', 78 | receiverTypes: ['NonResizableTextMixin'], 79 | }, 80 | ] 81 | 82 | export const banDeprecatedSyncMethods = createPluginRule({ 83 | name: 'ban-deprecated-sync-methods', 84 | meta: { 85 | docs: { 86 | description: 'Ban use of deprecated synchronous methods', 87 | }, 88 | fixable: 'code', 89 | messages: { 90 | useReplacement: 91 | '{{receiverType}}.{{method}} is deprecated. Please use {{replacement}} instead.', 92 | }, 93 | schema: [], 94 | type: 'problem', 95 | }, 96 | defaultOptions: [], 97 | create(context) { 98 | return { 99 | CallExpression(node: TSESTree.CallExpression) { 100 | const callee = node.callee 101 | if (callee.type !== AST_NODE_TYPES.MemberExpression) { 102 | return 103 | } 104 | 105 | const calleeProp = callee.property 106 | if (calleeProp.type !== AST_NODE_TYPES.Identifier) { 107 | return 108 | } 109 | 110 | const deprecation = deprecatedSyncMethods.find((m) => m.method === calleeProp.name) 111 | if (!deprecation) { 112 | return 113 | } 114 | 115 | const receiver = callee.object 116 | const match = matchAncestorTypes(context, receiver, deprecation.receiverTypes) 117 | if (!match) { 118 | return 119 | } 120 | 121 | context.report({ 122 | node, 123 | messageId: 'useReplacement', 124 | data: { 125 | receiverType: getTypeName(match.nodeType, match.matchedAncestorType), 126 | method: deprecation.method, 127 | replacement: deprecation.replacement, 128 | }, 129 | fix(fixer) { 130 | return addAsyncCallFix({ 131 | context, 132 | fixer, 133 | expression: node, 134 | receiver: receiver, 135 | asyncIdentifier: deprecation.replacement, 136 | args: node.arguments, 137 | }) 138 | }, 139 | }) 140 | }, 141 | } 142 | }, 143 | }) 144 | -------------------------------------------------------------------------------- /src/rules/banDeprecatedSyncPropGetters.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' 2 | import { addAsyncCallFix, createPluginRule, getTypeName, matchAncestorTypes } from '../util' 3 | 4 | // Calls to createPluginRule() cause typechecker errors without this import. 5 | // This is a TypeScript bug; cf https://github.com/microsoft/TypeScript/issues/47663 6 | import type { TSESLint as _ } from '@typescript-eslint/utils' 7 | 8 | interface DeprecatedSyncPropGetter { 9 | property: string 10 | replacement: string 11 | receiverTypes: string[] 12 | } 13 | 14 | const deprecatedSyncPropGetters: DeprecatedSyncPropGetter[] = [ 15 | { 16 | property: 'instances', 17 | replacement: 'getInstancesAsync', 18 | receiverTypes: ['ComponentNode'], 19 | }, 20 | { 21 | property: 'consumers', 22 | replacement: 'getConsumersAsync', 23 | receiverTypes: ['BaseStyle'], 24 | }, 25 | { 26 | property: 'mainComponent', 27 | replacement: 'getMainComponentAsync', 28 | receiverTypes: ['InstanceNode'], 29 | }, 30 | ] 31 | 32 | export const banDeprecatedSyncPropGetters = createPluginRule({ 33 | name: 'ban-deprecated-sync-prop-getters', 34 | meta: { 35 | docs: { 36 | description: 'Ban use of deprecated synchronous property getters', 37 | }, 38 | fixable: 'code', 39 | messages: { 40 | useReplacement: 41 | 'Reading from {{receiverType}}.{{property}} is deprecated. Please use {{replacement}} instead.', 42 | }, 43 | schema: [], 44 | type: 'problem', 45 | }, 46 | defaultOptions: [], 47 | create(context) { 48 | return { 49 | MemberExpression(node: TSESTree.MemberExpression) { 50 | // allow the expression to be used in an assignment 51 | const parent = node.parent 52 | if (parent && parent.type === AST_NODE_TYPES.AssignmentExpression && parent.left === node) { 53 | return 54 | } 55 | 56 | const prop = node.property 57 | if (prop.type !== AST_NODE_TYPES.Identifier) { 58 | return 59 | } 60 | 61 | const deprecation = deprecatedSyncPropGetters.find((g) => g.property === prop.name) 62 | if (!deprecation) { 63 | return 64 | } 65 | 66 | const receiver = node.object 67 | const match = matchAncestorTypes(context, receiver, deprecation.receiverTypes) 68 | if (!match) { 69 | return 70 | } 71 | 72 | context.report({ 73 | node, 74 | messageId: 'useReplacement', 75 | data: { 76 | receiverType: getTypeName(match.nodeType, match.matchedAncestorType), 77 | property: deprecation.property, 78 | replacement: deprecation.replacement, 79 | }, 80 | fix(fixer) { 81 | return addAsyncCallFix({ 82 | context, 83 | fixer, 84 | expression: node, 85 | receiver, 86 | asyncIdentifier: deprecation.replacement, 87 | args: [], 88 | }) 89 | }, 90 | }) 91 | }, 92 | } 93 | }, 94 | }) 95 | -------------------------------------------------------------------------------- /src/rules/banDeprecatedSyncPropSetters.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' 2 | import { addAsyncCallFix, createPluginRule, getTypeName, matchAncestorTypes } from '../util' 3 | 4 | // Calls to createPluginRule() cause typechecker errors without this import. 5 | // This is a TypeScript bug; cf https://github.com/microsoft/TypeScript/issues/47663 6 | import type { TSESLint as _ } from '@typescript-eslint/utils' 7 | 8 | interface DeprecatedSyncPropSetter { 9 | property: string 10 | replacement: string 11 | receiverTypes: string[] 12 | } 13 | 14 | const DeprecatedSyncPropSetters: DeprecatedSyncPropSetter[] = [ 15 | { 16 | property: 'currentPage', 17 | replacement: 'setCurrentPageAsync', 18 | receiverTypes: ['PluginAPI'], 19 | }, 20 | { 21 | property: 'effectStyleId', 22 | replacement: 'setEffectStyleIdAsync', 23 | receiverTypes: ['BlendMixin'], 24 | }, 25 | { 26 | property: 'fillStyleId', 27 | replacement: 'setFillStyleIdAsync', 28 | receiverTypes: ['MinimalFillsMixin'], 29 | }, 30 | { 31 | property: 'gridStyleId', 32 | replacement: 'setGridStyleIdAsync', 33 | receiverTypes: ['BaseFrameMixin'], 34 | }, 35 | { 36 | property: 'strokeStyleId', 37 | replacement: 'setStrokeStyleIdAsync', 38 | receiverTypes: ['MinimalStrokesMixin'], 39 | }, 40 | { 41 | property: 'textStyleId', 42 | replacement: 'setTextStyleIdAsync', 43 | receiverTypes: ['TextNode'], 44 | }, 45 | { 46 | property: 'backgroundStyleId', 47 | replacement: 'setFillStyleIdAsync', 48 | receiverTypes: ['DeprecatedBackgroundMixin'], 49 | }, 50 | { 51 | property: 'vectorNetwork', 52 | replacement: 'setVectorNetworkAsync', 53 | receiverTypes: ['VectorLikeMixin'], 54 | }, 55 | { 56 | property: 'reactions', 57 | replacement: 'setReactionsAsync', 58 | receiverTypes: ['ReactionMixin'], 59 | }, 60 | ] 61 | 62 | export const banDeprecatedSyncPropSetters = createPluginRule({ 63 | name: 'ban-deprecated-sync-prop-setters', 64 | meta: { 65 | docs: { 66 | description: 'Ban use of deprecated synchronous property getters', 67 | }, 68 | fixable: 'code', 69 | messages: { 70 | useReplacement: 71 | 'Assigning to {{receiverType}}.{{property}} is deprecated. Please use {{replacement}} instead.', 72 | }, 73 | schema: [], 74 | type: 'problem', 75 | }, 76 | defaultOptions: [], 77 | create(context) { 78 | return { 79 | AssignmentExpression(node: TSESTree.AssignmentExpression) { 80 | if (node.left.type !== AST_NODE_TYPES.MemberExpression) { 81 | return 82 | } 83 | 84 | const prop = node.left.property 85 | if (prop.type !== AST_NODE_TYPES.Identifier) { 86 | return 87 | } 88 | 89 | const deprecation = DeprecatedSyncPropSetters.find((s) => s.property === prop.name) 90 | if (!deprecation) { 91 | return 92 | } 93 | 94 | const receiver = node.left.object 95 | const match = matchAncestorTypes(context, receiver, deprecation.receiverTypes) 96 | if (!match) { 97 | return 98 | } 99 | 100 | context.report({ 101 | node, 102 | messageId: 'useReplacement', 103 | data: { 104 | receiverType: getTypeName(match.nodeType, match.matchedAncestorType), 105 | property: deprecation.property, 106 | replacement: deprecation.replacement, 107 | }, 108 | fix(fixer) { 109 | return addAsyncCallFix({ 110 | context, 111 | fixer, 112 | expression: node, 113 | receiver, 114 | asyncIdentifier: deprecation.replacement, 115 | args: [node.right], 116 | }) 117 | }, 118 | }) 119 | }, 120 | } 121 | }, 122 | }) 123 | -------------------------------------------------------------------------------- /src/rules/constrainProportionsReplacedByTargetAspectRatioAdvice.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' 2 | import { createPluginRule, matchAncestorTypes } from '../util' 3 | 4 | // Copied from dynamicPageFindMethodAdvice 5 | // Calls to createPluginRule() cause typechecker errors without this import. 6 | // Needed for TypeScript bug 7 | import type { TSESLint as _ } from '@typescript-eslint/utils' 8 | 9 | export const constrainProportionsReplacedByTargetAspectRatioAdvice = createPluginRule({ 10 | name: 'constrain-proportions-replaced-by-target-aspect-ratio-advice', 11 | meta: { 12 | docs: { 13 | description: 'Warns against using constrainProportions in favor of targetAspectRatio', 14 | }, 15 | messages: { 16 | readAdvice: 'Please use targetAspectRatio instead of constrainProportions for determining if a node will resize proportinally.', 17 | writeAdvice: 'Please use lockAspectRatio() or unlockAspectRatio() instead of setting constrainProportions.', 18 | }, 19 | schema: [], 20 | type: 'suggestion', 21 | }, 22 | defaultOptions: [], 23 | create(context) { 24 | return { 25 | MemberExpression(node: TSESTree.MemberExpression) { 26 | const property = node.property 27 | if ( 28 | property.type === AST_NODE_TYPES.Identifier && 29 | property.name === 'constrainProportions' 30 | ) { 31 | // Check if the receiver is a LayoutMixin, since that's what constrainProportions lives on 32 | const match = matchAncestorTypes(context, node.object, ['LayoutMixin']) 33 | if (!match) { 34 | return 35 | } 36 | 37 | // Check if it's being read or written to 38 | const parent = node.parent 39 | if ( 40 | parent?.type === AST_NODE_TYPES.AssignmentExpression && 41 | parent.left === node 42 | ) { 43 | // It's being written to 44 | context.report({ 45 | node, 46 | messageId: 'writeAdvice', 47 | }) 48 | } else { 49 | // It's being read 50 | context.report({ 51 | node, 52 | messageId: 'readAdvice', 53 | }) 54 | } 55 | } 56 | }, 57 | } 58 | }, 59 | }) -------------------------------------------------------------------------------- /src/rules/dynamicPageDocumentchangeEventAdvice.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' 2 | import { createPluginRule, matchAncestorTypes } from '../util' 3 | 4 | // Calls to createPluginRule() cause typechecker errors without this import. 5 | // This is a TypeScript bug; cf https://github.com/microsoft/TypeScript/issues/47663 6 | import type { TSESLint as _ } from '@typescript-eslint/utils' 7 | 8 | export const dynamicPageDocumentchangeEventAdvice = createPluginRule({ 9 | name: 'dynamic-page-documentchange-event-advice', 10 | meta: { 11 | docs: { 12 | description: 'Advice on using the `documentchange` event', 13 | }, 14 | messages: { 15 | advice: `When using the dynamic-page manifest field, remember to call figma.loadAllPagesAsync() before using DocumentNode.{{method}}(). loadAllPagesAsync() only needs to be called once.`, 16 | }, 17 | schema: [], 18 | type: 'suggestion', 19 | }, 20 | defaultOptions: [], 21 | create(context) { 22 | return { 23 | CallExpression(node: TSESTree.CallExpression) { 24 | // Check that we're calling one of on(), once(), or off() 25 | const callee = node.callee 26 | if (callee.type !== AST_NODE_TYPES.MemberExpression) { 27 | return 28 | } 29 | 30 | const calleeProp = callee.property 31 | if (calleeProp.type !== AST_NODE_TYPES.Identifier) { 32 | return 33 | } 34 | 35 | if (calleeProp.name !== 'on' && calleeProp.name !== 'once' && calleeProp.name !== 'off') { 36 | return 37 | } 38 | 39 | // Check that the first argument is 'documentchange' 40 | const args = node.arguments 41 | if (args.length < 1) { 42 | return 43 | } 44 | 45 | const eventName = args[0] 46 | if (eventName.type !== AST_NODE_TYPES.Literal) { 47 | return 48 | } 49 | 50 | if (eventName.value !== 'documentchange') { 51 | return 52 | } 53 | 54 | // Ensure that we're calling the event handler method on a PluginAPI instance 55 | if (!matchAncestorTypes(context, callee.object, ['PluginAPI'])) { 56 | return 57 | } 58 | 59 | context.report({ node, messageId: 'advice' }) 60 | }, 61 | } 62 | }, 63 | }) 64 | -------------------------------------------------------------------------------- /src/rules/dynamicPageFindMethodAdvice.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' 2 | import { createPluginRule, matchAncestorTypes } from '../util' 3 | 4 | // Calls to createPluginRule() cause typechecker errors without this import. 5 | // This is a TypeScript bug; cf https://github.com/microsoft/TypeScript/issues/47663 6 | import type { TSESLint as _ } from '@typescript-eslint/utils' 7 | 8 | const findMethods = ['findAll', 'findAllWithCriteria', 'findOne'] 9 | 10 | export const dynamicPageFindMethodAdvice = createPluginRule({ 11 | name: 'dynamic-page-find-method-advice', 12 | meta: { 13 | docs: { 14 | description: 'Advice on using the find*() family of methods', 15 | }, 16 | messages: { 17 | advice: 18 | 'When using the dynamic-page manifest field, remember to call figma.loadAllPagesAsync() before using DocumentNode.{{method}}(). loadAllPagesAsync() only needs to be called once.', 19 | }, 20 | schema: [], 21 | type: 'suggestion', 22 | }, 23 | defaultOptions: [], 24 | create(context) { 25 | return { 26 | CallExpression(node: TSESTree.CallExpression) { 27 | const callee = node.callee 28 | if (callee.type !== AST_NODE_TYPES.MemberExpression) { 29 | return 30 | } 31 | 32 | const calleeProp = callee.property 33 | if (calleeProp.type !== AST_NODE_TYPES.Identifier) { 34 | return 35 | } 36 | 37 | if (!findMethods.includes(calleeProp.name)) { 38 | return 39 | } 40 | 41 | const receiver = callee.object 42 | const match = matchAncestorTypes(context, receiver, ['DocumentNode']) 43 | if (!match) { 44 | return 45 | } 46 | 47 | context.report({ 48 | node, 49 | messageId: 'advice', 50 | data: { method: calleeProp.name }, 51 | }) 52 | }, 53 | } 54 | }, 55 | }) 56 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES, ESLintUtils, TSESLint, TSESTree } from '@typescript-eslint/utils' 2 | import ts from 'typescript' 3 | 4 | export const createPluginRule = ESLintUtils.RuleCreator( 5 | (name) => `https://github.com/figma/eslint-plugin-figma-plugins/blob/main/docs/rules/${name}.md`, 6 | ) 7 | 8 | function mapIdentity(val: T, _index: number): T { 9 | return val 10 | } 11 | 12 | export function addAsyncCallFix({ 13 | context, 14 | fixer, 15 | expression, 16 | receiver, 17 | asyncIdentifier, 18 | args, 19 | argsPostProcessor, 20 | }: { 21 | context: TSESLint.RuleContext 22 | fixer: TSESLint.RuleFixer 23 | expression: TSESTree.Node 24 | receiver: TSESTree.Node 25 | asyncIdentifier: string 26 | args: TSESTree.Node[] 27 | argsPostProcessor?: (s: string, index: number) => string 28 | }): TSESLint.RuleFix { 29 | const doParens = 30 | receiver.type !== AST_NODE_TYPES.Identifier && 31 | receiver.type !== AST_NODE_TYPES.MemberExpression && 32 | receiver.type !== AST_NODE_TYPES.CallExpression 33 | let rcvSrc = context.sourceCode.getText(receiver) 34 | rcvSrc = doParens ? `(${rcvSrc})` : rcvSrc 35 | const paramsSrc = args 36 | .map((a) => context.sourceCode.getText(a)) 37 | .map(argsPostProcessor ?? mapIdentity) 38 | .join(', ') 39 | return fixer.replaceText(expression, `await ${rcvSrc}.${asyncIdentifier}(${paramsSrc})`) 40 | } 41 | 42 | export interface MatchAncestorTypeResult { 43 | nodeType: ts.Type 44 | matchedAncestorType: string 45 | } 46 | 47 | export function matchAncestorTypes( 48 | context: TSESLint.RuleContext, 49 | node: TSESTree.Node, 50 | ancestorTypes: string[], 51 | ): MatchAncestorTypeResult | undefined { 52 | const type = ESLintUtils.getParserServices(context).getTypeAtLocation(node) 53 | const match = ancestorTypes.find((name) => composedOfTypeWithName(type, name)) 54 | return match ? { nodeType: type, matchedAncestorType: match } : undefined 55 | } 56 | 57 | export enum TraverseTreeResult { 58 | Continue, 59 | SkipChildren, 60 | Done, 61 | } 62 | 63 | /** 64 | * Traverse a TSESTree.Node tree in depth-first order. The visitor function can 65 | * indicate whether to continue traversing the node's children, skip the node's 66 | * children, or stop traversing altogether. 67 | */ 68 | export function traverseTree( 69 | root: TSESTree.Node, 70 | visitor: (node: TSESTree.Node) => TraverseTreeResult, 71 | ): void { 72 | traverseTreeRecursive(root, visitor) 73 | } 74 | 75 | function traverseTreeRecursive( 76 | node: TSESTree.Node, 77 | visitor: (node: TSESTree.Node) => TraverseTreeResult, 78 | ): TraverseTreeResult.Done | undefined { 79 | // This algorithm is provided by: 80 | // github.com/typescript-eslint/typescript-eslint/blob/705370ac0d9c54081657b8855b398e57d6ea4ddb/packages/typescript-estree/src/simple-traverse.ts 81 | 82 | const result = visitor(node) 83 | if (result === TraverseTreeResult.Done) { 84 | return TraverseTreeResult.Done 85 | } 86 | if (result === TraverseTreeResult.SkipChildren) { 87 | return 88 | } 89 | 90 | for (const [k, childOrChildren] of Object.entries(node)) { 91 | // Avoid cycles. Ideally, we could restrict this to an even narrower set of 92 | // keys, but it's a lot of work to inventory all possible keys containing 93 | // child nodes, and it wouldn't be future-proof. 94 | if (k === 'parent') { 95 | continue 96 | } 97 | 98 | if (isValidNode(childOrChildren)) { 99 | if (traverseTreeRecursive(childOrChildren, visitor) === TraverseTreeResult.Done) { 100 | return TraverseTreeResult.Done 101 | } 102 | } else if (Array.isArray(childOrChildren)) { 103 | for (const child of childOrChildren) { 104 | if (!isValidNode(child)) { 105 | // We're not in an array of children, so let's just skip this key 106 | break 107 | } 108 | 109 | if (traverseTreeRecursive(child, visitor) === TraverseTreeResult.Done) { 110 | return TraverseTreeResult.Done 111 | } 112 | } 113 | } 114 | } 115 | } 116 | 117 | /** 118 | * This is a duck-type test to determine whether a value is a TSESTree.Node. It 119 | * is not particularly bulletproof, and I'd suggest not using it unless you can 120 | * guarantee that the input value is either a node or comes from a node. 121 | */ 122 | function isValidNode(x: unknown): x is TSESTree.Node { 123 | return typeof x === 'object' && x != null && 'type' in x && typeof x.type === 'string' 124 | } 125 | 126 | function composedOfTypeWithName(t: ts.Type, typeName: string): boolean { 127 | if (t.symbol && t.symbol.name === typeName) { 128 | return true 129 | } 130 | 131 | if (t.aliasSymbol && t.aliasSymbol.name === typeName) { 132 | return true 133 | } 134 | 135 | if (t.isUnion()) { 136 | return t.types.some((t) => composedOfTypeWithName(t, typeName)) 137 | } 138 | 139 | if (t.isIntersection()) { 140 | return t.types.some((t) => composedOfTypeWithName(t, typeName)) 141 | } 142 | 143 | const baseTypes = t.getBaseTypes() 144 | if (baseTypes) { 145 | return baseTypes.some((t) => composedOfTypeWithName(t, typeName)) 146 | } 147 | 148 | return false 149 | } 150 | 151 | /** 152 | * When running these rules from tests, sometimes a TypeScript Type object's 153 | * symbol property is undefined, contrary to the type declaration. This seems to 154 | * happen when an expression has a named type, but the type does not to resolve 155 | * to anything that the typechecker knows about. 156 | * 157 | * The discrepancy between the compiler API and its type definitions may be due 158 | * to this bug: https://github.com/microsoft/TypeScript/issues/13165 159 | * 160 | * As a workaround, we use two fallbacks, in order of priority: 161 | * - aliasSymbol.escapedName 162 | * - the fallback argument, which should be the type we searched for in 163 | * matchAncestorTypes() 164 | */ 165 | export function getTypeName(t: ts.Type, fallback: string): string { 166 | return t.symbol?.name ?? t.aliasSymbol?.escapedName ?? fallback 167 | } 168 | 169 | export function isStringNode( 170 | context: TSESLint.RuleContext, 171 | node: TSESTree.Node, 172 | ): boolean { 173 | const type = ESLintUtils.getParserServices(context).getTypeAtLocation(node) 174 | return !!(type.flags & ts.TypeFlags.StringLike) 175 | } 176 | -------------------------------------------------------------------------------- /test/awaitRequiresAsync.test.ts: -------------------------------------------------------------------------------- 1 | import { awaitRequiresAsync } from '../src/rules/awaitRequiresAsync' 2 | import { ruleTester } from './testUtil' 3 | 4 | ruleTester().run('await-requires-async', awaitRequiresAsync, { 5 | valid: [ 6 | { 7 | code: ` 8 | function foo() { 9 | async function bar() { 10 | await baz() 11 | } 12 | 13 | qux(async () => { 14 | await baz() 15 | }) 16 | 17 | qux(async function() { 18 | await baz() 19 | }) 20 | } 21 | `, 22 | }, 23 | ], 24 | invalid: [ 25 | { 26 | code: ` 27 | function foo() { 28 | await bar() 29 | } 30 | `, 31 | output: ` 32 | async function foo() { 33 | await bar() 34 | } 35 | `, 36 | errors: [{ messageId: 'requiresAsync' }], 37 | }, 38 | { 39 | // multple awaits 40 | code: ` 41 | function foo() { 42 | await bar() 43 | await baz() 44 | } 45 | `, 46 | output: ` 47 | async function foo() { 48 | await bar() 49 | await baz() 50 | } 51 | `, 52 | errors: [{ messageId: 'requiresAsync' }], 53 | }, 54 | { 55 | // nested awaits 56 | code: ` 57 | function foo() { 58 | function bar() { 59 | await baz() 60 | } 61 | 62 | qux(() => { 63 | await baz() 64 | }) 65 | 66 | qux(function() { 67 | await baz() 68 | }) 69 | } 70 | `, 71 | output: ` 72 | function foo() { 73 | async function bar() { 74 | await baz() 75 | } 76 | 77 | qux(async () => { 78 | await baz() 79 | }) 80 | 81 | qux(async function() { 82 | await baz() 83 | }) 84 | } 85 | `, 86 | errors: [ 87 | { messageId: 'requiresAsync' }, 88 | { messageId: 'requiresAsync' }, 89 | { messageId: 'requiresAsync' }, 90 | ], 91 | }, 92 | ], 93 | }) 94 | -------------------------------------------------------------------------------- /test/banDeprecatedIdParams.test.ts: -------------------------------------------------------------------------------- 1 | import { banDeprecatedIdParams } from '../src/rules/banDeprecatedIdParams' 2 | import { ruleTester } from './testUtil' 3 | 4 | const types = ` 5 | interface BaseNode {} 6 | 7 | interface VariablesAPI { 8 | createVariable(name: string, collectionId: string, resolvedType: string): void 9 | } 10 | ` 11 | 12 | ruleTester().run('banDeprecatedIdParams', banDeprecatedIdParams, { 13 | valid: [ 14 | { 15 | code: ` 16 | ${types} 17 | function func(notVariables: NotVariablesAPI) { 18 | notVariables.createVariable('name', '123') 19 | } 20 | `, 21 | }, 22 | { 23 | code: ` 24 | ${types} 25 | function func(variables: VariablesAPI) { 26 | variables.nonDeprecatedMethod('123') 27 | } 28 | `, 29 | }, 30 | ], 31 | invalid: [ 32 | { 33 | code: ` 34 | ${types} 35 | function func(variables: VariablesAPI) { 36 | variables.createVariable('name', '123', 'type') 37 | } 38 | `, 39 | output: ` 40 | ${types} 41 | function func(variables: VariablesAPI) { 42 | variables.createVariable('name', await figma.variables.getVariableCollectionByIdAsync('123'), 'type') 43 | } 44 | `, 45 | errors: [{ messageId: 'useReplacement' }], 46 | }, 47 | { 48 | code: ` 49 | ${types} 50 | function func(variables: VariablesAPI, getID: () => string) { 51 | variables.createVariable('name', getID(), 'type') 52 | } 53 | `, 54 | output: ` 55 | ${types} 56 | function func(variables: VariablesAPI, getID: () => string) { 57 | variables.createVariable('name', await figma.variables.getVariableCollectionByIdAsync(getID()), 'type') 58 | } 59 | `, 60 | errors: [{ messageId: 'useReplacement' }], 61 | }, 62 | ], 63 | }) 64 | -------------------------------------------------------------------------------- /test/banDeprecatedSyncMethods.test.ts: -------------------------------------------------------------------------------- 1 | import { banDeprecatedSyncMethods } from '../src/rules/banDeprecatedSyncMethods' 2 | import { ruleTester } from './testUtil' 3 | 4 | const types = ` 5 | interface BaseNode {} 6 | 7 | interface PluginAPI { 8 | getNodeById(id: string): BaseNode | null 9 | } 10 | ` 11 | 12 | ruleTester().run('ban-deprecated-sync-methods', banDeprecatedSyncMethods, { 13 | valid: [ 14 | { 15 | code: ` 16 | ${types} 17 | function func(notFigma: NotPluginAPI) { 18 | notFigma.getNodeById('123') 19 | } 20 | `, 21 | }, 22 | { 23 | code: ` 24 | ${types} 25 | function func(figma: PluginApi) { 26 | figma.nonDeprecatedMethod('123') 27 | } 28 | `, 29 | }, 30 | ], 31 | invalid: [ 32 | { 33 | code: ` 34 | ${types} 35 | function func(figma: PluginAPI) { 36 | figma.getNodeById('123') 37 | } 38 | `, 39 | output: ` 40 | ${types} 41 | function func(figma: PluginAPI) { 42 | await figma.getNodeByIdAsync('123') 43 | } 44 | `, 45 | errors: [{ messageId: 'useReplacement' }], 46 | }, 47 | { 48 | code: ` 49 | ${types} 50 | function func(getFigma: () => PluginAPI) { 51 | getFigma().getNodeById('123') 52 | } 53 | `, 54 | output: ` 55 | ${types} 56 | function func(getFigma: () => PluginAPI) { 57 | await getFigma().getNodeByIdAsync('123') 58 | } 59 | `, 60 | errors: [{ messageId: 'useReplacement' }], 61 | }, 62 | { 63 | code: ` 64 | ${types} 65 | function func(figma: PluginAPI) { 66 | (figma).getNodeById('123') 67 | } 68 | `, 69 | output: ` 70 | ${types} 71 | function func(figma: PluginAPI) { 72 | await figma.getNodeByIdAsync('123') 73 | } 74 | `, 75 | errors: [{ messageId: 'useReplacement' }], 76 | }, 77 | { 78 | code: ` 79 | ${types} 80 | function func(a: PluginAPI, b: PluginAPI) { 81 | ;(true ? a : b).getNodeById('123') 82 | } 83 | `, 84 | output: ` 85 | ${types} 86 | function func(a: PluginAPI, b: PluginAPI) { 87 | ;await (true ? a : b).getNodeByIdAsync('123') 88 | } 89 | `, 90 | errors: [{ messageId: 'useReplacement' }], 91 | }, 92 | ], 93 | }) 94 | -------------------------------------------------------------------------------- /test/banDeprecatedSyncPropGetters.test.ts: -------------------------------------------------------------------------------- 1 | import { banDeprecatedSyncPropGetters } from '../src/rules/banDeprecatedSyncPropGetters' 2 | import { ruleTester } from './testUtil' 3 | 4 | const types = ` 5 | interface InstanceNode {} 6 | 7 | interface ComponentNode { 8 | instances: InstanceNode[] 9 | } 10 | ` 11 | 12 | ruleTester().run('ban-deprecated-sync-prop-getters', banDeprecatedSyncPropGetters, { 13 | valid: [ 14 | { 15 | code: ` 16 | ${types} 17 | function func(notComponentNode: NotComponentNode) { 18 | notComponentNode.instances 19 | } 20 | `, 21 | }, 22 | { 23 | code: ` 24 | ${types} 25 | function func(componentNode: ComponentNode) { 26 | componentNode.someOtherProp 27 | } 28 | `, 29 | }, 30 | { 31 | // assignment expressions should be safe 32 | code: ` 33 | ${types} 34 | function func(node: InstanceNode, comp: ComponentNode) { 35 | node.mainComponent = comp 36 | } 37 | `, 38 | }, 39 | ], 40 | invalid: [ 41 | { 42 | code: ` 43 | ${types} 44 | function func(componentNode: ComponentNode) { 45 | componentNode.instances 46 | } 47 | `, 48 | output: ` 49 | ${types} 50 | function func(componentNode: ComponentNode) { 51 | await componentNode.getInstancesAsync() 52 | } 53 | `, 54 | errors: [{ messageId: 'useReplacement' }], 55 | }, 56 | { 57 | code: ` 58 | ${types} 59 | function func(getComponentNode: () => ComponentNode) { 60 | getComponentNode().instances 61 | } 62 | `, 63 | output: ` 64 | ${types} 65 | function func(getComponentNode: () => ComponentNode) { 66 | await getComponentNode().getInstancesAsync() 67 | } 68 | `, 69 | errors: [{ messageId: 'useReplacement' }], 70 | }, 71 | { 72 | code: ` 73 | ${types} 74 | function func(componentNode: ComponentNode) { 75 | (componentNode).instances 76 | } 77 | `, 78 | output: ` 79 | ${types} 80 | function func(componentNode: ComponentNode) { 81 | await componentNode.getInstancesAsync() 82 | } 83 | `, 84 | errors: [{ messageId: 'useReplacement' }], 85 | }, 86 | { 87 | code: ` 88 | ${types} 89 | function func(a: ComponentNode, b: ComponentNode) { 90 | ;(true ? a : b).instances 91 | } 92 | `, 93 | output: ` 94 | ${types} 95 | function func(a: ComponentNode, b: ComponentNode) { 96 | ;await (true ? a : b).getInstancesAsync() 97 | } 98 | `, 99 | errors: [{ messageId: 'useReplacement' }], 100 | }, 101 | ], 102 | }) 103 | -------------------------------------------------------------------------------- /test/banDeprecatedSyncPropSetters.test.ts: -------------------------------------------------------------------------------- 1 | import { banDeprecatedSyncPropSetters } from '../src/rules/banDeprecatedSyncPropSetters' 2 | import { ruleTester } from './testUtil' 3 | 4 | const types = ` 5 | interface BlendMixin { 6 | effectStyleId: string 7 | } 8 | 9 | interface DefaultShapeMixin extends BlendMixin {} 10 | 11 | interface LineNode extends DefaultShapeMixin {} 12 | ` 13 | 14 | ruleTester().run('ban-deprecated-sync-prop-setters', banDeprecatedSyncPropSetters, { 15 | valid: [ 16 | { 17 | code: ` 18 | ${types} 19 | function func(notLineNode: NotLineNode) { 20 | notLineNode.effectStyleId = '1' 21 | } 22 | `, 23 | }, 24 | { 25 | code: ` 26 | ${types} 27 | function func(lineNode: LineNode) { 28 | lineNode.someOtherProp = '1' 29 | } 30 | `, 31 | }, 32 | ], 33 | invalid: [ 34 | { 35 | code: ` 36 | ${types} 37 | function func(lineNode: LineNode) { 38 | lineNode.effectStyleId = '1' 39 | } 40 | `, 41 | output: ` 42 | ${types} 43 | function func(lineNode: LineNode) { 44 | await lineNode.setEffectStyleIdAsync('1') 45 | } 46 | `, 47 | errors: [{ messageId: 'useReplacement' }], 48 | }, 49 | { 50 | code: ` 51 | ${types} 52 | function func(getLineNode: () => LineNode) { 53 | getLineNode().effectStyleId = '1' 54 | } 55 | `, 56 | output: ` 57 | ${types} 58 | function func(getLineNode: () => LineNode) { 59 | await getLineNode().setEffectStyleIdAsync('1') 60 | } 61 | `, 62 | errors: [{ messageId: 'useReplacement' }], 63 | }, 64 | { 65 | code: ` 66 | ${types} 67 | function func(lineNode: LineNode) { 68 | (lineNode).effectStyleId = '1' 69 | } 70 | `, 71 | output: ` 72 | ${types} 73 | function func(lineNode: LineNode) { 74 | await lineNode.setEffectStyleIdAsync('1') 75 | } 76 | `, 77 | errors: [{ messageId: 'useReplacement' }], 78 | }, 79 | { 80 | code: ` 81 | ${types} 82 | function func(a: LineNode, b: LineNode) { 83 | ;(true ? a : b).effectStyleId = '1' 84 | } 85 | `, 86 | output: ` 87 | ${types} 88 | function func(a: LineNode, b: LineNode) { 89 | ;await (true ? a : b).setEffectStyleIdAsync('1') 90 | } 91 | `, 92 | errors: [{ messageId: 'useReplacement' }], 93 | }, 94 | ], 95 | }) 96 | -------------------------------------------------------------------------------- /test/constrainProportionsReplacedByTargetAspectRatio.test.ts: -------------------------------------------------------------------------------- 1 | import { constrainProportionsReplacedByTargetAspectRatioAdvice } from '../src/rules/constrainProportionsReplacedByTargetAspectRatioAdvice' 2 | import { ruleTester } from './testUtil' 3 | 4 | const types = ` 5 | interface LayoutMixin { 6 | constrainProportions: boolean 7 | } 8 | 9 | interface DefaultFrameMixin extends LayoutMixin {} 10 | 11 | interface FrameNode extends DefaultFrameMixin {} 12 | 13 | interface SceneNode extends LayoutMixin {} 14 | ` 15 | 16 | ruleTester().run('constrain-proportions-replaced-by-target-aspect-ratio', constrainProportionsReplacedByTargetAspectRatioAdvice, { 17 | valid: [ 18 | { 19 | code: ` 20 | ${types} 21 | function func(node: FrameNode) { 22 | node.someOtherProp = true 23 | } 24 | `, 25 | }, 26 | ], 27 | invalid: [ 28 | { 29 | // Test write case 30 | code: ` 31 | ${types} 32 | function func(node: SceneNode) { 33 | node.constrainProportions = true 34 | } 35 | `, 36 | errors: [{ messageId: 'writeAdvice' }], 37 | }, 38 | { 39 | // Test write case 40 | code: ` 41 | ${types} 42 | function func(node: FrameNode) { 43 | node.constrainProportions = false 44 | } 45 | `, 46 | errors: [{ messageId: 'writeAdvice' }], 47 | }, 48 | { 49 | // Test read case 50 | code: ` 51 | ${types} 52 | function func(node: SceneNode) { 53 | const value = node.constrainProportions 54 | } 55 | `, 56 | errors: [{ messageId: 'readAdvice' }], 57 | }, 58 | ], 59 | }) -------------------------------------------------------------------------------- /test/dynamicPageDocumentchangeEventAdvice.ts: -------------------------------------------------------------------------------- 1 | import { dynamicPageDocumentchangeEventAdvice } from '../src/rules/dynamicPageDocumentchangeEventAdvice' 2 | import { ruleTester } from './testUtil' 3 | 4 | const types = ` 5 | interface PluginAPI { 6 | on(event: string): void 7 | once(event: string): void 8 | off(event: string): void 9 | } 10 | ` 11 | 12 | ruleTester().run('ban-deprecated-documentchange-event', dynamicPageDocumentchangeEventAdvice, { 13 | valid: [ 14 | { 15 | code: ` 16 | ${types} 17 | function func(figma: PluginAPI) { 18 | figma.on('selectionchange') 19 | } 20 | `, 21 | }, 22 | { 23 | code: ` 24 | ${types} 25 | function func(figma: PluginAPI) { 26 | figma.once('selectionchange') 27 | } 28 | `, 29 | }, 30 | { 31 | code: ` 32 | ${types} 33 | function func(figma: PluginAPI) { 34 | figma.off('selectionchange') 35 | } 36 | `, 37 | }, 38 | { 39 | code: ` 40 | function func(notFigma: NotPluginAPI) { 41 | notFigma.on('documentchange') 42 | } 43 | `, 44 | }, 45 | ], 46 | invalid: [ 47 | { 48 | code: ` 49 | ${types} 50 | function func(figma: PluginAPI) { 51 | figma.on('documentchange') 52 | } 53 | `, 54 | errors: [{ messageId: 'advice' }], 55 | }, 56 | { 57 | code: ` 58 | ${types} 59 | function func(figma: PluginAPI) { 60 | figma.once('documentchange') 61 | } 62 | `, 63 | errors: [{ messageId: 'advice' }], 64 | }, 65 | { 66 | code: ` 67 | ${types} 68 | function func(figma: PluginAPI) { 69 | figma.off('documentchange') 70 | } 71 | `, 72 | errors: [{ messageId: 'advice' }], 73 | }, 74 | ], 75 | }) 76 | -------------------------------------------------------------------------------- /test/dynamicPageFindMethodAdvice.test.ts: -------------------------------------------------------------------------------- 1 | import { dynamicPageFindMethodAdvice } from '../src/rules/dynamicPageFindMethodAdvice' 2 | import { ruleTester } from './testUtil' 3 | 4 | const types = ` 5 | interface BaseNode {} 6 | 7 | interface DocumentNode { 8 | findAll() 9 | findAllWithCriteria() 10 | findChild() 11 | findChildren() 12 | findOne() 13 | } 14 | ` 15 | 16 | ruleTester().run('ban-deprecated-sync-methods', dynamicPageFindMethodAdvice, { 17 | valid: [ 18 | { 19 | code: ` 20 | ${types} 21 | function func(notDoc: NotDocumentNode) { 22 | notDoc.findAll() 23 | } 24 | `, 25 | }, 26 | { 27 | code: ` 28 | ${types} 29 | function func(doc: DocumentNode) { 30 | doc.appendChild() 31 | } 32 | `, 33 | }, 34 | ], 35 | invalid: [ 36 | { 37 | code: ` 38 | ${types} 39 | function func(doc: DocumentNode) { 40 | doc.findAll() 41 | } 42 | `, 43 | errors: [{ messageId: 'advice' }], 44 | }, 45 | { 46 | code: ` 47 | ${types} 48 | function func(doc: DocumentNode) { 49 | doc.findOne() 50 | } 51 | `, 52 | errors: [{ messageId: 'advice' }], 53 | }, 54 | ], 55 | }) 56 | -------------------------------------------------------------------------------- /test/fixture/README.md: -------------------------------------------------------------------------------- 1 | This fixture is required for type-aware rule testing. 2 | 3 | See the [typescript-eslint docs](https://typescript-eslint.io/packages/rule-tester/#type-aware-testing) for more. 4 | -------------------------------------------------------------------------------- /test/fixture/file.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/eslint-plugin-figma-plugins/bcdbe359a8e61ce83ca174b33448c72f3bdf978d/test/fixture/file.ts -------------------------------------------------------------------------------- /test/fixture/file.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/eslint-plugin-figma-plugins/bcdbe359a8e61ce83ca174b33448c72f3bdf978d/test/fixture/file.tsx -------------------------------------------------------------------------------- /test/fixture/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true 4 | }, 5 | "include": ["file.ts", "file.tsx"] 6 | } 7 | -------------------------------------------------------------------------------- /test/testUtil.ts: -------------------------------------------------------------------------------- 1 | import { RuleTester } from '@typescript-eslint/rule-tester' 2 | import * as path from 'path' 3 | 4 | export function ruleTester(): RuleTester { 5 | return new RuleTester({ 6 | parser: require.resolve('@typescript-eslint/parser'), 7 | parserOptions: { 8 | tsconfigRootDir: path.join(__dirname, 'fixture'), 9 | project: path.join(__dirname, 'fixture', 'tsconfig.json'), 10 | }, 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "include": ["src/**/*", "test/**/*"], 4 | "compilerOptions": { 5 | // @typescript-eslint/utils requires the use of a modern module resolution strategy. 6 | // See: https://github.com/typescript-eslint/typescript-eslint/issues/7284 7 | "module": "Node16", 8 | "moduleResolution": "Node16", 9 | "outDir": "dist", 10 | 11 | // We emit declarations so that importing projects can use 'node_modules/@figma' as a type root. 12 | // If we don't emit types, TypeScript will complain that this directory does not export any types. 13 | "declaration": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /vscode-quickfix.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/figma/eslint-plugin-figma-plugins/bcdbe359a8e61ce83ca174b33448c72f3bdf978d/vscode-quickfix.gif --------------------------------------------------------------------------------