├── .node-version ├── pnpm-workspace.yaml ├── tests └── fixtures │ ├── file.ts │ ├── react.tsx │ └── tsconfig.json ├── packages ├── internal │ ├── index.ts │ ├── package.json │ └── eslint-plugin-tester.ts ├── eslint-config-sukka │ ├── src │ │ ├── foximport.d.ts │ │ ├── index.ts │ │ ├── is-in-editor.ts │ │ ├── foxquire.ts │ │ ├── modules │ │ │ ├── regexp.ts │ │ │ ├── eslint-comment.ts │ │ │ ├── promise.ts │ │ │ ├── legacy.ts │ │ │ ├── ignores.ts │ │ │ └── _generated_typescript_overrides.ts │ │ └── deprecate.ts │ ├── rollup.config.ts │ ├── package.json │ └── scripts │ │ └── codegen.ts ├── eslint-plugin-stylistic │ ├── src │ │ └── index.ts │ ├── rollup.config.ts │ └── package.json ├── react │ ├── src │ │ ├── index.ts │ │ ├── next.ts │ │ └── stylex.ts │ ├── rollup.config.ts │ └── package.json ├── shared │ ├── rollup.config.ts │ ├── src │ │ ├── types.ts │ │ ├── globals.ts │ │ ├── memoize-eslint-plugin.ts │ │ ├── constants.ts │ │ ├── create-eslint-rule.ts │ │ ├── get-package-json.ts │ │ └── index.ts │ └── package.json ├── eslint-formatter-sukka │ ├── rollup.config.ts │ ├── src │ │ └── ansi-escape.ts │ └── package.json ├── node │ ├── rollup.config.ts │ ├── package.json │ └── src │ │ └── index.ts ├── yaml │ ├── rollup.config.ts │ ├── package.json │ └── src │ │ └── index.ts ├── eslint-plugin-sukka-full │ ├── rollup.config.ts │ └── package.json ├── eslint-plugin-sukka │ ├── rollup.config.ts │ ├── src │ │ └── rules │ │ │ ├── ban-eslint-disable │ │ │ ├── index.test.ts │ │ │ ├── index.md │ │ │ └── index.ts │ │ │ ├── no-export-const-enum │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ │ ├── no-expression-empty-lines │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ │ ├── no-chain-array-higher-order-functions │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ │ ├── no-small-switch │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ │ ├── no-useless-plusplus │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ │ ├── no-try-promise │ │ │ └── utils.ts │ │ │ ├── no-useless-string-operation │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ │ ├── bool-param-default │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ │ ├── no-unthrown-error │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ │ ├── class-prototype │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ │ ├── object-format │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ │ ├── no-top-level-this │ │ │ └── index.ts │ │ │ ├── track-todo-fixme-comment │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ │ ├── no-for-in-iterable │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ │ ├── no-same-line-conditional │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ │ ├── comma-or-logical-or-case │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ │ ├── call-argument-line │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ │ ├── only-await-thenable │ │ │ └── index.ts │ │ │ ├── no-equals-in-for-termination │ │ │ └── index.test.ts │ │ │ ├── no-undefined-optional-parameters │ │ │ └── index.ts │ │ │ ├── no-return-await │ │ │ └── index.ts │ │ │ └── prefer-single-boolean-return │ │ │ └── index.ts │ └── package.json ├── rollup-config │ ├── rollup.config.ts │ ├── src │ │ └── rollup-foxquire.ts │ └── package.json └── eslint-plugin-react-jsx-a11y │ ├── rollup.config.ts │ ├── package.json │ └── src │ └── index.ts ├── eslint.config.js ├── .gitignore ├── README.md ├── patches ├── @masknet__eslint-plugin@0.2.0.patch └── eslint-plugin-unicorn.patch ├── tsconfig.json ├── turbo.json ├── LICENSE ├── .github └── workflows │ └── publish.yml └── package.json /.node-version: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /tests/fixtures/file.ts: -------------------------------------------------------------------------------- 1 | // noop for typescript-eslint with projectService 2 | -------------------------------------------------------------------------------- /tests/fixtures/react.tsx: -------------------------------------------------------------------------------- 1 | // noop for typescript-eslint with projectService 2 | -------------------------------------------------------------------------------- /packages/internal/index.ts: -------------------------------------------------------------------------------- 1 | export { runTest } from './eslint-plugin-tester'; 2 | -------------------------------------------------------------------------------- /packages/eslint-config-sukka/src/foximport.d.ts: -------------------------------------------------------------------------------- 1 | declare function foximport(module: string): Promise; 2 | -------------------------------------------------------------------------------- /packages/eslint-config-sukka/src/index.ts: -------------------------------------------------------------------------------- 1 | export { ignores } from './modules/ignores'; 2 | export { constants } from '@eslint-sukka/shared'; 3 | export { sukka } from './factory'; 4 | -------------------------------------------------------------------------------- /packages/eslint-plugin-stylistic/src/index.ts: -------------------------------------------------------------------------------- 1 | import $ from '@stylistic/eslint-plugin'; 2 | import type { ESLint } from 'eslint'; 3 | 4 | export const stylistic_eslint_plugin: ESLint.Plugin = $; 5 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | export { next } from './next'; 2 | 3 | export { react } from './react'; 4 | export type { OptionsReact } from './react'; 5 | 6 | export { stylex } from './stylex'; 7 | export type { OptionsStyleX } from './stylex'; 8 | -------------------------------------------------------------------------------- /tests/fixtures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "preserve", 5 | "strict": true, 6 | "esModuleInterop": true 7 | }, 8 | "include": [ 9 | "*.ts", 10 | "*.tsx" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/shared/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { createRollupConfig } from '@eslint-sukka/rollup-config'; 2 | 3 | export default createRollupConfig(new URL('./package.json', import.meta.url), [], { 4 | nodeResolve: true, 5 | commonjs: true, 6 | json: true 7 | }); 8 | -------------------------------------------------------------------------------- /packages/eslint-formatter-sukka/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { createRollupConfig } from '@eslint-sukka/rollup-config'; 2 | 3 | export default createRollupConfig(new URL('./package.json', import.meta.url), [], { 4 | nodeResolve: true, 5 | commonjs: true, 6 | json: true 7 | }); 8 | -------------------------------------------------------------------------------- /packages/shared/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Linter } from 'eslint'; 2 | 3 | export type FlatESLintConfigItem = Linter.Config; 4 | export type SukkaESLintRuleConfig = Pick; 5 | 6 | export type ESLintRulesRecord = Linter.RulesRecord; 7 | -------------------------------------------------------------------------------- /packages/node/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { createRollupConfig } from '@eslint-sukka/rollup-config'; 2 | 3 | export default createRollupConfig(new URL('./package.json', import.meta.url), [], { 4 | nodeResolve: true, 5 | commonjs: { 6 | ignoreDynamicRequires: true 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /packages/yaml/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { createRollupConfig } from '@eslint-sukka/rollup-config'; 2 | 3 | export default createRollupConfig(new URL('./package.json', import.meta.url), [], { 4 | nodeResolve: true, 5 | commonjs: { 6 | ignoreDynamicRequires: true 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { sukka } = require('eslint-config-sukka'); 4 | 5 | module.exports = sukka( 6 | { 7 | ignores: { 8 | customGlobs: [ 9 | '**/_generated*' 10 | ] 11 | }, 12 | node: true, 13 | react: { 14 | reactCompiler: true 15 | }, 16 | yaml: true 17 | } 18 | ); 19 | -------------------------------------------------------------------------------- /packages/eslint-config-sukka/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { createRollupConfig } from '@eslint-sukka/rollup-config'; 2 | 3 | export default createRollupConfig(new URL('./package.json', import.meta.url), [], { 4 | foxquire: true, 5 | nodeResolve: true, 6 | commonjs: true, 7 | analyze: false, 8 | buildCjsOnly: true, 9 | externalLiveBindings: false 10 | }); 11 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka-full/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { createRollupConfig } from '@eslint-sukka/rollup-config'; 2 | 3 | export default createRollupConfig(new URL('./package.json', import.meta.url), [], { 4 | nodeResolve: true, 5 | commonjs: true, 6 | json: true, 7 | alias: { 8 | entries: [ 9 | { find: 'lodash', replacement: 'lodash-unified' } 10 | ] 11 | }, 12 | buildCjsOnly: true 13 | }); 14 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { createRollupConfig } from '@eslint-sukka/rollup-config'; 2 | 3 | export default createRollupConfig(new URL('./package.json', import.meta.url), [], { 4 | nodeResolve: true, 5 | commonjs: true, 6 | json: true, 7 | alias: { 8 | entries: [ 9 | { find: 'lodash', replacement: 'lodash-unified' } 10 | ] 11 | }, 12 | buildCjsOnly: false 13 | }); 14 | -------------------------------------------------------------------------------- /packages/internal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eslint-sukka/internal", 3 | "version": "8.0.6", 4 | "private": true, 5 | "main": "./index.ts", 6 | "types": "./index.ts", 7 | "exports": { 8 | ".": { 9 | "types": "./index.ts", 10 | "default": "./index.ts" 11 | }, 12 | "./package.json": "./package.json" 13 | }, 14 | "scripts": {}, 15 | "license": "MIT", 16 | "dependencies": {}, 17 | "devDependencies": {} 18 | } 19 | -------------------------------------------------------------------------------- /packages/rollup-config/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { createRollupConfig } from './src'; 2 | 3 | export default createRollupConfig( 4 | new URL('./package.json', import.meta.url), 5 | [ 6 | 'rollup-plugin-swc3', 7 | 'rollup-plugin-dts', 8 | 'rollup', 9 | '@rollup/plugin-node-resolve', 10 | '@rollup/plugin-json', 11 | '@rollup/plugin-commonjs', 12 | 'fs', 13 | '@rollup/plugin-alias', 14 | 'vite-bundle-analyzer' 15 | ] 16 | ); 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | .pnp 4 | .pnp.js 5 | 6 | # testing 7 | coverage 8 | 9 | # next.js 10 | .next/ 11 | out/ 12 | build 13 | 14 | # dist 15 | dist 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # turbo 34 | .turbo 35 | 36 | # vercel 37 | .vercel 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-config-sukka 2 | 3 | ESLint config for [Sukka](https://skk.moe). 4 | 5 | ## License 6 | 7 | [MIT](./LICENSE) 8 | 9 | ## Maintainer 10 | 11 | **eslint-config-sukka** © [Sukka](https://github.com/SukkaW), Released under the [MIT](./LICENSE) License. 12 | 13 | > [Personal Website](https://skk.moe) · [Blog](https://blog.skk.moe) · GitHub [@SukkaW](https://github.com/SukkaW) · Telegram Channel [@SukkaChannel](https://t.me/SukkaChannel) · Twitter [@isukkaw](https://twitter.com/isukkaw) · Keybase [@sukka](https://keybase.io/sukka) 14 | -------------------------------------------------------------------------------- /patches/@masknet__eslint-plugin@0.2.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/package.json b/package.json 2 | index 0fda1b98fa2a8cc95c1032717eb02d8540246194..633519ff2ee06b2b072edf2060565b6f23342576 100644 3 | --- a/package.json 4 | +++ b/package.json 5 | @@ -25,8 +25,8 @@ 6 | "types": "./lib/index.d.ts", 7 | "default": "./lib/index.js" 8 | }, 9 | - "./configs/": "./lib/configs/", 10 | - "./rules/": "./lib/rules/" 11 | + "./configs/*": "./lib/configs/*", 12 | + "./rules/*": "./lib/rules/*" 13 | }, 14 | "files": [ 15 | "src/", 16 | -------------------------------------------------------------------------------- /packages/eslint-plugin-stylistic/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { createRollupConfig } from '@eslint-sukka/rollup-config'; 2 | 3 | export default createRollupConfig(new URL('./package.json', import.meta.url), [], { 4 | buildCjsOnly: true, 5 | // rollup plugins 6 | nodeResolve: true, 7 | commonjs: { 8 | ignoreDynamicRequires: false, 9 | transformMixedEsModules: true 10 | }, 11 | json: true, 12 | analyze: false, 13 | replace: { 14 | values: { 15 | '__require(': 'require(' 16 | }, 17 | delimiters: ['', ''] 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /patches/eslint-plugin-unicorn.patch: -------------------------------------------------------------------------------- 1 | diff --git a/package.json b/package.json 2 | index 0d7a747d3cf66559b1f88eb43be3de08cf718607..f6d6e84a8fc79f6aba621ddd1bc0b8df85a536c0 100644 3 | --- a/package.json 4 | +++ b/package.json 5 | @@ -12,8 +12,12 @@ 6 | }, 7 | "type": "module", 8 | "exports": { 9 | - "types": "./index.d.ts", 10 | - "default": "./index.js" 11 | + ".": { 12 | + "types": "./index.d.ts", 13 | + "default": "./index.js" 14 | + }, 15 | + "./package.json": "./package.json", 16 | + "./rules/*": "./rules/*" 17 | }, 18 | "sideEffects": false, 19 | "engines": { 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": [ 5 | "ESNext" 6 | ], 7 | "module": "preserve", 8 | "moduleResolution": "bundler", 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "preserveSymlinks": true, 12 | "strict": true, 13 | "strictNullChecks": true, 14 | "skipLibCheck": true 15 | }, 16 | "include": [ 17 | "./packages/**/*.ts", 18 | "./lib/**/*.ts" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "**/fixtures/**/*", 23 | "./packages/*/dist/**/*" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/shared/src/globals.ts: -------------------------------------------------------------------------------- 1 | import { 2 | es2025 as globalEs2025, 3 | browser as globalBrowser, 4 | webextensions as globalWebextensions, 5 | greasemonkey as globalGreasemonkey, 6 | node as globalNode 7 | } from 'globals/globals.json'; 8 | 9 | export const es2025: Record = globalEs2025; 10 | export const browser: Record = globalBrowser; 11 | export const webextensions: Record = globalWebextensions; 12 | export const greasemonkey: Record = globalGreasemonkey; 13 | export const node: Record = globalNode; 14 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/ban-eslint-disable/index.test.ts: -------------------------------------------------------------------------------- 1 | import { runTest } from '@eslint-sukka/internal'; 2 | import module from './index'; 3 | 4 | runTest({ 5 | module, 6 | *valid() { 7 | yield '// eslint-disable-next-line no-console -- Log an error\nconsole.log(``)'; 8 | }, 9 | *invalid() { 10 | yield { 11 | code: '// eslint-disable-next-line\nconsole.log()', 12 | errors: [ 13 | { messageId: 'require-description', data: { directive: 'eslint-disable-next-line' } }, 14 | { messageId: 'require-specific-rule' } 15 | ] 16 | }; 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /packages/eslint-config-sukka/src/is-in-editor.ts: -------------------------------------------------------------------------------- 1 | import { isCI } from 'ci-info'; 2 | 3 | export function isInEditorEnv(): boolean { 4 | if (process.env.CI || isCI) return false; 5 | 6 | // is in git hooks or lint-staged 7 | if ( 8 | process.env.GIT_PARAMS 9 | || process.env.VSCODE_GIT_COMMAND 10 | || process.env.npm_lifecycle_script?.startsWith('lint-staged') 11 | ) return false; 12 | 13 | // is in editor 14 | return !!( 15 | process.env.VSCODE_PID 16 | || process.env.VSCODE_CWD 17 | || process.env.JETBRAINS_IDE 18 | || process.env.VIM 19 | || process.env.NVIM 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /packages/react/src/next.ts: -------------------------------------------------------------------------------- 1 | import { constants } from '@eslint-sukka/shared'; 2 | import type { FlatESLintConfigItem } from '@eslint-sukka/shared'; 3 | 4 | import { configs as eslint_plugin_next_flatconfig } from '@next/eslint-plugin-next'; 5 | 6 | export function next(): FlatESLintConfigItem[] { 7 | return [{ 8 | ...eslint_plugin_next_flatconfig['core-web-vitals'], 9 | files: [ 10 | constants.GLOB_TS, 11 | constants.GLOB_TSX, 12 | // constants.GLOB_JS, 13 | constants.GLOB_JSX 14 | ], 15 | rules: { 16 | ...eslint_plugin_next_flatconfig['core-web-vitals'].rules, 17 | '@next/next/no-img-element': 'off' 18 | } 19 | }]; 20 | } 21 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/ban-eslint-disable/index.md: -------------------------------------------------------------------------------- 1 | # `eslint-plugin-sukka/ban-eslint-disable` 2 | 3 | Ban `eslint-disable` comment directive 4 | 5 | ## Rule Details 6 | 7 | see 8 | 9 | TypeScript Support: 10 | 11 | ## Options 12 | 13 | ```ts 14 | /** 15 | * @minItems Infinity 16 | */ 17 | export type Options = [boolean | 'allow-with-description'] 18 | ``` 19 | 20 | ### :x: Incorrect 21 | ```ts 22 | // eslint-disable-next-line 23 | console.log(error) 24 | ``` 25 | ### :white_check_mark: Correct 26 | ```ts 27 | // eslint-disable-next-line no-console -- Log a error 28 | console.log(error) 29 | ``` 30 | -------------------------------------------------------------------------------- /packages/eslint-formatter-sukka/src/ansi-escape.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | 3 | // const ESC = '\u001B['; 4 | const OSC = '\u001B]'; 5 | const BEL = '\u0007'; 6 | const SEP = ';'; 7 | 8 | const PARAM_SEP = ':'; 9 | const EQ = '='; 10 | 11 | export function link(text: string, url: string, params: Record = {}) { 12 | return OSC 13 | + '8' 14 | + SEP 15 | + Object.keys(params).map(key => key + EQ + params[key]).join(PARAM_SEP) 16 | + SEP 17 | + url 18 | + BEL 19 | + text 20 | + OSC 21 | + '8' 22 | + SEP 23 | + SEP 24 | + BEL; 25 | } 26 | 27 | export function iTermSetCwd(cwd = process.cwd()) { 28 | return OSC 29 | + '50;' 30 | + 'CurrentDir=' 31 | + cwd 32 | + BEL; 33 | } 34 | -------------------------------------------------------------------------------- /packages/eslint-config-sukka/src/foxquire.ts: -------------------------------------------------------------------------------- 1 | import { isCI } from 'ci-info'; 2 | import { isPackageExists } from '@eslint-sukka/shared'; 3 | import process from 'node:process'; 4 | 5 | export async function foxquire(pkg: string): Promise { 6 | if ( 7 | !isCI 8 | && process.stdout.isTTY 9 | && !isPackageExists(pkg, typeof __dirname === 'string' ? __dirname : import.meta.dirname) 10 | ) { 11 | const { confirm } = await import('@clack/prompts'); 12 | const result = await confirm({ 13 | message: `Package is required for this config: ${pkg}. Do you want to install it?` 14 | }); 15 | if (result) { 16 | await import('@antfu/install-pkg').then(i => i.installPackage(pkg, { dev: true })); 17 | }; 18 | } 19 | 20 | return foximport(pkg); 21 | } 22 | -------------------------------------------------------------------------------- /packages/eslint-config-sukka/src/modules/regexp.ts: -------------------------------------------------------------------------------- 1 | import eslint_plugin_sukka from '@eslint-sukka/eslint-plugin-sukka-full'; 2 | 3 | import { UNSAFE_excludeJsonYamlFiles } from '@eslint-sukka/shared'; 4 | import type { FlatESLintConfigItem } from '@eslint-sukka/shared'; 5 | import { configs as eslint_pluin_regexp_configs } from 'eslint-plugin-regexp'; 6 | 7 | export function regexp(): FlatESLintConfigItem[] { 8 | // this is safe because JSON/YAML files won't have parsable regexes 9 | return UNSAFE_excludeJsonYamlFiles([ 10 | eslint_plugin_sukka.configs.regexp, 11 | eslint_pluin_regexp_configs['flat/recommended'], 12 | { 13 | name: 'sukka/regexp', 14 | rules: { 15 | 'regexp/strict': 'off' // we accepts Annex B regex from better-regex 16 | } 17 | } 18 | ]); 19 | } 20 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-export-const-enum/index.test.ts: -------------------------------------------------------------------------------- 1 | import module from '.'; 2 | import { runTest } from '@eslint-sukka/internal'; 3 | import { dedent } from 'ts-dedent'; 4 | 5 | runTest({ 6 | module, 7 | *valid() { 8 | yield 'enum E {}'; 9 | yield 'const enum E {}'; 10 | }, 11 | *invalid() { 12 | yield { 13 | code: 'export const enum E {}', 14 | errors: [{ messageId: 'noConstEnum' }] 15 | }; 16 | yield { 17 | code: dedent` 18 | const enum A { 19 | MB = 'MiB' 20 | }; 21 | export const A; 22 | `, 23 | errors: [{ messageId: 'noConstEnum' }] 24 | }; 25 | yield { 26 | code: dedent` 27 | const enum A { 28 | MB = 'MiB' 29 | }; 30 | export default A; 31 | `, 32 | errors: [{ messageId: 'noConstEnum' }] 33 | }; 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-expression-empty-lines/index.test.ts: -------------------------------------------------------------------------------- 1 | import { runTest } from '@eslint-sukka/internal'; 2 | import module from '.'; 3 | import { dedent } from 'ts-dedent'; 4 | 5 | runTest({ 6 | module, 7 | *valid() { 8 | yield dedent` 9 | const result = [] 10 | .map(x => x) 11 | .map(x => x); 12 | `; 13 | yield dedent` 14 | const result = [] 15 | // comment 16 | .map(x => x) // comment 17 | .map(x => x); 18 | `; 19 | }, 20 | *invalid() { 21 | yield { 22 | code: dedent` 23 | const result = [] 24 | 25 | .map(x => x) 26 | 27 | 28 | .map(x => x); 29 | `, 30 | output: dedent` 31 | const result = [] 32 | .map(x => x) 33 | .map(x => x); 34 | `, 35 | errors: [ 36 | { messageId: 'unexpectedEmptyLine' }, 37 | { messageId: 'unexpectedEmptyLine' } 38 | ] 39 | }; 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /packages/eslint-plugin-stylistic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eslint-sukka/eslint-plugin-stylistic", 3 | "version": "8.0.6", 4 | "homepage": "https://github.com/SukkaW/eslint-config-sukka", 5 | "repository": { 6 | "url": "https://github.com/SukkaW/eslint-config-sukka", 7 | "directory": "packages/eslint-plugin-stylistic" 8 | }, 9 | "main": "dist/index.cjs", 10 | "types": "dist/index.d.ts", 11 | "files": [ 12 | "dist" 13 | ], 14 | "exports": { 15 | ".": { 16 | "types": "./dist/index.d.ts", 17 | "require": "./dist/index.cjs", 18 | "default": "./dist/index.cjs" 19 | }, 20 | "./package.json": "./package.json" 21 | }, 22 | "scripts": { 23 | "build": "rollup -c rollup.config.ts --configPlugin swc3" 24 | }, 25 | "license": "MIT", 26 | "devDependencies": { 27 | "@eslint-sukka/rollup-config": "workspace:*", 28 | "@stylistic/eslint-plugin": "^5.6.1" 29 | }, 30 | "peerDependencies": { 31 | "eslint": "*" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/eslint-config-sukka/src/deprecate.ts: -------------------------------------------------------------------------------- 1 | import { isPackageExists } from '@eslint-sukka/shared'; 2 | import { isCI } from 'ci-info'; 3 | import picocolors from 'picocolors'; 4 | 5 | export async function deprecate(pkg: string) { 6 | if (!isPackageExists(pkg, typeof __dirname === 'string' ? __dirname : import.meta.dirname)) { 7 | return; 8 | } 9 | 10 | // eslint-disable-next-line no-console -- in cli warn 11 | console.error(picocolors.yellow(`[eslint-config-sukka] "${pkg}" is deprecated and you should uninstall it`)); 12 | 13 | if (isCI || !process.stdout.isTTY) { 14 | throw new Error(`[eslint-config-sukka] "${pkg}" is deprecated and you should uninstall it`); 15 | } 16 | 17 | const { confirm } = await import('@clack/prompts'); 18 | const result = await confirm({ 19 | message: ` "${pkg}" is deprecated. Do you want to uninstall it?` 20 | }); 21 | if (result) { 22 | await import('@antfu/install-pkg').then(i => i.uninstallPackage(pkg, { dev: true })); 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/rollup-config/src/rollup-foxquire.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'rollup'; 2 | 3 | import { MagicString } from '@napi-rs/magic-string'; 4 | 5 | const CJSShim = ` 6 | // -- CommonJS Shims -- 7 | const foximport = (id) => Promise.resolve(require(id)); 8 | `; 9 | const ESMShim = ` 10 | // -- ESM Shims -- 11 | const foximport = (id) => import(id); 12 | `; 13 | 14 | export function rollupFoximport(): Plugin { 15 | return { 16 | name: 'esm-cjs-bridge', 17 | renderChunk(code, _chunk, opts) { 18 | if (code.includes('foximport')) { 19 | const ms = new MagicString(code); 20 | if (opts.format === 'es') { 21 | if (!code.includes(ESMShim)) { 22 | ms.prepend(ESMShim); 23 | } 24 | } else if (!code.includes(CJSShim)) { 25 | ms.prepend(CJSShim); 26 | } 27 | return { 28 | code: ms.toString(), 29 | map: ms.generateMap({ hires: true }).toMap() 30 | }; 31 | } 32 | return null; 33 | } 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": [ 4 | "**/.env.*local", 5 | "tsconfig.json", 6 | "eslint.config.js" 7 | ], 8 | "tasks": { 9 | "codegen": { 10 | "dependsOn": [ 11 | "^codegen" 12 | ] 13 | }, 14 | "build": { 15 | "dependsOn": [ 16 | "^build" 17 | ], 18 | "outputs": [ 19 | "dist/**" 20 | ] 21 | }, 22 | "test": { 23 | "cache": false 24 | }, 25 | "eslint-config-sukka#codegen": { 26 | "dependsOn": [ 27 | "@eslint-sukka/shared#build", 28 | "@eslint-sukka/eslint-plugin-sukka-full#build", 29 | "@eslint-sukka/eslint-plugin-stylistic#build" 30 | ] 31 | }, 32 | "eslint-config-sukka#build": { 33 | "dependsOn": [ 34 | "eslint-config-sukka#codegen", 35 | "@eslint-sukka/react#build" 36 | ] 37 | }, 38 | "//#lint:root": { 39 | "dependsOn": [ 40 | "eslint-config-sukka#build" 41 | ] 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/react/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { createRollupConfig } from '@eslint-sukka/rollup-config'; 2 | 3 | export default createRollupConfig(new URL('./package.json', import.meta.url), ['eslint-plugin-import', 'eslint-plugin-import-x'], { 4 | buildCjsOnly: true, 5 | nodeResolve: true, 6 | commonjs: { 7 | ignoreDynamicRequires: true 8 | }, 9 | json: true, 10 | alias: { 11 | entries: { 12 | 'es-iterator-helpers': '@nolyfill/es-iterator-helpers', 13 | 'array-includes': '@nolyfill/array-includes', 14 | 'array.prototype.tosorted': '@nolyfill/array.prototype.tosorted', 15 | 'array.prototype.flat': '@nolyfill/array.prototype.flat', 16 | 'array.prototype.flatmap': '@nolyfill/array.prototype.flatmap', 17 | 'string.prototype.matchall': '@nolyfill/string.prototype.matchall', 18 | 'object.values': '@nolyfill/object.values', 19 | 'object.assign': '@nolyfill/object.assign', 20 | 'object.entries': '@nolyfill/object.entries', 21 | 'object.fromentries': '@nolyfill/object.fromentries', 22 | 'object.hasown': '@nolyfill/object.hasown' 23 | } 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /packages/shared/src/memoize-eslint-plugin.ts: -------------------------------------------------------------------------------- 1 | import type { ESLint } from 'eslint'; 2 | 3 | declare global { 4 | // eslint-disable-next-line vars-on-top -- fuck 5 | var __ESLINT_PLUGIN_MEMO__: Record | undefined; 6 | } 7 | 8 | /** 9 | * Every package manager has this flaw: Even if a pinned, same version of transive dependency 10 | * is depended on by multiple packages, all npm/pnpm/yarn/bun will not dedupe it, some package 11 | * manager even doesn't have dedupe feature (yes, bun. You are literally wasting my disk space 12 | * for speed). 13 | * 14 | * But if there are multiple copy of the same version of transive dependency, they will not have 15 | * the same referential identity, which causes ESLint to panic and throw error. 16 | * 17 | * So we have to memoize the plugins and configs to make sure they are the same referential identity. 18 | */ 19 | export function memo(fn: NonNullable, key: string): T { 20 | globalThis.__ESLINT_PLUGIN_MEMO__ ||= {}; 21 | globalThis.__ESLINT_PLUGIN_MEMO__[key] ||= fn; 22 | return globalThis.__ESLINT_PLUGIN_MEMO__[key] as T; 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sukka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/eslint-config-sukka/src/modules/eslint-comment.ts: -------------------------------------------------------------------------------- 1 | import type { FlatESLintConfigItem } from '@eslint-sukka/shared'; 2 | // @ts-expect-error -- no types 3 | import eslint_plugin_eslint_comments from '@eslint-community/eslint-plugin-eslint-comments'; 4 | import eslint_plugin_sukka from '@eslint-sukka/eslint-plugin-sukka-full'; 5 | 6 | export function comment(): FlatESLintConfigItem[] { 7 | return [ 8 | eslint_plugin_sukka.configs.comment, 9 | { 10 | name: 'sukka/eslint-comments', 11 | plugins: { 12 | '@eslint-community/eslint-comments': eslint_plugin_eslint_comments 13 | }, 14 | rules: { 15 | '@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }], 16 | '@eslint-community/eslint-comments/no-aggregating-enable': 'error', 17 | '@eslint-community/eslint-comments/no-duplicate-disable': 'error', 18 | '@eslint-community/eslint-comments/no-unlimited-disable': 'error', 19 | '@eslint-community/eslint-comments/no-unused-disable': 'error', 20 | '@eslint-community/eslint-comments/no-unused-enable': 'error' 21 | } 22 | } 23 | ]; 24 | } 25 | -------------------------------------------------------------------------------- /packages/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eslint-sukka/node", 3 | "version": "8.0.6", 4 | "description": "Sukka's ESLint config", 5 | "homepage": "https://github.com/SukkaW/eslint-config-sukka", 6 | "repository": { 7 | "url": "https://github.com/SukkaW/eslint-config-sukka", 8 | "directory": "packages/node" 9 | }, 10 | "main": "dist/index.cjs", 11 | "module": "dist/index.mjs", 12 | "types": "dist/index.d.ts", 13 | "files": [ 14 | "dist" 15 | ], 16 | "exports": { 17 | ".": { 18 | "types": "./dist/index.d.ts", 19 | "import": "./dist/index.mjs", 20 | "require": "./dist/index.cjs", 21 | "default": "./dist/index.cjs" 22 | }, 23 | "./package.json": "./package.json" 24 | }, 25 | "scripts": { 26 | "build": "rollup -c rollup.config.ts --configPlugin swc3" 27 | }, 28 | "keywords": [ 29 | "eslint-config" 30 | ], 31 | "author": "Sukka ", 32 | "license": "MIT", 33 | "dependencies": { 34 | "@eslint-sukka/eslint-plugin-sukka-full": "workspace:*", 35 | "@eslint-sukka/shared": "workspace:*", 36 | "eslint-plugin-n": "^17.23.1" 37 | }, 38 | "devDependencies": { 39 | "@eslint-sukka/rollup-config": "workspace:*" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/yaml/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eslint-sukka/yaml", 3 | "version": "8.0.6", 4 | "description": "Sukka's ESLint config", 5 | "homepage": "https://github.com/SukkaW/eslint-config-sukka", 6 | "repository": { 7 | "url": "https://github.com/SukkaW/eslint-config-sukka", 8 | "directory": "packages/yaml" 9 | }, 10 | "main": "dist/index.cjs", 11 | "module": "dist/index.mjs", 12 | "types": "dist/index.d.ts", 13 | "files": [ 14 | "dist" 15 | ], 16 | "exports": { 17 | ".": { 18 | "types": "./dist/index.d.ts", 19 | "import": "./dist/index.mjs", 20 | "require": "./dist/index.cjs", 21 | "default": "./dist/index.cjs" 22 | }, 23 | "./package.json": "./package.json" 24 | }, 25 | "scripts": { 26 | "build": "rollup -c rollup.config.ts --configPlugin swc3" 27 | }, 28 | "keywords": [ 29 | "eslint-config" 30 | ], 31 | "author": "Sukka ", 32 | "license": "MIT", 33 | "dependencies": { 34 | "@eslint-sukka/shared": "workspace:*", 35 | "eslint-plugin-yml": "^1.19.0", 36 | "foxts": "^5.0.3", 37 | "yaml-eslint-parser": "^1.3.2" 38 | }, 39 | "devDependencies": { 40 | "@eslint-sukka/rollup-config": "workspace:*" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eslint-sukka/shared", 3 | "version": "8.0.6", 4 | "description": "Sukka's ESLint config", 5 | "homepage": "https://github.com/SukkaW/eslint-config-sukka", 6 | "repository": { 7 | "url": "https://github.com/SukkaW/eslint-config-sukka", 8 | "directory": "packages/shared" 9 | }, 10 | "main": "dist/index.cjs", 11 | "module": "dist/index.mjs", 12 | "types": "dist/index.d.ts", 13 | "files": [ 14 | "dist" 15 | ], 16 | "exports": { 17 | ".": { 18 | "types": "./dist/index.d.ts", 19 | "import": "./dist/index.mjs", 20 | "require": "./dist/index.cjs", 21 | "default": "./dist/index.cjs" 22 | }, 23 | "./package.json": "./package.json" 24 | }, 25 | "scripts": { 26 | "build": "rollup -c rollup.config.ts --configPlugin swc3" 27 | }, 28 | "keywords": [ 29 | "eslint-config" 30 | ], 31 | "author": "Sukka ", 32 | "license": "MIT", 33 | "dependencies": { 34 | "@package-json/types": "^0.0.12", 35 | "@typescript-eslint/utils": "^8.49.0", 36 | "foxts": "^5.0.3", 37 | "oxc-resolver": "^11.15.0" 38 | }, 39 | "devDependencies": { 40 | "@eslint-sukka/rollup-config": "workspace:*", 41 | "globals": "^16.5.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka-full/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eslint-sukka/eslint-plugin-sukka-full", 3 | "version": "8.0.6", 4 | "homepage": "https://github.com/SukkaW/eslint-config-sukka", 5 | "repository": { 6 | "url": "https://github.com/SukkaW/eslint-config-sukka", 7 | "directory": "packages/eslint-plugin-sukka-full" 8 | }, 9 | "main": "dist/index.cjs", 10 | "types": "dist/index.d.ts", 11 | "files": [ 12 | "dist" 13 | ], 14 | "exports": { 15 | ".": { 16 | "types": "./dist/index.d.ts", 17 | "require": "./dist/index.cjs", 18 | "default": "./dist/index.cjs" 19 | }, 20 | "./package.json": "./package.json" 21 | }, 22 | "scripts": { 23 | "test": "mocha --require @swc-node/register src/rules/**/*.test.ts", 24 | "build": "rollup -c rollup.config.ts --configPlugin swc3" 25 | }, 26 | "license": "MIT", 27 | "dependencies": { 28 | "eslint-plugin-sukka": "workspace:*" 29 | }, 30 | "devDependencies": { 31 | "@eslint-sukka/rollup-config": "workspace:*", 32 | "@masknet/eslint-plugin": "^0.4.1", 33 | "eslint-plugin-unicorn": "^62.0.0" 34 | }, 35 | "peerDependencies": { 36 | "eslint": "*", 37 | "typescript": "*" 38 | }, 39 | "peerDependenciesMeta": { 40 | "typescript": { 41 | "optional": true 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/eslint-formatter-sukka/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-formatter-sukka", 3 | "version": "8.0.6", 4 | "description": "Sukka's ESLint config", 5 | "homepage": "https://github.com/SukkaW/eslint-config-sukka", 6 | "repository": { 7 | "url": "https://github.com/SukkaW/eslint-config-sukka", 8 | "directory": "packages/formatter-sukka" 9 | }, 10 | "main": "dist/index.cjs", 11 | "module": "dist/index.mjs", 12 | "types": "dist/index.d.ts", 13 | "files": [ 14 | "dist" 15 | ], 16 | "exports": { 17 | ".": { 18 | "types": "./dist/index.d.ts", 19 | "import": "./dist/index.mjs", 20 | "require": "./dist/index.cjs", 21 | "default": "./dist/index.cjs" 22 | }, 23 | "./package.json": "./package.json" 24 | }, 25 | "scripts": { 26 | "build": "rollup -c rollup.config.ts --configPlugin swc3" 27 | }, 28 | "keywords": [ 29 | "eslint-config" 30 | ], 31 | "author": "Sukka ", 32 | "license": "MIT", 33 | "dependencies": { 34 | "ci-info": "^4.3.1", 35 | "foxts": "^5.0.3", 36 | "picocolors": "^1.1.1" 37 | }, 38 | "devDependencies": { 39 | "@eslint-sukka/rollup-config": "workspace:*", 40 | "fast-string-width": "^3.0.2", 41 | "supports-hyperlinks": "^4.3.0" 42 | }, 43 | "peerDependencies": { 44 | "eslint": "*" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-chain-array-higher-order-functions/index.test.ts: -------------------------------------------------------------------------------- 1 | import { runTest } from '@eslint-sukka/internal'; 2 | import module from '.'; 3 | import { dedent } from 'ts-dedent'; 4 | 5 | runTest({ 6 | module, 7 | valid: [ 8 | '[].reduce(() => {}, 0);', 9 | '[].map(() => {});', 10 | '[].filter(() => {});', 11 | '[].reduce(() => {}, 0).sort();', 12 | '[].filter(() => {}).every(() => true);' 13 | ], 14 | invalid: [ 15 | { 16 | code: '[].map(() => {}).filter(() => {}, 0);', 17 | errors: [{ 18 | messageId: 'detected' 19 | }] 20 | }, 21 | { 22 | code: '[].filter(() => {}).map(() => {}, 0);', 23 | errors: [{ 24 | messageId: 'detected' 25 | }] 26 | }, 27 | { 28 | code: dedent` 29 | [] 30 | .map(() => {}) 31 | .reduce(() => {}, 0); 32 | `, 33 | errors: [{ 34 | messageId: 'detected' 35 | }] 36 | }, 37 | { 38 | code: dedent` 39 | arr 40 | .reduce(() => {}, 0) 41 | .map(() => {}); 42 | `, 43 | errors: [{ 44 | messageId: 'detected' 45 | }] 46 | }, 47 | { 48 | code: dedent` 49 | arr 50 | .map(() => {}) 51 | .filter(() => {}, 0); 52 | `, 53 | errors: [{ 54 | messageId: 'detected' 55 | }] 56 | } 57 | ] 58 | }); 59 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Automatic Publish 2 | on: 3 | push: 4 | branches: 5 | - master 6 | tags-ignore: 7 | - '**' 8 | paths-ignore: 9 | - '**/*.md' 10 | - LICENSE 11 | - '**/*.gitignore' 12 | - .editorconfig 13 | - docs/** 14 | pull_request: null 15 | jobs: 16 | publish: 17 | name: Publish 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: read 21 | id-token: write 22 | steps: 23 | - uses: actions/checkout@v5 24 | - uses: pnpm/action-setup@v4 25 | - uses: actions/setup-node@v5 26 | with: 27 | node-version-file: '.node-version' 28 | check-latest: true 29 | cache: 'pnpm' 30 | registry-url: 'https://registry.npmjs.org' 31 | - run: npm install -g npm 32 | - run: pnpm install 33 | - run: pnpm run build 34 | - run: pnpm run lint 35 | - name: Publish 36 | run: | 37 | if git log -1 --pretty=%B | grep "^release: [0-9]\+\.[0-9]\+\.[0-9]\+$"; 38 | then 39 | pnpm -r publish --provenance --access public 40 | elif git log -1 --pretty=%B | grep "^release: [0-9]\+\.[0-9]\+\.[0-9]\+"; 41 | then 42 | pnpm -r publish --provenance --access public --tag next 43 | else 44 | echo "Not a release, skipping publish" 45 | fi 46 | env: 47 | NPM_CONFIG_PROVENANCE: true 48 | -------------------------------------------------------------------------------- /packages/shared/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const GLOB_TS = '**/*.?([cm])ts'; 2 | export const GLOB_TSX = '**/*.?([cm])tsx'; 3 | export const GLOB_JS = '**/*.?([cm])js'; 4 | export const GLOB_JSX = '**/*.?([cm])jsx'; 5 | 6 | export const GLOB_SRC_EXT = '?([cm])[jt]s?(x)'; 7 | export const GLOB_SRC = '**/*.?([cm])[jt]s?(x)'; 8 | 9 | export const GLOB_JSON = '**/*.json'; 10 | export const GLOB_JSONC = '**/*.jsonc'; 11 | export const GLOB_JSON5 = '**/*.json5'; 12 | export const GLOB_ALL_JSON = [GLOB_JSON, GLOB_JSONC, GLOB_JSON5]; 13 | 14 | export const GLOB_YML = ['*.yaml', '**/*.yaml', '*.yml', '**/*.yml']; 15 | 16 | export const GLOB_MARKDOWN = '**/*.md'; 17 | 18 | export const GLOB_TESTS = [ 19 | `**/__tests__/**/*.${GLOB_SRC_EXT}`, 20 | `**/*.spec.${GLOB_SRC_EXT}`, 21 | `**/*.test.${GLOB_SRC_EXT}` 22 | ]; 23 | 24 | export const GLOB_EXCLUDE = [ 25 | '**/node_modules', 26 | '**/dist', 27 | 28 | '**/output', 29 | '**/out', 30 | '**/coverage', 31 | '**/temp', 32 | '**/tmp', 33 | '**/.vitepress/cache', 34 | '**/.next', 35 | '**/.nuxt', 36 | '**/.vercel', 37 | '**/.changeset', 38 | '**/.idea', 39 | '**/.output', 40 | '**/.vite-inspect', 41 | 42 | '**/*.md', // '**/CHANGELOG*.md', 43 | '**/*.min.*', 44 | '**/__snapshots__', 45 | '**/fixtures', 46 | '**/auto-import?(s).d.ts', 47 | '**/components.d.ts', 48 | 49 | '**/next-env.d.ts', // codegen by Next.js, not for eslint 50 | 51 | // @eslint-sukka/yaml 52 | '**/pnpm-lock.yaml' 53 | ]; 54 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-sukka", 3 | "version": "8.0.6", 4 | "homepage": "https://github.com/SukkaW/eslint-config-sukka", 5 | "repository": { 6 | "url": "https://github.com/SukkaW/eslint-config-sukka", 7 | "directory": "packages/eslint-plugin-sukka" 8 | }, 9 | "main": "dist/index.cjs", 10 | "module": "dist/index.mjs", 11 | "types": "dist/index.d.ts", 12 | "files": [ 13 | "dist" 14 | ], 15 | "exports": { 16 | ".": { 17 | "types": "./dist/index.d.ts", 18 | "import": "./dist/index.mjs", 19 | "require": "./dist/index.cjs", 20 | "default": "./dist/index.cjs" 21 | }, 22 | "./package.json": "./package.json" 23 | }, 24 | "scripts": { 25 | "test": "mocha --require @swc-node/register src/rules/**/*.test.ts", 26 | "build": "rollup -c rollup.config.ts --configPlugin swc3" 27 | }, 28 | "license": "MIT", 29 | "dependencies": { 30 | "@eslint-sukka/shared": "workspace:*", 31 | "@typescript-eslint/type-utils": "^8.49.0", 32 | "@typescript-eslint/types": "^8.49.0", 33 | "@typescript-eslint/utils": "^8.49.0", 34 | "foxts": "^5.0.3" 35 | }, 36 | "devDependencies": { 37 | "@eslint-sukka/rollup-config": "workspace:*", 38 | "@masknet/eslint-plugin": "^0.4.1" 39 | }, 40 | "peerDependencies": { 41 | "eslint": "*", 42 | "typescript": "*" 43 | }, 44 | "peerDependenciesMeta": { 45 | "typescript": { 46 | "optional": true 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/rollup-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eslint-sukka/rollup-config", 3 | "version": "8.0.6", 4 | "private": true, 5 | "description": "Sukka's ESLint config", 6 | "homepage": "https://github.com/SukkaW/eslint-config-sukka", 7 | "repository": { 8 | "url": "https://github.com/SukkaW/eslint-config-sukka", 9 | "directory": "packages/rollup-config" 10 | }, 11 | "main": "dist/index.cjs", 12 | "module": "dist/index.mjs", 13 | "types": "dist/index.d.ts", 14 | "files": [ 15 | "dist" 16 | ], 17 | "exports": { 18 | ".": { 19 | "types": "./dist/index.d.ts", 20 | "import": "./dist/index.mjs", 21 | "require": "./dist/index.cjs", 22 | "default": "./dist/index.cjs" 23 | }, 24 | "./package.json": "./package.json" 25 | }, 26 | "scripts": { 27 | "build": "rollup -c rollup.config.ts --configPlugin swc3" 28 | }, 29 | "keywords": [ 30 | "eslint-config" 31 | ], 32 | "author": "Sukka ", 33 | "license": "MIT", 34 | "dependencies": { 35 | "@napi-rs/magic-string": "^0.3.4", 36 | "@package-json/types": "^0.0.12", 37 | "@rollup/plugin-alias": "^5.1.1", 38 | "@rollup/plugin-commonjs": "^28.0.9", 39 | "@rollup/plugin-json": "^6.1.0", 40 | "@rollup/plugin-node-resolve": "^16.0.3", 41 | "@rollup/plugin-replace": "^6.0.3", 42 | "rollup": "^4.53.3", 43 | "rollup-plugin-dts": "^6.3.0", 44 | "rollup-plugin-swc3": "^0.12.1", 45 | "vite-bundle-analyzer": "^1.3.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/eslint-plugin-react-jsx-a11y/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { createRollupConfig } from '@eslint-sukka/rollup-config'; 2 | 3 | export default createRollupConfig(new URL('./package.json', import.meta.url), [ 4 | 'eslint' 5 | ], { 6 | buildCjsOnly: true, 7 | // rollup plugins 8 | nodeResolve: true, 9 | commonjs: { 10 | ignoreDynamicRequires: true 11 | }, 12 | json: true, 13 | alias: { 14 | entries: { 15 | 'es-iterator-helpers': '@nolyfill/es-iterator-helpers', 16 | 'array-includes': '@nolyfill/array-includes', 17 | 'array.prototype.tosorted': '@nolyfill/array.prototype.tosorted', 18 | 'array.prototype.flat': '@nolyfill/array.prototype.flat', 19 | 'array.prototype.flatmap': '@nolyfill/array.prototype.flatmap', 20 | 'string.prototype.matchall': '@nolyfill/string.prototype.matchall', 21 | 'object.values': '@nolyfill/object.values', 22 | 'object.assign': '@nolyfill/object.assign', 23 | 'object.entries': '@nolyfill/object.entries', 24 | 'object.fromentries/polyfill': '@nolyfill/object.fromentries/polyfill', 25 | 'object.fromentries': '@nolyfill/object.fromentries', 26 | 'object.hasown/polyfill': '@nolyfill/object.hasown/polyfill', 27 | 'object.hasown': '@nolyfill/object.hasown', 28 | hasown: '@nolyfill/hasown', 29 | 'safe-regex-test': '@nolyfill/safe-regex-test', 30 | 'string.prototype.includes': '@nolyfill/string.prototype.includes', 31 | 'deep-equal': '@nolyfill/deep-equal' 32 | } 33 | }, 34 | analyze: false 35 | }); 36 | -------------------------------------------------------------------------------- /packages/eslint-config-sukka/src/modules/promise.ts: -------------------------------------------------------------------------------- 1 | import { UNSAFE_excludeJsonYamlFiles } from '@eslint-sukka/shared'; 2 | import type { FlatESLintConfigItem } from '@eslint-sukka/shared'; 3 | // @ts-expect-error -- no types available 4 | import eslint_plugin_promise from 'eslint-plugin-promise'; 5 | 6 | export interface OptionsPromise { 7 | typescript: boolean 8 | } 9 | 10 | export function promise({ typescript }: OptionsPromise): FlatESLintConfigItem[] { 11 | return [ 12 | // this is safe because JSON and YAML won't have promises 13 | UNSAFE_excludeJsonYamlFiles({ 14 | name: 'sukka/promise', 15 | plugins: { 16 | promise: eslint_plugin_promise 17 | }, 18 | rules: { 19 | 'promise/always-return': typescript 20 | ? 'off' 21 | : ['error', { ignoreLastCallback: true }], 22 | 'promise/no-return-wrap': 'error', 23 | 'promise/param-names': 'error', 24 | 'promise/catch-or-return': [ 25 | 'error', 26 | { 27 | allowFinally: true, 28 | terminationMethod: ['catch', 'asCallback', 'finally'] 29 | } 30 | ], 31 | 'promise/no-native': 'off', 32 | 'promise/no-nesting': 'warn', 33 | 'promise/no-promise-in-callback': 'warn', 34 | 'promise/no-callback-in-promise': 'warn', 35 | 'promise/avoid-new': 'off', 36 | 'promise/no-new-statics': 'error', 37 | 'promise/no-return-in-finally': 'warn', 38 | 'promise/valid-params': 'warn', 39 | 'promise/prefer-catch': 'error' 40 | } 41 | }) 42 | ]; 43 | } 44 | -------------------------------------------------------------------------------- /packages/eslint-plugin-react-jsx-a11y/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eslint-sukka/eslint-plugin-react-jsx-a11y", 3 | "version": "8.0.6", 4 | "homepage": "https://github.com/SukkaW/eslint-config-sukka", 5 | "repository": { 6 | "url": "https://github.com/SukkaW/eslint-config-sukka", 7 | "directory": "packages/eslint-plugin-react-jsx-a11y" 8 | }, 9 | "main": "dist/index.cjs", 10 | "types": "dist/index.d.ts", 11 | "files": [ 12 | "dist" 13 | ], 14 | "exports": { 15 | ".": { 16 | "types": "./dist/index.d.ts", 17 | "require": "./dist/index.cjs", 18 | "default": "./dist/index.cjs" 19 | }, 20 | "./package.json": "./package.json" 21 | }, 22 | "scripts": { 23 | "build": "rollup -c rollup.config.ts --configPlugin swc3" 24 | }, 25 | "license": "MIT", 26 | "dependencies": { 27 | "@nolyfill/array-includes": "^1.0.44", 28 | "@nolyfill/array.prototype.flat": "^1.0.44", 29 | "@nolyfill/array.prototype.flatmap": "^1.0.44", 30 | "@nolyfill/array.prototype.tosorted": "^1.0.44", 31 | "@nolyfill/deep-equal": "^1.0.44", 32 | "@nolyfill/es-iterator-helpers": "^1.0.21", 33 | "@nolyfill/hasown": "^1.0.44", 34 | "@nolyfill/object.assign": "^1.0.44", 35 | "@nolyfill/object.fromentries": "^1.0.44", 36 | "@nolyfill/object.hasown": "^1.0.44", 37 | "@nolyfill/object.values": "^1.0.44", 38 | "@nolyfill/safe-regex-test": "^1.0.44", 39 | "@nolyfill/string.prototype.includes": "^1.0.44" 40 | }, 41 | "devDependencies": { 42 | "@eslint-sukka/rollup-config": "workspace:*", 43 | "eslint-plugin-jsx-a11y": "^6.10.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eslint-sukka/react", 3 | "version": "8.0.6", 4 | "description": "Sukka's ESLint config", 5 | "homepage": "https://github.com/SukkaW/eslint-config-sukka", 6 | "repository": { 7 | "url": "https://github.com/SukkaW/eslint-config-sukka", 8 | "directory": "packages/react" 9 | }, 10 | "main": "dist/index.cjs", 11 | "types": "dist/index.d.ts", 12 | "files": [ 13 | "dist" 14 | ], 15 | "exports": { 16 | ".": { 17 | "types": "./dist/index.d.ts", 18 | "require": "./dist/index.cjs", 19 | "default": "./dist/index.cjs" 20 | }, 21 | "./package.json": "./package.json" 22 | }, 23 | "scripts": { 24 | "build": "rollup -c rollup.config.ts --configPlugin swc3" 25 | }, 26 | "keywords": [ 27 | "eslint-config" 28 | ], 29 | "author": "Sukka ", 30 | "license": "MIT", 31 | "dependencies": { 32 | "@eslint-sukka/eslint-plugin-react-jsx-a11y": "workspace:*", 33 | "@eslint-sukka/eslint-plugin-stylistic": "workspace:*", 34 | "@eslint-sukka/shared": "workspace:*", 35 | "@eslint/compat": "^2.0.0", 36 | "@next/eslint-plugin-next": "^16.0.10", 37 | "@stylexjs/eslint-plugin": "^0.17.3", 38 | "eslint-plugin-react-compiler": "19.1.0-rc.2", 39 | "eslint-plugin-react-hooks": "^7.0.1", 40 | "eslint-plugin-react-refresh": "^0.4.24", 41 | "eslint-plugin-ssr-friendly": "^1.3.0", 42 | "foxts": "^5.0.3" 43 | }, 44 | "devDependencies": { 45 | "@eslint-react/eslint-plugin": "^2.3.13", 46 | "@eslint-sukka/rollup-config": "workspace:*", 47 | "eslint-plugin-react-prefer-function-component": "^5.0.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/yaml/src/index.ts: -------------------------------------------------------------------------------- 1 | import { constants } from '@eslint-sukka/shared'; 2 | import type { FlatESLintConfigItem } from '@eslint-sukka/shared'; 3 | import eslint_plugin_yml from 'eslint-plugin-yml'; 4 | import { appendArrayInPlace } from 'foxts/append-array-in-place'; 5 | 6 | export function yaml(): FlatESLintConfigItem[] { 7 | const myCfg: FlatESLintConfigItem[] = [ 8 | { 9 | name: 'sukka/yaml disable spaced-comment', 10 | files: constants.GLOB_YML, 11 | rules: { 12 | // FIXME: https://github.com/ota-meshi/eslint-plugin-yml/issues/277 13 | '@stylistic/spaced-comment': 'off' 14 | } 15 | }, 16 | { 17 | files: ['pnpm-workspace.yaml'], 18 | name: 'sukka/yaml/pnpm-workspace', 19 | rules: { 20 | 'yml/sort-keys': [ 21 | 'error', 22 | { 23 | order: [ 24 | 'packages', 25 | 'overrides', 26 | 'patchedDependencies', 27 | 'hoistPattern', 28 | 'catalog', 29 | 'catalogs', 30 | 31 | 'allowedDeprecatedVersions', 32 | 'allowNonAppliedPatches', 33 | 'configDependencies', 34 | 'ignoredBuiltDependencies', 35 | 'ignoredOptionalDependencies', 36 | 'neverBuiltDependencies', 37 | 'onlyBuiltDependencies', 38 | 'onlyBuiltDependenciesFile', 39 | 'packageExtensions', 40 | 'peerDependencyRules', 41 | 'supportedArchitectures' 42 | ], 43 | pathPattern: '^$' 44 | } 45 | ] 46 | } 47 | } 48 | ]; 49 | return appendArrayInPlace(myCfg, eslint_plugin_yml.configs['flat/recommended']); 50 | } 51 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-expression-empty-lines/index.ts: -------------------------------------------------------------------------------- 1 | import { createRule } from '@eslint-sukka/shared'; 2 | import type { RuleFix } from '@typescript-eslint/utils/ts-eslint'; 3 | import { detectEol } from 'foxts/detect-eol'; 4 | 5 | export type MessageId = 'unexpectedEmptyLine'; 6 | 7 | export default createRule({ 8 | name: 'no-expression-empty-lines', 9 | meta: { 10 | messages: { unexpectedEmptyLine: 'Unexpected empty line before' }, 11 | docs: { 12 | description: 'Disallows empty lines inside expressions.' 13 | }, 14 | fixable: 'whitespace', 15 | type: 'suggestion', 16 | schema: [] 17 | }, 18 | create(context) { 19 | const code = context.sourceCode.getText(); 20 | const eol = detectEol(code); 21 | 22 | return { 23 | MemberExpression(node) { 24 | const pos = node.object.range[1]; 25 | 26 | const got = leadingSpaces(code.slice(pos)); 27 | 28 | if (got.includes('\n')) { 29 | const expected = eol + trimLeadingEmptyLines(got); 30 | 31 | if (got !== expected) { 32 | context.report({ 33 | fix(): RuleFix { 34 | return { 35 | range: [pos, pos + got.length], 36 | text: expected 37 | }; 38 | }, 39 | messageId: 'unexpectedEmptyLine', 40 | node: node.property 41 | }); 42 | } 43 | } 44 | } 45 | }; 46 | } 47 | }); 48 | 49 | function leadingSpaces(str: string): string { 50 | return str.slice(0, str.length - str.trimStart().length); 51 | } 52 | 53 | function trimLeadingEmptyLines(str: string): string { 54 | const leadingLines = leadingSpaces(str).split(/\r\n|\n/u); 55 | 56 | return (leadingLines.at(-1) ?? '') + str.trimStart(); 57 | } 58 | -------------------------------------------------------------------------------- /packages/eslint-config-sukka/src/modules/legacy.ts: -------------------------------------------------------------------------------- 1 | import { globals } from '@eslint-sukka/shared'; 2 | import type { FlatESLintConfigItem } from '@eslint-sukka/shared'; 3 | 4 | export interface OptionsLegacy { 5 | browser?: boolean, 6 | node?: boolean, 7 | files?: FlatESLintConfigItem['files'] 8 | } 9 | 10 | export function legacy(options: OptionsLegacy = {}): FlatESLintConfigItem[] { 11 | return [{ 12 | name: '@eslint-sukka/legacy base', 13 | ...(options.files ? { files: options.files } : {}), 14 | // only turn off rules 15 | // plugins: { 16 | // sukka: memo(eslint_plugin_sukka, '@eslint-sukka/eslint-plugin-sukka-full') 17 | // }, 18 | rules: { 19 | 'prefer-numeric-literals': 'off', 20 | 'no-restricted-properties': ['error', { 21 | object: 'arguments', 22 | property: 'callee', 23 | message: 'arguments.callee is deprecated' 24 | }, { 25 | property: '__defineGetter__', 26 | message: 'Please use Object.defineProperty instead.' 27 | }, { 28 | property: '__defineSetter__', 29 | message: 'Please use Object.defineProperty instead.' 30 | }], 31 | 'no-var': 'off', 32 | 'prefer-object-spread': 'off', 33 | strict: ['error', 'safe'], 34 | 35 | // default parameters is not supported in legacy environment 36 | 'sukka/unicorn/prefer-default-parameters': 'off', // function foo(bar = 1) {} 37 | // nullable logical operator is not supported in legacy environment 38 | 'sukka/unicorn/prefer-logical-operator-over-ternary': 'off', // foo ? foo : bar 39 | // optional catch binding is not supported in legacy environment 40 | 'sukka/unicorn/prefer-optional-catch-binding': 'off' // try {} catch {} 41 | }, 42 | languageOptions: { 43 | globals: { 44 | ...((options.browser ?? true) ? globals.browser : {}), 45 | ...((options.node ?? false) ? globals.node : {}) 46 | } 47 | } 48 | }]; 49 | } 50 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-chain-array-higher-order-functions/index.ts: -------------------------------------------------------------------------------- 1 | import { createRule } from '@eslint-sukka/shared'; 2 | import { AST_NODE_TYPES } from '@typescript-eslint/types'; 3 | import type { TSESTree } from '@typescript-eslint/types'; 4 | 5 | const ARRAY_HIGH_ORDER_FUNCTIONS = new Set([ 6 | 'map', 7 | 'filter', 8 | 'reduce', 9 | 'reduceRight', 10 | 'forEach' 11 | ]); 12 | 13 | export default createRule({ 14 | name: 'no-chain-array-higher-order-functions', 15 | meta: { 16 | type: 'problem', 17 | fixable: 'code', 18 | docs: { 19 | description: 'Prefer `.reduce` over chaining `.filter`, `.map` methods', 20 | recommended: 'recommended' 21 | }, 22 | schema: [], 23 | messages: { 24 | detected: 'Detected the chaining of array methods: {{methods}}. Reaplce with `.reduce` to reduce array iterations and improve the performance.' 25 | } 26 | }, 27 | create(context) { 28 | return { 29 | MemberExpression(node) { 30 | if (isArrayHigherOrderFunction(node)) { 31 | const parent = node.parent as TSESTree.CallExpression; 32 | if (isArrayHigherOrderFunction(parent.parent)) { 33 | context.report({ 34 | node: parent, 35 | messageId: 'detected', 36 | data: { 37 | methods: `arr.${(node.property as TSESTree.Identifier).name}().${(parent.parent.property as TSESTree.Identifier).name}()` 38 | } 39 | }); 40 | } 41 | } 42 | } 43 | }; 44 | } 45 | }); 46 | 47 | function isArrayHigherOrderFunction(node: TSESTree.Node): node is TSESTree.MemberExpressionNonComputedName { 48 | if (node.type !== AST_NODE_TYPES.MemberExpression) { 49 | return false; 50 | } 51 | if (node.computed) { 52 | return false; 53 | } 54 | if (node.property.type !== AST_NODE_TYPES.Identifier) { 55 | return false; 56 | } 57 | return ARRAY_HIGH_ORDER_FUNCTIONS.has(node.property.name) && node.parent.type === AST_NODE_TYPES.CallExpression; 58 | } 59 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-small-switch/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | import mod from '.'; 21 | import { runTest } from '@eslint-sukka/internal'; 22 | 23 | runTest({ 24 | module: mod, 25 | valid: [ 26 | { code: 'switch (a) { case 1: case 2: break; default: doSomething(); break; }' }, 27 | { code: 'switch (a) { case 1: break; default: doSomething(); break; case 2: }' }, 28 | { code: 'switch (a) { case 1: break; case 2: }' } 29 | ], 30 | invalid: [ 31 | { 32 | code: 'switch (a) { case 1: doSomething(); break; default: doSomething(); }', 33 | errors: [ 34 | { 35 | messageId: 'replaceSwitch', 36 | column: 1, 37 | endColumn: 7 38 | } 39 | ] 40 | }, 41 | { 42 | code: 'switch (a) { case 1: break; }', 43 | errors: [ 44 | { 45 | messageId: 'replaceSwitch', 46 | column: 1, 47 | endColumn: 7 48 | } 49 | ] 50 | }, 51 | { 52 | code: 'switch (a) {}', 53 | errors: [ 54 | { 55 | messageId: 'replaceSwitch', 56 | column: 1, 57 | endColumn: 7 58 | } 59 | ] 60 | } 61 | ] 62 | }); 63 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-small-switch/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | // https://sonarsource.github.io/rspec/#/rspec/S1301 21 | 22 | import { createRule } from '@eslint-sukka/shared'; 23 | import type { TSESTree } from '@typescript-eslint/types'; 24 | 25 | export default createRule({ 26 | name: 'no-small-switch', 27 | meta: { 28 | schema: [], 29 | messages: { 30 | replaceSwitch: 'Replace this "switch" statement by "if" statements to increase readability.' 31 | }, 32 | type: 'suggestion', 33 | docs: { 34 | description: '"if" statements should be preferred over "switch" when simpler', 35 | recommended: 'recommended', 36 | url: 'https://sonarsource.github.io/rspec/#/rspec/S1301/javascript' 37 | } 38 | }, 39 | create(context) { 40 | return { 41 | SwitchStatement(node: TSESTree.SwitchStatement) { 42 | const { cases } = node; 43 | const hasDefault = cases.some(x => !x.test); 44 | if (cases.length < 2 || (cases.length === 2 && hasDefault)) { 45 | const firstToken = context.sourceCode.getFirstToken(node) as TSESTree.Token | null; 46 | if (firstToken) { 47 | context.report({ 48 | messageId: 'replaceSwitch', 49 | loc: firstToken.loc 50 | }); 51 | } 52 | } 53 | } 54 | }; 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-export-const-enum/index.ts: -------------------------------------------------------------------------------- 1 | import { createRule } from '@eslint-sukka/shared'; 2 | import { AST_NODE_TYPES } from '@typescript-eslint/types'; 3 | import { ASTUtils } from '@typescript-eslint/utils'; 4 | import type { TSESTree } from '@typescript-eslint/types'; 5 | 6 | export default createRule({ 7 | name: 'no-export-const-enum', 8 | meta: { 9 | type: 'suggestion', 10 | docs: { 11 | description: 12 | 'Disallow using `const enum` expression as it can not be inlined and tree-shaken by swc/esbuild/babel/webpack/rollup/vite/bun/rspack' 13 | // recommended: 'recommended' 14 | }, 15 | messages: { 16 | noConstEnum: 'Do not use `const enum` expression' 17 | }, 18 | schema: [] 19 | }, 20 | 21 | create(context) { 22 | const reportTsEnumDeclarationInExportDeclaration = ( 23 | node: TSESTree.ExportDefaultDeclaration | TSESTree.ExportNamedDeclaration, 24 | id: TSESTree.Identifier 25 | ) => { 26 | const variable = ASTUtils.findVariable( 27 | context.sourceCode.getScope(node), 28 | id 29 | ); 30 | variable?.defs.forEach((def) => { 31 | if (def.node.type === AST_NODE_TYPES.TSEnumDeclaration && def.node.const) { 32 | context.report({ 33 | node, 34 | messageId: 'noConstEnum' 35 | }); 36 | } 37 | }); 38 | }; 39 | 40 | return { 41 | ExportNamedDeclaration(node) { 42 | const decl = node.declaration; 43 | if (decl?.type === AST_NODE_TYPES.TSEnumDeclaration && decl.const) { 44 | context.report({ 45 | node, 46 | messageId: 'noConstEnum' 47 | }); 48 | return; 49 | } 50 | if (decl?.type === AST_NODE_TYPES.VariableDeclaration) { 51 | const id = decl.declarations[0].id; 52 | if (id.type === AST_NODE_TYPES.Identifier) { 53 | reportTsEnumDeclarationInExportDeclaration(node, id); 54 | } 55 | } 56 | }, 57 | ExportDefaultDeclaration(node) { 58 | const decl = node.declaration; 59 | if (decl.type === AST_NODE_TYPES.Identifier) { 60 | reportTsEnumDeclarationInExportDeclaration(node, decl); 61 | } 62 | } 63 | }; 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /packages/eslint-config-sukka/src/modules/ignores.ts: -------------------------------------------------------------------------------- 1 | import { constants } from '@eslint-sukka/shared'; 2 | import type { FlatESLintConfigItem } from '@eslint-sukka/shared'; 3 | import eslint_config_flat_gitignore from 'eslint-config-flat-gitignore'; 4 | import { never } from 'foxts/guard'; 5 | 6 | import { globalIgnores } from '@eslint/config-helpers'; 7 | 8 | export interface OptionsIgnores { 9 | /** 10 | * If `customGlobs` is not provided, or provided one is set to `false` or `null`, only the built-in globs will be used. 11 | * 12 | * If `customGlobs` is `string` or `string[]`, it will be appended to the built-in globs. 13 | * 14 | * If `customGlobs` is `function`, it will be called with the built-in globs as the first argument, and the return value will be used as the final globs. 15 | * This is the only way to disable the built-in globs. 16 | */ 17 | customGlobs?: string | string[] | null | false | ((builtinGlobs: typeof constants.GLOB_EXCLUDE) => string[]), 18 | gitignore?: string | string[] | boolean | null 19 | } 20 | 21 | export function ignores(options: OptionsIgnores = {}): FlatESLintConfigItem[] { 22 | const { 23 | customGlobs = null, 24 | gitignore = ['.gitignore'] 25 | } = options; 26 | const configs: FlatESLintConfigItem[] = []; 27 | 28 | let ignores: string[] = []; 29 | if (customGlobs === false || customGlobs === null) { 30 | ignores = constants.GLOB_EXCLUDE; 31 | } else if (typeof customGlobs === 'function') { 32 | ignores = customGlobs(constants.GLOB_EXCLUDE); 33 | } else if (typeof customGlobs === 'string') { 34 | ignores.push(...constants.GLOB_EXCLUDE, customGlobs); 35 | } else if (Array.isArray(customGlobs)) { 36 | ignores.push(...constants.GLOB_EXCLUDE, ...customGlobs); 37 | } else { 38 | never(customGlobs); 39 | } 40 | 41 | configs.push(globalIgnores(ignores, '@eslint-sukka global ignores')); 42 | 43 | if (gitignore === false || gitignore === null) { 44 | // do nothing 45 | } else if (gitignore === true) { 46 | configs.push(eslint_config_flat_gitignore({ files: ['.gitignore'], strict: false })); 47 | } else if (typeof gitignore === 'string' || Array.isArray(gitignore)) { 48 | configs.push(eslint_config_flat_gitignore({ files: gitignore, strict: false })); 49 | } else { 50 | never(gitignore); 51 | } 52 | 53 | return configs; 54 | } 55 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-useless-plusplus/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | import mod from '.'; 21 | import { runTest } from '@eslint-sukka/internal'; 22 | 23 | runTest({ 24 | module: mod, 25 | valid: [ 26 | { 27 | code: 'i = j++;' 28 | }, 29 | { 30 | code: 'i = ++i;' 31 | }, 32 | { 33 | code: 'i++;' 34 | }, 35 | { 36 | code: `function f1() { 37 | let i = 1; 38 | i++; 39 | }` 40 | }, 41 | { 42 | code: `let outside = 1; 43 | function f1() { 44 | return outside++; 45 | }` 46 | }, 47 | { 48 | code: `function f1() { 49 | let i = 1; 50 | return ++i; 51 | }` 52 | } 53 | ], 54 | invalid: [ 55 | { 56 | code: 'i = i++;', 57 | errors: [ 58 | { 59 | messageId: 'removeIncrement', 60 | line: 1, 61 | endLine: 1, 62 | column: 5, 63 | endColumn: 8 64 | } 65 | ] 66 | }, 67 | { 68 | code: 'i = i--; ', 69 | errors: [ 70 | { 71 | messageId: 'removeIncrement' 72 | } 73 | ] 74 | }, 75 | { 76 | code: `function f1() { 77 | let i = 1; 78 | return i++; 79 | }`, 80 | errors: [ 81 | { 82 | messageId: 'removeIncrement' 83 | } 84 | ] 85 | } 86 | ] 87 | }); 88 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-try-promise/utils.ts: -------------------------------------------------------------------------------- 1 | import type { RuleContext } from '@eslint-sukka/shared'; 2 | import { AST_NODE_TYPES } from '@typescript-eslint/types'; 3 | import type { TSESTree } from '@typescript-eslint/types'; 4 | import type { TSESLint } from '@typescript-eslint/utils'; 5 | 6 | type CallLikeExpression = 7 | | TSESTree.CallExpression 8 | | TSESTree.NewExpression 9 | | TSESTree.AwaitExpression; 10 | export class CallLikeExpressionVisitor> { 11 | private readonly callLikeExpressions: CallLikeExpression[] = []; 12 | 13 | static readonly getCallExpressions = >(node: TSESTree.Node, context: TRuleContext) => { 14 | const visitor = new CallLikeExpressionVisitor(); 15 | visitor.visit(node, context); 16 | return visitor.callLikeExpressions; 17 | }; 18 | 19 | protected visit(root: TSESTree.Node, context: TRuleContext) { 20 | const visitNode = (node: TSESTree.Node) => { 21 | switch (node.type) { 22 | case AST_NODE_TYPES.AwaitExpression: 23 | case AST_NODE_TYPES.CallExpression: 24 | case AST_NODE_TYPES.NewExpression: 25 | this.callLikeExpressions.push(node); 26 | break; 27 | case AST_NODE_TYPES.FunctionDeclaration: 28 | case AST_NODE_TYPES.FunctionExpression: 29 | case AST_NODE_TYPES.ArrowFunctionExpression: 30 | return; 31 | default: 32 | // noop 33 | } 34 | 35 | childrenOf(node, context.sourceCode.visitorKeys).forEach(visitNode); 36 | }; 37 | visitNode(root); 38 | } 39 | } 40 | 41 | function childrenOf(node: TSESTree.Node, visitorKeys: TSESLint.Parser.VisitorKeys): TSESTree.Node[] { 42 | const children = []; 43 | const keys = visitorKeys[node.type] as readonly string[] | undefined; 44 | 45 | if (keys?.length) { 46 | for (const key of keys) { 47 | /** 48 | * A node's child may be a node or an array of nodes, e.g., `body` in `estree.Program`. 49 | * If it's an array, we extract all the nodes from it; if not, we just add the node. 50 | */ 51 | const child = node[key as keyof typeof node]; 52 | if (Array.isArray(child)) { 53 | children.push(...child); 54 | } else { 55 | children.push(child); 56 | } 57 | } 58 | } 59 | 60 | return children.filter(Boolean) as TSESTree.Node[]; 61 | } 62 | -------------------------------------------------------------------------------- /packages/react/src/stylex.ts: -------------------------------------------------------------------------------- 1 | import { constants } from '@eslint-sukka/shared'; 2 | import type { FlatESLintConfigItem } from '@eslint-sukka/shared'; 3 | import type { ESLint } from 'eslint'; 4 | 5 | export interface OptionsStyleX { 6 | opt?: StyleXESLintOptions 7 | } 8 | 9 | interface StyleXESLintOptions { 10 | // Possible strings where you can import stylex from 11 | // 12 | // Default: ['@stylexjs/stylex'] 13 | validImports?: string[], 14 | 15 | // Custom limits for values of various properties 16 | propLimits?: PropLimits, 17 | 18 | // @deprecated 19 | // Allow At Rules and Pseudo Selectors outside of 20 | // style values. 21 | // 22 | // Default: false 23 | allowOuterPseudoAndMedia?: boolean, 24 | 25 | // @deprecated 26 | // Disallow properties that are known to break 27 | // in 'legacy-expand-shorthands' style resolution mode. 28 | // 29 | // Default: false 30 | banPropsForLegacy?: boolean 31 | } 32 | 33 | interface PropLimits { 34 | // The property name as a string or a glob pattern 35 | [propertyName: string]: { 36 | limit: 37 | // Disallow the property 38 | | null 39 | // Allow any string value 40 | | 'string' 41 | // Allow any number value 42 | | 'number' 43 | // Any string other 'string' or 'number' 44 | // will be considered to be a valid constant 45 | // e.g. 'red' or '100px'. 46 | | string 47 | // You can also provide numeric constants 48 | // e.g. 100 or 0.5 49 | | number 50 | // You can also provide an array of valid 51 | // number or string constant values. 52 | | Array, 53 | // The error message to show when a value doesn't 54 | // conform to the provided restriction. 55 | reason: string 56 | } 57 | } 58 | 59 | export async function stylex({ opt = {} }: OptionsStyleX = {}): Promise { 60 | const stylex_eslint_plugin = (await import('@stylexjs/eslint-plugin')).default; 61 | 62 | return [{ 63 | plugins: { 64 | '@stylexjs': stylex_eslint_plugin as object as ESLint.Plugin 65 | }, 66 | files: [ 67 | constants.GLOB_TS, 68 | constants.GLOB_TSX, 69 | // constants.GLOB_JS, 70 | constants.GLOB_JSX 71 | ], 72 | rules: { 73 | '@stylexjs/valid-styles': ['error', opt], 74 | '@stylexjs/valid-shorthands': ['error', opt], 75 | '@stylexjs/sort-keys': 'off' 76 | } 77 | }]; 78 | } 79 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/ban-eslint-disable/index.ts: -------------------------------------------------------------------------------- 1 | import { createRule } from '@eslint-sukka/shared'; 2 | 3 | type Options = boolean | 'allow-with-description'; 4 | 5 | const rDirective = /^\s*(?eslint-disable(?:-(?:next-)?line)?)/; 6 | 7 | function getDirective(comment: string) { 8 | const matched = rDirective.exec(comment); 9 | return matched?.groups?.directive; 10 | } 11 | 12 | const rRuleId = /^eslint-disable(?:-next-line|-line)?(?$|(?:\s+(?:@(?:[\w-]+\/){1,2})?[\w-]+)?)/; 13 | 14 | const matchNotFound = Symbol('rule-id-no-match'); 15 | 16 | function getRuleId(comment: string) { 17 | const matched = rRuleId.exec(comment.trim()); 18 | if (!matched) return matchNotFound; 19 | return matched.groups?.ruleId; 20 | } 21 | 22 | export default createRule({ 23 | name: 'ban-eslint-disable', 24 | meta: { 25 | type: 'problem', 26 | docs: { 27 | description: 'Ban `eslint-disable` comment directive' 28 | }, 29 | schema: [{ oneOf: [{ type: 'boolean' }, { type: 'string', enum: ['allow-with-description'] }] }], 30 | messages: { 31 | 'do-not-use': 'Do not use `{{directive}}`', 32 | 'require-description': 33 | 'Include a description after the `{{directive}}` directive to explain why the `{{directive}}` is necessary.', 34 | 'require-specific-rule': 'Enforce specifying rules to disable in `eslint-disable` comments. If you want to disable ESLint on a file altogether, you should ignore it through ESLint configuration.' 35 | } 36 | }, 37 | create(context, options: Options = 'allow-with-description') { 38 | if (options === false) return {}; 39 | return { 40 | Program(program) { 41 | if (!program.comments || program.comments.length === 0) return; 42 | 43 | for (const comment of program.comments) { 44 | const directive = getDirective(comment.value); 45 | if (directive && (options !== 'allow-with-description' || !comment.value.includes('--'))) { 46 | const messageId = options === 'allow-with-description' ? 'require-description' : 'do-not-use'; 47 | context.report({ node: comment, data: { directive }, messageId }); 48 | } 49 | const ruleId = getRuleId(comment.value); 50 | if (ruleId !== matchNotFound && !ruleId) { 51 | context.report({ node: comment, messageId: 'require-specific-rule' }); 52 | } 53 | } 54 | } 55 | }; 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /packages/eslint-config-sukka/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-sukka", 3 | "version": "8.0.6", 4 | "description": "Sukka's ESLint config", 5 | "homepage": "https://github.com/SukkaW/eslint-config-sukka", 6 | "repository": { 7 | "url": "https://github.com/SukkaW/eslint-config-sukka", 8 | "directory": "packages/eslint-config-sukka" 9 | }, 10 | "main": "dist/index.cjs", 11 | "types": "dist/index.d.ts", 12 | "files": [ 13 | "dist" 14 | ], 15 | "exports": { 16 | ".": { 17 | "types": "./dist/index.d.ts", 18 | "require": "./dist/index.cjs", 19 | "default": "./dist/index.cjs" 20 | }, 21 | "./package.json": "./package.json" 22 | }, 23 | "scripts": { 24 | "codegen": "SWC_NODE_IGNORE_DYNAMIC=true node -r @swc-node/register scripts/codegen.ts", 25 | "build": "rollup -c rollup.config.ts --configPlugin swc3" 26 | }, 27 | "keywords": [ 28 | "eslint-config" 29 | ], 30 | "author": "Sukka ", 31 | "license": "MIT", 32 | "dependencies": { 33 | "@antfu/install-pkg": "^1.1.0", 34 | "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0", 35 | "@eslint-sukka/eslint-plugin-stylistic": "workspace:*", 36 | "@eslint-sukka/eslint-plugin-sukka-full": "workspace:*", 37 | "@eslint-sukka/shared": "workspace:*", 38 | "@eslint/config-helpers": "^0.5.0", 39 | "@eslint/js": "^9.39.2", 40 | "@typescript-eslint/eslint-plugin": "^8.49.0", 41 | "@typescript-eslint/parser": "^8.49.0", 42 | "ci-info": "^4.3.1", 43 | "defu": "^6.1.4", 44 | "eslint-import-resolver-typescript": "^4.4.4", 45 | "eslint-plugin-autofix": "^2.2.0", 46 | "eslint-plugin-import-x": "^4.16.1", 47 | "eslint-plugin-jsonc": "^2.21.0", 48 | "eslint-plugin-paths": "^1.1.0", 49 | "eslint-plugin-promise": "^7.2.1", 50 | "eslint-plugin-regexp": "^2.10.0", 51 | "eslint-plugin-unused-imports": "^4.3.0", 52 | "foxts": "^5.0.3", 53 | "jsonc-eslint-parser": "^2.4.2", 54 | "picocolors": "^1.1.1", 55 | "typescript-eslint": "^8.49.0" 56 | }, 57 | "devDependencies": { 58 | "@clack/prompts": "^0.11.0", 59 | "@eslint-sukka/node": "workspace:*", 60 | "@eslint-sukka/react": "workspace:*", 61 | "@eslint-sukka/rollup-config": "workspace:*", 62 | "@eslint-sukka/yaml": "workspace:*", 63 | "eslint-config-flat-gitignore": "^2.1.0", 64 | "eslint-plugin-antfu": "^3.1.1", 65 | "eslint-plugin-de-morgan": "^2.0.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-useless-string-operation/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | import mod from '.'; 21 | import { runTest } from '@eslint-sukka/internal'; 22 | 23 | runTest({ 24 | module: mod, 25 | valid: [ 26 | { 27 | code: 'let res = \'hello\'.toUpperCase();' 28 | }, 29 | { 30 | code: 'let res = \'hello\'.substr(1, 2).toUpperCase();' 31 | }, 32 | { 33 | code: ` 34 | let str = 'hello'; 35 | let res = str.toUpperCase(); 36 | ` 37 | }, 38 | { 39 | code: '\'hello\'[\'whatever\']();' 40 | } 41 | ], 42 | invalid: [ 43 | { 44 | code: '\'hello\'.toUpperCase();', 45 | errors: [ 46 | { 47 | messageId: 'uselessStringOp', 48 | line: 1, 49 | column: 9, 50 | endLine: 1, 51 | endColumn: 20 52 | } 53 | ] 54 | }, 55 | { 56 | code: ` 57 | let str = 'hello'; 58 | str.toUpperCase();`, 59 | errors: [ 60 | { 61 | messageId: 'uselessStringOp', 62 | line: 3, 63 | column: 13, 64 | endLine: 3, 65 | endColumn: 24 66 | } 67 | ] 68 | }, 69 | { 70 | code: ` 71 | let str = 'hello'; 72 | str.toLowerCase().toUpperCase().toLowerCase();`, 73 | errors: [ 74 | { 75 | messageId: 'uselessStringOp', 76 | line: 3, 77 | column: 41, 78 | endLine: 3, 79 | endColumn: 52 80 | } 81 | ] 82 | }, 83 | { 84 | code: '\'hello\'.substr(1, 2).toUpperCase();', 85 | errors: [{ messageId: 'uselessStringOp' }] 86 | } 87 | ] 88 | }); 89 | -------------------------------------------------------------------------------- /packages/shared/src/create-eslint-rule.ts: -------------------------------------------------------------------------------- 1 | import type { TSESLint, ParserServices, ParserServicesWithTypeInformation } from '@typescript-eslint/utils'; 2 | 3 | export type { RuleContext } from '@typescript-eslint/utils/ts-eslint'; 4 | 5 | const BASE_URL = 'https://eslint-plugin.skk.moe/src/rules/'; 6 | 7 | interface Metadata extends TSESLint.RuleMetaData { 9 | hidden?: boolean 10 | } 11 | 12 | export interface RuleModule< 13 | TResolvedOptions, 14 | TOptions extends readonly unknown[], 15 | TMessageIDs extends string, 16 | TMetaDocs = unknown 17 | > { 18 | readonly name: string, 19 | readonly meta: Metadata, 20 | resolveOptions?(this: void, ...options: TOptions): TResolvedOptions, 21 | create(this: void, context: Readonly>, options: TResolvedOptions): TSESLint.RuleListener 22 | } 23 | 24 | export interface ExportedRuleModule< 25 | TOptions extends readonly unknown[] = unknown[], 26 | TMessageIDs extends string = string 27 | > { 28 | readonly name: string, 29 | readonly meta: Metadata, 30 | create(context: Readonly>): TSESLint.RuleListener 31 | } 32 | 33 | export function createRule< 34 | TResolvedOptions, 35 | TOptions extends unknown[], 36 | TMessageIDs extends string, 37 | PluginDocs = unknown 38 | >({ name, meta, create, resolveOptions }: RuleModule): ExportedRuleModule { 39 | if (meta.docs) { 40 | meta.docs.url ??= new URL(name, BASE_URL).toString(); 41 | } 42 | return { 43 | name, 44 | meta, 45 | create(context) { 46 | const options = resolveOptions?.(...context.options) ?? (context.options[0] as TResolvedOptions); 47 | const listener = Object.entries(create(context, options)); 48 | return Object.fromEntries(listener.filter((pair) => pair[1])) as TSESLint.RuleListener; 49 | } 50 | } satisfies ExportedRuleModule; 51 | } 52 | 53 | export function isParserWithTypeInformation( 54 | parserServices: Partial | undefined 55 | ): parserServices is ParserServicesWithTypeInformation { 56 | return !!parserServices?.program; 57 | } 58 | 59 | export function ensureParserWithTypeInformation( 60 | parserServices: Partial | undefined 61 | ): asserts parserServices is ParserServicesWithTypeInformation { 62 | if (!parserServices?.program) { 63 | throw new Error('see https://typescript-eslint.io/docs/linting/type-linting'); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/bool-param-default/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | import { dedent } from 'ts-dedent'; 21 | import mod from '.'; 22 | import { runTest } from '@eslint-sukka/internal'; 23 | 24 | runTest({ 25 | module: mod, 26 | valid: [ 27 | 'function f(b: boolean = false) {}', 28 | 'function f(b: boolean | undefined = true) {}', 29 | 'function f(b: boolean | string) {}', 30 | 'function f(b: boolean & string) {}', 31 | 'function f(b?: string) {}', 32 | dedent` 33 | abstract class A{ 34 | abstract foo(p?: boolean): number; 35 | } 36 | `, 37 | { 38 | code: 'function foo(b?: boolean);' 39 | }, 40 | dedent` 41 | interface i { 42 | m(b?: boolean): void; 43 | new (b?: boolean): void; // Construct signatures can not contain initializer 44 | (b?: boolean): void; // Call signatures can not contain initializer 45 | } 46 | `, 47 | 'type Foo = (p?: boolean) => void; // A parameter initializer is only allowed in a function or constructor implementation' 48 | ], 49 | invalid: [ 50 | { 51 | code: 'function f(b?: boolean) {}', 52 | errors: [ 53 | { 54 | messageId: 'provideDefault', 55 | line: 1, 56 | endLine: 1, 57 | column: 12, 58 | endColumn: 23 59 | } 60 | ] 61 | }, 62 | { 63 | code: 'function f(b: boolean | undefined) {}', 64 | errors: [{ messageId: 'provideDefault' }] 65 | }, 66 | { 67 | code: 'function f(b: undefined | boolean) {}', 68 | errors: [{ messageId: 'provideDefault' }] 69 | }, 70 | { 71 | code: 'let f = (b?: boolean) => b;', 72 | errors: [{ messageId: 'provideDefault' }] 73 | }, 74 | { 75 | code: ` 76 | class c { 77 | m(b?: boolean): void {} 78 | } 79 | `, 80 | errors: [{ messageId: 'provideDefault' }] 81 | } 82 | ] 83 | }); 84 | -------------------------------------------------------------------------------- /packages/eslint-config-sukka/src/modules/_generated_typescript_overrides.ts: -------------------------------------------------------------------------------- 1 | // This file is generated by scripts/codegen.ts 2 | // DO NOT EDIT THIS FILE MANUALLY 3 | import type { SukkaESLintRuleConfig } from '@eslint-sukka/shared'; 4 | 5 | export const generated_typescript_overrides: SukkaESLintRuleConfig = { 6 | rules: { 7 | 'no-dupe-class-members': 'off', 8 | '@typescript-eslint/no-dupe-class-members': 'off', 9 | 'no-redeclare': 'off', 10 | '@typescript-eslint/no-redeclare': 'off', 11 | 'no-unused-private-class-members': 'off', 12 | '@typescript-eslint/no-unused-private-class-members': 'error', 13 | 'no-unused-vars': 'off', 14 | '@typescript-eslint/no-unused-vars': [ 15 | 'error', 16 | { 17 | vars: 'all', 18 | varsIgnorePattern: '^_', 19 | args: 'after-used', 20 | argsIgnorePattern: '^_', 21 | ignoreRestSiblings: true 22 | } 23 | ], 24 | 'class-methods-use-this': 'off', 25 | '@typescript-eslint/class-methods-use-this': [ 26 | 'error', 27 | { 28 | exceptMethods: [] 29 | } 30 | ], 31 | 'default-param-last': 'off', 32 | '@typescript-eslint/default-param-last': 'error', 33 | 'dot-notation': 'off', 34 | '@typescript-eslint/dot-notation': [ 35 | 'error', 36 | { 37 | allowKeywords: true 38 | } 39 | ], 40 | 'no-implied-eval': 'off', 41 | '@typescript-eslint/no-implied-eval': 'error', 42 | 'no-invalid-this': 'off', 43 | '@typescript-eslint/no-invalid-this': 'off', 44 | 'no-loop-func': 'off', 45 | '@typescript-eslint/no-loop-func': 'error', 46 | 'no-unused-expressions': 'off', 47 | '@typescript-eslint/no-unused-expressions': [ 48 | 'error', 49 | { 50 | allowShortCircuit: false, 51 | allowTernary: false, 52 | allowTaggedTemplates: false 53 | } 54 | ], 55 | 'prefer-promise-reject-errors': 'off', 56 | '@typescript-eslint/prefer-promise-reject-errors': [ 57 | 'error', 58 | { 59 | allowEmptyReject: true 60 | } 61 | ], 62 | 'require-await': 'off', 63 | '@typescript-eslint/require-await': 'error', 64 | 'no-empty-function': 'off', 65 | '@typescript-eslint/no-empty-function': 'error', 66 | 'no-useless-constructor': 'off', 67 | '@typescript-eslint/no-useless-constructor': 'error', 68 | camelcase: 'off', 69 | 'no-array-constructor': 'off', 70 | '@typescript-eslint/no-array-constructor': 'error', 71 | 'no-use-before-define': 'off', 72 | '@typescript-eslint/no-use-before-define': [ 73 | 'error', 74 | { 75 | functions: false, 76 | classes: true, 77 | variables: true 78 | } 79 | ], 80 | 'no-restricted-imports': 'off', 81 | 'sukka/no-return-await': 'off', 82 | '@typescript-eslint/return-await': [ 83 | 'error', 84 | 'in-try-catch' 85 | ] 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /packages/eslint-plugin-react-jsx-a11y/src/index.ts: -------------------------------------------------------------------------------- 1 | // import sort_comp from 'eslint-plugin-react/lib/rules/sort-comp'; 2 | import type { Linter } from 'eslint'; 3 | 4 | // @ts-expect-error -- missing types 5 | import alt_text from 'eslint-plugin-jsx-a11y/lib/rules/alt-text'; 6 | // @ts-expect-error -- missing types 7 | import aria_props from 'eslint-plugin-jsx-a11y/lib/rules/aria-props'; 8 | // @ts-expect-error -- missing types 9 | import aria_proptypes from 'eslint-plugin-jsx-a11y/lib/rules/aria-proptypes'; 10 | // @ts-expect-error -- missing types 11 | import aria_role from 'eslint-plugin-jsx-a11y/lib/rules/aria-role'; 12 | // @ts-expect-error -- missing types 13 | import aria_unsupported_elements from 'eslint-plugin-jsx-a11y/lib/rules/aria-unsupported-elements'; 14 | // @ts-expect-error -- missing types 15 | import iframe_has_title from 'eslint-plugin-jsx-a11y/lib/rules/iframe-has-title'; 16 | // @ts-expect-error -- missing types 17 | import no_access_key from 'eslint-plugin-jsx-a11y/lib/rules/no-access-key'; 18 | // @ts-expect-error -- missing types 19 | import role_has_required_aria_props from 'eslint-plugin-jsx-a11y/lib/rules/role-has-required-aria-props'; 20 | // @ts-expect-error -- missing types 21 | import role_supports_aria_props from 'eslint-plugin-jsx-a11y/lib/rules/role-supports-aria-props'; 22 | // @ts-expect-error -- missing types 23 | import tabindex_no_positive from 'eslint-plugin-jsx-a11y/lib/rules/tabindex-no-positive'; 24 | 25 | export const eslint_plugin_jsx_a11y_minimal = { 26 | configs: { 27 | minimal: { 28 | name: '@eslint-sukka/eslint-plugin-react-jsx-a11y minimal preset', 29 | plugins: { 30 | get 'jsx-a11y'() { 31 | return eslint_plugin_jsx_a11y_minimal; 32 | } 33 | }, 34 | rules: { 35 | 'jsx-a11y/alt-text': [ 36 | 'warn', 37 | { 38 | elements: ['img'], 39 | img: ['Image'] 40 | } 41 | ], 42 | 'jsx-a11y/aria-props': 'warn', 43 | 'jsx-a11y/aria-proptypes': 'warn', 44 | 'jsx-a11y/aria-role': 'warn', 45 | 'jsx-a11y/aria-unsupported-elements': 'warn', 46 | 'jsx-a11y/iframe-has-title': 'warn', 47 | 'jsx-a11y/no-access-key': 'warn', 48 | 'jsx-a11y/role-has-required-aria-props': 'warn', 49 | 'jsx-a11y/role-supports-aria-props': 'warn', 50 | 'jsx-a11y/tabindex-no-positive': 'warn' 51 | } as Linter.RulesRecord 52 | } 53 | }, 54 | rules: { 55 | 'alt-text': alt_text, 56 | 'aria-props': aria_props, 57 | 'aria-proptypes': aria_proptypes, 58 | 'aria-role': aria_role, 59 | 'aria-unsupported-elements': aria_unsupported_elements, 60 | 'iframe-has-title': iframe_has_title, 61 | 'no-access-key': no_access_key, 62 | 'role-has-required-aria-props': role_has_required_aria_props, 63 | 'role-supports-aria-props': role_supports_aria_props, 64 | 'tabindex-no-positive': tabindex_no_positive 65 | } 66 | } as const; 67 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-unthrown-error/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | // https://sonarsource.github.io/rspec/#/rspec/S3984/javascript 21 | 22 | import { createRule } from '@eslint-sukka/shared'; 23 | import type { TSESTree } from '@typescript-eslint/types'; 24 | 25 | export default createRule({ 26 | name: 'no-unthrown-error', 27 | meta: { 28 | schema: [], 29 | type: 'problem', 30 | docs: { 31 | description: 'Errors should not be created without being thrown', 32 | recommended: 'recommended', 33 | url: 'https://sonarsource.github.io/rspec/#/rspec/S3984/javascript', 34 | requiresTypeChecking: false 35 | }, 36 | fixable: 'code', 37 | hasSuggestions: true, 38 | messages: { 39 | throwOrRemoveError: 'Throw this error or remove this useless statement.', 40 | suggestThrowError: 'Throw this error' 41 | } 42 | }, 43 | create(context) { 44 | function looksLikeAnError(expression: TSESTree.Expression | TSESTree.Super): boolean { 45 | const text = context.sourceCode.getText(expression); 46 | return text.endsWith('Error') || text.endsWith('Exception'); 47 | } 48 | 49 | function getParent(node: TSESTree.Node) { 50 | const ancestors = context.sourceCode.getAncestors(node); 51 | return ancestors.length > 0 ? ancestors[ancestors.length - 1] : undefined; 52 | } 53 | 54 | return { 55 | 'ExpressionStatement > NewExpression': function (node: TSESTree.NewExpression) { 56 | const expression = node.callee; 57 | if (looksLikeAnError(expression)) { 58 | const parent = getParent(node); 59 | 60 | context.report({ 61 | messageId: 'throwOrRemoveError', 62 | node, 63 | suggest: parent 64 | ? [ 65 | { 66 | messageId: 'suggestThrowError', 67 | fix: fixer => fixer.insertTextBefore(parent, 'throw ') 68 | } 69 | ] 70 | : [] 71 | }); 72 | } 73 | } 74 | }; 75 | } 76 | }); 77 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-unthrown-error/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | import { dedent } from 'ts-dedent'; 21 | import rule from '.'; 22 | import { runTest } from '@eslint-sukka/internal'; 23 | 24 | runTest({ 25 | module: rule, 26 | valid: [ 27 | 'foo(new Error());', 28 | 'foo(TypeError);', 29 | 'throw new Error();', 30 | 'new LooksLikeAnError().doSomething();', 31 | 'const error = new Error();', 32 | 'const error = new Error(); error.something = 10; throw error;' 33 | ], 34 | invalid: [ 35 | { 36 | code: 'new Error();', 37 | errors: [ 38 | { 39 | messageId: 'throwOrRemoveError', 40 | line: 1, 41 | column: 1, 42 | endLine: 1, 43 | endColumn: 12, 44 | suggestions: [ 45 | { 46 | messageId: 'suggestThrowError', 47 | output: 'throw new Error();' 48 | } 49 | ] 50 | } 51 | ] 52 | }, 53 | { 54 | code: 'new TypeError();', 55 | errors: [{ messageId: 'throwOrRemoveError', suggestions: [{ messageId: 'suggestThrowError', output: 'throw new TypeError();' }] }] 56 | }, 57 | { 58 | code: 'new MyError();', 59 | errors: [{ messageId: 'throwOrRemoveError', suggestions: [{ messageId: 'suggestThrowError', output: 'throw new MyError();' }] }] 60 | }, 61 | { 62 | code: 'new A.MyError();', 63 | errors: [{ messageId: 'throwOrRemoveError', suggestions: [{ messageId: 'suggestThrowError', output: 'throw new A.MyError();' }] }] 64 | }, 65 | { 66 | code: dedent` 67 | new A(function () { 68 | new SomeError(); 69 | }); 70 | `, 71 | errors: [{ messageId: 'throwOrRemoveError', suggestions: [{ messageId: 'suggestThrowError', output: dedent` 72 | new A(function () { 73 | throw new SomeError(); 74 | }); 75 | ` }] }] 76 | }, 77 | { 78 | code: '(new MyException());', 79 | errors: [{ messageId: 'throwOrRemoveError', suggestions: [{ messageId: 'suggestThrowError', output: 'throw (new MyException());' }] }] 80 | } 81 | ] 82 | }); 83 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/class-prototype/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | import { describe } from 'mocha'; 21 | import mod from '.'; 22 | import { runTest } from '@eslint-sukka/internal'; 23 | 24 | runTest({ 25 | module: mod, 26 | valid: [ 27 | 'Foo.prototype.property = 1;', 28 | 'Foo.prototype = function () {};', 29 | 'Foo.proto.property = function () {};' 30 | 31 | ], 32 | invalid: [ 33 | { 34 | code: 'Foo.prototype.property = function () {};', 35 | errors: [ 36 | { 37 | messageId: 'declareClass', 38 | data: { 39 | class: 'Foo', 40 | declaration: 'property' 41 | }, 42 | line: 1, 43 | endLine: 1, 44 | column: 1, 45 | endColumn: 23 46 | } 47 | ] 48 | }, 49 | { 50 | code: ` 51 | const Bar = () => {}; 52 | Foo.prototype.property = () => {};`, 53 | errors: 1 54 | } 55 | ] 56 | }); 57 | 58 | describe('Class methods should be used instead of "prototype" assignments [ts]', () => { 59 | runTest({ 60 | module: mod, 61 | valid: [ 62 | { 63 | code: 'Foo.prototype.property = 1;' 64 | }, 65 | { 66 | code: 'Foo.prototype.property = Bar;' 67 | } 68 | ], 69 | invalid: [ 70 | { 71 | code: 'Foo.prototype.property = function () {};', 72 | errors: [ 73 | { 74 | messageId: 'declareClass', 75 | data: { 76 | class: 'Foo', 77 | declaration: 'property' 78 | }, 79 | line: 1, 80 | endLine: 1, 81 | column: 1, 82 | endColumn: 23 83 | } 84 | ] 85 | }, 86 | { 87 | code: ` 88 | function Bar() {} 89 | Foo.prototype.property = Bar;`, 90 | errors: 1 91 | }, 92 | { 93 | code: ` 94 | const Bar = () => {}; 95 | Foo.prototype.property = Bar;`, 96 | errors: 1 97 | } 98 | ] 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-useless-string-operation/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | // https://sonarsource.github.io/rspec/#/rspec/S1154/javascript 21 | 22 | import * as ts from 'typescript'; 23 | import { createRule, ensureParserWithTypeInformation } from '@eslint-sukka/shared'; 24 | import { AST_NODE_TYPES } from '@typescript-eslint/types'; 25 | import type { TSESTree } from '@typescript-eslint/types'; 26 | import { getTypeFromTreeNode } from '../no-for-in-iterable'; 27 | 28 | export default createRule({ 29 | name: 'no-useless-string-operation', 30 | meta: { 31 | schema: [], 32 | messages: { 33 | uselessStringOp: 34 | '{{symbol}} is an immutable object; you must either store or return the result of the operation.' 35 | }, 36 | type: 'problem', 37 | docs: { 38 | description: 'Results of operations on strings should not be ignored', 39 | recommended: 'recommended', 40 | url: 'https://sonarsource.github.io/rspec/#/rspec/S1154/javascript' 41 | } 42 | }, 43 | create(context) { 44 | const services = context.sourceCode.parserServices; 45 | 46 | function isString(node: TSESTree.Node) { 47 | ensureParserWithTypeInformation(services); 48 | const type = getTypeFromTreeNode(node, services); 49 | return (type.flags & ts.TypeFlags.StringLike) !== 0; 50 | } 51 | 52 | function getVariable(node: TSESTree.Node) { 53 | let variable = context.sourceCode.getText(node); 54 | if (variable.length > 30) { 55 | variable = 'String'; 56 | } 57 | return variable; 58 | } 59 | 60 | return { 61 | 'ExpressionStatement > CallExpression[callee.type="MemberExpression"]': ( 62 | node: TSESTree.Node 63 | ) => { 64 | const { object, property } = (node as TSESTree.CallExpression) 65 | .callee as TSESTree.MemberExpression; 66 | if (isString(object) && property.type === AST_NODE_TYPES.Identifier) { 67 | context.report({ 68 | messageId: 'uselessStringOp', 69 | data: { 70 | symbol: getVariable(object) 71 | }, 72 | node: property 73 | }); 74 | } 75 | } 76 | }; 77 | } 78 | }); 79 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/object-format/index.test.ts: -------------------------------------------------------------------------------- 1 | import { runTest } from '@eslint-sukka/internal'; 2 | import module from '.'; 3 | import type { MessageIds, Options } from '.'; 4 | import { dedent } from 'ts-dedent'; 5 | 6 | runTest<[Options], MessageIds>({ 7 | module, 8 | valid: [ 9 | { 10 | code: dedent` 11 | const obj = { // Comment 12 | x: 1 13 | }; 14 | ` 15 | }, 16 | { 17 | code: dedent` 18 | const obj = { 19 | // Comment 20 | x: 1 21 | }; 22 | ` 23 | }, 24 | { 25 | code: dedent` 26 | const obj = { 27 | x: 1 // Comment 28 | }; 29 | ` 30 | }, 31 | { 32 | code: dedent` 33 | const obj = { 34 | x: 1, // Comment 35 | x: 2 36 | }; 37 | ` 38 | } 39 | ], 40 | invalid: [ 41 | { 42 | options: [{ maxLineLength: 22 }], 43 | code: dedent` 44 | const obj1 = { 45 | x: 1 46 | }; 47 | const obj2 = { 48 | x: 12 49 | }; 50 | `, 51 | output: dedent` 52 | const obj1 = {x: 1}; 53 | const obj2 = { 54 | x: 12 55 | }; 56 | `, 57 | errors: [{ line: 1, endLine: 3, messageId: 'preferSingleLine' }] 58 | }, 59 | { 60 | options: [{ maxLineLength: 25 }], 61 | code: dedent` 62 | const obj1 = f({ 63 | x: 1 64 | }); 65 | const obj2 = f({ 66 | x: 12 67 | }); 68 | `, 69 | output: dedent` 70 | const obj1 = f({x: 1}); 71 | const obj2 = f({ 72 | x: 12 73 | }); 74 | `, 75 | errors: [{ line: 1, endLine: 3, messageId: 'preferSingleLine' }] 76 | }, 77 | { 78 | options: [{ maxLineLength: 28 }], 79 | code: dedent` 80 | const obj1 = { 81 | x: 1, 82 | y: 2 83 | }; 84 | const obj2 = { 85 | x: 1, 86 | y: 23 87 | }; 88 | `, 89 | output: dedent` 90 | const obj1 = {x: 1,y: 2}; 91 | const obj2 = { 92 | x: 1, 93 | y: 23 94 | }; 95 | `, 96 | errors: [{ line: 1, endLine: 4, messageId: 'preferSingleLine' }] 97 | }, 98 | { 99 | options: [{ maxObjectSize: 2 }], 100 | code: 'const obj = {x: 1,y: 2,y: 3};', 101 | output: dedent` 102 | const obj = { 103 | x: 1, 104 | y: 2, 105 | y: 3 106 | }; 107 | `, 108 | errors: [{ line: 1, messageId: 'preferMultiline' }] 109 | }, 110 | { 111 | options: [{ maxLineLength: 31 }], 112 | code: dedent` 113 | const obj1 = { 114 | x: 1 115 | } as const; 116 | const obj2 = { 117 | x: 12 118 | } as const; 119 | `, 120 | output: dedent` 121 | const obj1 = {x: 1} as const; 122 | const obj2 = { 123 | x: 12 124 | } as const; 125 | `, 126 | errors: [{ line: 1, endLine: 3, messageId: 'preferSingleLine' }] 127 | } 128 | ] 129 | }); 130 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-useless-plusplus/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | // https://sonarsource.github.io/rspec/#/rspec/S2123/javascript 21 | 22 | import { createRule } from '@eslint-sukka/shared'; 23 | import { AST_NODE_TYPES } from '@typescript-eslint/types'; 24 | import type { TSESTree } from '@typescript-eslint/types'; 25 | import type { TSESLint } from '@typescript-eslint/utils'; 26 | 27 | export default createRule({ 28 | name: 'no-useless-plusplus', 29 | meta: { 30 | schema: [], 31 | type: 'problem', 32 | docs: { 33 | description: 'Values should not be uselessly incremented', 34 | recommended: 'recommended', 35 | url: 'https://sonarsource.github.io/rspec/#/rspec/S2123/javascript' 36 | }, 37 | messages: { 38 | removeIncrement: 'Remove this {{updateOperator}}rement or correct the code not to waste it.' 39 | } 40 | }, 41 | create(context) { 42 | function reportUpdateExpression(updateExpression: TSESTree.UpdateExpression) { 43 | const updateOperator = updateExpression.operator === '++' ? 'inc' : 'dec'; 44 | context.report({ 45 | messageId: 'removeIncrement', 46 | data: { 47 | updateOperator 48 | }, 49 | node: updateExpression 50 | }); 51 | } 52 | 53 | return { 54 | 'ReturnStatement > UpdateExpression': function (node: TSESTree.Node) { 55 | const updateExpression = node as TSESTree.UpdateExpression; 56 | const argument = updateExpression.argument; 57 | if ( 58 | !updateExpression.prefix 59 | && argument.type === AST_NODE_TYPES.Identifier 60 | && isLocalIdentifier(argument, context.sourceCode.getScope(node)) 61 | ) { 62 | reportUpdateExpression(updateExpression); 63 | } 64 | }, 65 | AssignmentExpression(node) { 66 | const assignment = node; 67 | const rhs = assignment.right; 68 | if (rhs.type === AST_NODE_TYPES.UpdateExpression && !rhs.prefix) { 69 | const lhs = assignment.left; 70 | if ( 71 | lhs.type === AST_NODE_TYPES.Identifier 72 | && rhs.argument.type === AST_NODE_TYPES.Identifier 73 | && rhs.argument.name === lhs.name 74 | ) { 75 | reportUpdateExpression(rhs); 76 | } 77 | } 78 | } 79 | }; 80 | } 81 | }); 82 | 83 | function isLocalIdentifier(id: TSESTree.Identifier, scope: TSESLint.Scope.Scope) { 84 | return scope.variables.some(v => v.identifiers.some(i => i.name === id.name)); 85 | } 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-sukka-monorepo", 3 | "version": "8.0.6", 4 | "private": true, 5 | "description": "", 6 | "scripts": { 7 | "build": "turbo build", 8 | "codegen": "turbo codegen", 9 | "test": "turbo test", 10 | "lint": "turbo run lint:root", 11 | "lint:root": "eslint --format=sukka .", 12 | "prerelease": "pnpm run build && pnpm run lint", 13 | "release": "bumpp -r --all --commit \"release: %s\" --tag \"%s\"" 14 | }, 15 | "keywords": [], 16 | "author": "Sukka ", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@eslint-sukka/internal": "workspace:*", 20 | "@eslint-sukka/node": "workspace:*", 21 | "@eslint-sukka/react": "workspace:*", 22 | "@eslint-sukka/shared": "workspace:*", 23 | "@eslint-sukka/yaml": "workspace:*", 24 | "@eslint/core": "^0.17.0", 25 | "@swc-node/register": "^1.11.1", 26 | "@swc/core": "^1.15.4", 27 | "@types/mocha": "^10.0.10", 28 | "@types/node": "^24.10.4", 29 | "@types/stringify-object": "^4.0.5", 30 | "@typescript-eslint/rule-tester": "^8.49.0", 31 | "@typescript-eslint/scope-manager": "^8.49.0", 32 | "@typescript-eslint/utils": "^8.49.0", 33 | "bumpp": "^10.3.2", 34 | "eslint": "^9.39.2", 35 | "eslint-config-sukka": "workspace:*", 36 | "eslint-formatter-sukka": "workspace:*", 37 | "eslint-plugin-sukka": "workspace:*", 38 | "foxts": "^5.0.3", 39 | "mocha": "^11.7.5", 40 | "rollup": "^4.53.3", 41 | "stringify-object": "5.0.0", 42 | "ts-dedent": "^2.2.0", 43 | "turbo": "^2.6.3", 44 | "typescript": "^5.9.3" 45 | }, 46 | "packageManager": "pnpm@10.25.0", 47 | "pnpm": { 48 | "overrides": { 49 | "array-includes": "npm:@nolyfill/array-includes@^1.0.44", 50 | "array.prototype.findlastindex": "npm:@nolyfill/array.prototype.findlastindex@^1.0.44", 51 | "array.prototype.flat": "npm:@nolyfill/array.prototype.flat@^1.0.44", 52 | "array.prototype.flatmap": "npm:@nolyfill/array.prototype.flatmap@^1.0.44", 53 | "array.prototype.tosorted": "npm:@nolyfill/array.prototype.tosorted@^1.0.44", 54 | "deep-equal": "npm:@nolyfill/deep-equal@^1.0.44", 55 | "es-iterator-helpers": "npm:@nolyfill/es-iterator-helpers@^1.0.21", 56 | "eslint-import-resolver-typescript>eslint-plugin-import": "npm:@favware/skip-dependency", 57 | "eslint>chalk": "npm:picocolors@^1.1.1", 58 | "has": "npm:@nolyfill/has@^1.0.44", 59 | "hasown": "npm:@nolyfill/hasown@^1.0.44", 60 | "is-core-module": "npm:@nolyfill/is-core-module@^1.0.39", 61 | "object.assign": "npm:@nolyfill/object.assign@^1.0.44", 62 | "object.entries": "npm:@nolyfill/object.entries@^1.0.44", 63 | "object.fromentries": "npm:@nolyfill/object.fromentries@^1.0.44", 64 | "object.groupby": "npm:@nolyfill/object.groupby@^1.0.44", 65 | "object.hasown": "npm:@nolyfill/object.hasown@^1.0.44", 66 | "object.values": "npm:@nolyfill/object.values@^1.0.44", 67 | "safe-regex-test": "npm:@nolyfill/safe-regex-test@^1.0.44", 68 | "string.prototype.includes": "npm:@nolyfill/string.prototype.includes@^1.0.44", 69 | "string.prototype.matchall": "npm:@nolyfill/string.prototype.matchall@^1.0.44" 70 | }, 71 | "onlyBuiltDependencies": [ 72 | "@swc/core", 73 | "esbuild", 74 | "oxc-resolver", 75 | "unrs-resolver" 76 | ], 77 | "patchedDependencies": { 78 | "eslint-plugin-unicorn": "patches/eslint-plugin-unicorn.patch" 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-top-level-this/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | // https://sonarsource.github.io/rspec/#/rspec/S2990/javascript 21 | 22 | import { createRule } from '@eslint-sukka/shared'; 23 | import { TSESLint } from '@typescript-eslint/utils'; 24 | import { AST_NODE_TYPES } from '@typescript-eslint/types'; 25 | import type { TSESTree } from '@typescript-eslint/types'; 26 | 27 | type MessageIds = 'removeThis' | 'suggestRemoveThis' | 'suggestUseGlobalThis'; 28 | 29 | export default createRule({ 30 | name: 'no-top-level-this', 31 | meta: { 32 | hasSuggestions: true, 33 | schema: [], 34 | messages: { 35 | removeThis: 'Remove the use of "this".', 36 | suggestRemoveThis: 'Remove "this"', 37 | suggestUseGlobalThis: 'Replace "this" with "globalThis" object' 38 | }, 39 | type: 'suggestion', 40 | docs: { 41 | description: 'The global "this" object should not be used', 42 | recommended: 'recommended', 43 | url: 'https://sonarsource.github.io/rspec/#/rspec/S2990/javascript' 44 | }, 45 | fixable: 'code' 46 | }, 47 | create(context) { 48 | return { 49 | 'MemberExpression[object.type="ThisExpression"]': (memberExpression: TSESTree.MemberExpression) => { 50 | const scopeType = context.sourceCode.getScope(memberExpression).variableScope.type; 51 | const isInsideClass = context.sourceCode 52 | .getAncestors(memberExpression) 53 | .some( 54 | ancestor => ancestor.type === AST_NODE_TYPES.ClassDeclaration || ancestor.type === AST_NODE_TYPES.ClassExpression 55 | ); 56 | if ((scopeType === TSESLint.Scope.ScopeType.global || scopeType === TSESLint.Scope.ScopeType.module) && !isInsideClass) { 57 | const suggest: Array> = []; 58 | if (!memberExpression.computed) { 59 | const propertyText = context.sourceCode.getText(memberExpression.property); 60 | suggest.push( 61 | { 62 | messageId: 'suggestRemoveThis', 63 | fix: fixer => fixer.replaceText(memberExpression, propertyText) 64 | }, 65 | { 66 | messageId: 'suggestUseGlobalThis', 67 | fix: fixer => fixer.replaceText(memberExpression.object, 'globalThis') 68 | } 69 | ); 70 | } 71 | context.report({ 72 | messageId: 'removeThis', 73 | node: memberExpression.object, 74 | suggest 75 | }); 76 | } 77 | } 78 | }; 79 | } 80 | }); 81 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/bool-param-default/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | // https://sonarsource.github.io/rspec/#/rspec/S4798/javascript 21 | 22 | import { AST_NODE_TYPES } from '@typescript-eslint/types'; 23 | import type { TSESTree } from '@typescript-eslint/types'; 24 | import { createRule } from '@eslint-sukka/shared'; 25 | 26 | type FunctionLike = 27 | | TSESTree.FunctionDeclaration 28 | | TSESTree.FunctionExpression 29 | | TSESTree.ArrowFunctionExpression; 30 | 31 | export default createRule({ 32 | name: 'bool-param-default', 33 | meta: { 34 | schema: [], 35 | type: 'suggestion', 36 | docs: { 37 | description: 'Optional boolean parameters should have default value' 38 | }, 39 | messages: { 40 | provideDefault: 41 | 'Provide a default value for \'{{parameter}}\' so that ' 42 | + 'the logic of the function is more evident when this parameter is missing. ' 43 | + 'Consider defining another function if providing default value is not possible.' 44 | } 45 | }, 46 | create(context) { 47 | return { 48 | 'FunctionDeclaration, FunctionExpression, ArrowFunctionExpression': (node: FunctionLike) => { 49 | const functionLike = node; 50 | for (const param of functionLike.params) { 51 | if (param.type === AST_NODE_TYPES.Identifier && isOptionalBoolean(param)) { 52 | context.report({ 53 | messageId: 'provideDefault', 54 | data: { 55 | parameter: param.name 56 | }, 57 | node: param 58 | }); 59 | } 60 | } 61 | } 62 | }; 63 | } 64 | }); 65 | 66 | function isOptionalBoolean(node: TSESTree.Identifier): boolean { 67 | return usesQuestionOptionalSyntax(node) || usesUnionUndefinedOptionalSyntax(node); 68 | } 69 | 70 | /** 71 | * Matches "param?: boolean" 72 | */ 73 | function usesQuestionOptionalSyntax(node: TSESTree.Identifier): boolean { 74 | return ( 75 | node.optional 76 | && !!node.typeAnnotation 77 | && node.typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSBooleanKeyword 78 | ); 79 | } 80 | 81 | /** 82 | * Matches "boolean | undefined" 83 | */ 84 | function usesUnionUndefinedOptionalSyntax(node: TSESTree.Identifier): boolean { 85 | if (!!node.typeAnnotation && node.typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSUnionType) { 86 | const types = node.typeAnnotation.typeAnnotation.types; 87 | return ( 88 | types.length === 2 89 | && types.some(tp => tp.type === AST_NODE_TYPES.TSBooleanKeyword) 90 | && types.some(tp => tp.type === AST_NODE_TYPES.TSUndefinedKeyword) 91 | ); 92 | } 93 | return false; 94 | } 95 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/track-todo-fixme-comment/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | // https://sonarsource.github.io/rspec/#/rspec/S1134/javascript 21 | 22 | import type { TSESTree } from '@typescript-eslint/types'; 23 | import { createRule } from '@eslint-sukka/shared'; 24 | 25 | export default createRule({ 26 | name: 'track-todo-fixme-comment', 27 | meta: { 28 | type: 'suggestion', 29 | docs: { 30 | description: 'Track uses of "FIXME" and "TODO" tags', 31 | recommended: 'recommended', 32 | url: 'https://sonarsource.github.io/rspec/#/rspec/S1134/javascript' 33 | }, 34 | schema: [], 35 | messages: { 36 | fixme: 'Take the required action to fix the issue indicated by this comment.', 37 | todo: 'Complete the task associated to this "TODO" comment.' 38 | } 39 | }, 40 | create(context) { 41 | function reportPatternInComment( 42 | pattern: string, 43 | messageId: 'fixme' | 'todo' 44 | ) { 45 | const sourceCode = context.sourceCode; 46 | (sourceCode.getAllComments()).forEach(comment => { 47 | const rawText = comment.value.toLowerCase(); 48 | 49 | if (rawText.includes(pattern)) { 50 | const lines = rawText.split(/\r\n?|\n/); 51 | 52 | for (let i = 0; i < lines.length; i++) { 53 | const index = lines[i].indexOf(pattern); 54 | if (index >= 0 && !isLetterAround(lines[i], index, pattern)) { 55 | context.report({ 56 | messageId, 57 | loc: getPatternPosition(i, index, comment, pattern) 58 | }); 59 | } 60 | } 61 | } 62 | }); 63 | } 64 | 65 | return { 66 | 'Program:exit': () => { 67 | reportPatternInComment('fixme', 'fixme'); 68 | reportPatternInComment('todo', 'todo'); 69 | } 70 | }; 71 | } 72 | }); 73 | 74 | const letterPattern = /\p{Letter}/u; 75 | 76 | function isLetterAround(line: string, start: number, pattern: string) { 77 | const end = start + pattern.length; 78 | 79 | const pre = start > 0 && letterPattern.test(line.charAt(start - 1)); 80 | const post = end <= line.length - 1 && letterPattern.test(line.charAt(end)); 81 | 82 | return pre || post; 83 | } 84 | 85 | function getPatternPosition( 86 | lineIdx: number, 87 | index: number, 88 | comment: TSESTree.Comment, 89 | pattern: string 90 | ) { 91 | const line = comment.loc.start.line + lineIdx; 92 | const columnStart = lineIdx === 0 ? comment.loc.start.column + 2 : 0; 93 | const patternStart = columnStart + index; 94 | 95 | return { 96 | start: { line, column: patternStart }, 97 | end: { line, column: patternStart + pattern.length } 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /packages/shared/src/get-package-json.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import fs from 'node:fs'; 3 | 4 | import type { PackageJson } from '@package-json/types'; 5 | 6 | const SKIP_TIME = 20000; 7 | 8 | /** 9 | * The class of cache. 10 | * The cache will dispose of each value if the value has not been accessed 11 | * during 20 seconds. 12 | */ 13 | class Cache { 14 | /** 15 | * Initialize this cache instance. 16 | */ 17 | map = new Map(); 18 | 19 | /** 20 | * Get the cached value of the given key. 21 | */ 22 | get(key: string): T | null { 23 | const entry = this.map.get(key); 24 | const now = Date.now(); 25 | 26 | if (entry) { 27 | if (entry.expire > now) { 28 | entry.expire = now + SKIP_TIME; 29 | return entry.value; 30 | } 31 | this.map.delete(key); 32 | } 33 | return null; 34 | } 35 | 36 | /** 37 | * Set the value of the given key. 38 | * @returns {void} 39 | */ 40 | set(key: string, value: T) { 41 | const entry = this.map.get(key); 42 | const expire = Date.now() + SKIP_TIME; 43 | 44 | if (entry) { 45 | entry.value = value; 46 | entry.expire = expire; 47 | } else { 48 | this.map.set(key, { value, expire }); 49 | } 50 | } 51 | } 52 | 53 | const cache = new Cache(); 54 | 55 | /** 56 | * Reads the `package.json` data in a given path. 57 | * 58 | * Don't cache the data. 59 | * 60 | * @param dir - The path to a directory to read. 61 | * @returns The read `package.json` data, or null. 62 | */ 63 | function readPackageJson(dir: string): PackageJson | null { 64 | const filePath = path.join(dir, 'package.json'); 65 | try { 66 | const text = fs.readFileSync(filePath, 'utf8'); 67 | const data: PackageJson | null | undefined = JSON.parse(text); 68 | 69 | if (data && typeof data === 'object') { 70 | data.filePath = filePath; 71 | return data; 72 | } 73 | } catch { 74 | // do nothing. 75 | } 76 | 77 | return null; 78 | } 79 | 80 | /** 81 | * Gets a `package.json` data. 82 | * The data is cached if found, then it's used after. 83 | * 84 | * @param startPath - A file path to lookup. 85 | * @returns A found `package.json` data or `null`. 86 | * This object have additional property `filePath`. 87 | */ 88 | export function getPackageJson(startPath = 'a.js'): PackageJson | null { 89 | const startDir = path.dirname(path.resolve(startPath)); 90 | let dir: string = startDir; 91 | // eslint-disable-next-line no-useless-assignment -- pre-init value with empty string for boosting performance 92 | let prevDir = ''; 93 | let data: PackageJson; 94 | 95 | do { 96 | data = cache.get(dir); 97 | if (data) { 98 | if (dir !== startDir) { 99 | cache.set(startDir, data); 100 | } 101 | return data; 102 | } 103 | 104 | data = readPackageJson(dir); 105 | if (data) { 106 | cache.set(dir, data); 107 | cache.set(startDir, data); 108 | return data; 109 | } 110 | 111 | // Go to next. 112 | prevDir = dir; 113 | dir = path.resolve(dir, '..'); 114 | } while (dir !== prevDir); 115 | 116 | cache.set(startDir, null); 117 | return null; 118 | } 119 | 120 | export function isDirectDependency( 121 | name: string, 122 | startPath = 'a.js' 123 | ): boolean { 124 | const pkg = getPackageJson(startPath); 125 | if (!pkg) return false; 126 | const deps = pkg.dependencies || {}; 127 | const devDeps = pkg.devDependencies || {}; 128 | const peerDeps = pkg.peerDependencies || {}; 129 | return Boolean(deps[name] || devDeps[name] || peerDeps[name]); 130 | } 131 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/class-prototype/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | // https://sonarsource.github.io/rspec/#/rspec/S3525/javascript 21 | 22 | import { createRule, isParserWithTypeInformation, ensureParserWithTypeInformation } from '@eslint-sukka/shared'; 23 | import { AST_NODE_TYPES } from '@typescript-eslint/types'; 24 | import { SymbolFlags as tsSymbolFlags } from 'typescript'; 25 | import type { TSESTree } from '@typescript-eslint/types'; 26 | import type { ParserServices } from '@typescript-eslint/utils'; 27 | 28 | export default createRule({ 29 | name: 'class-prototype', 30 | meta: { 31 | schema: [], 32 | type: 'suggestion', 33 | docs: { 34 | description: 'Class methods should be used instead of "prototype" assignments', 35 | recommended: 'recommended' 36 | }, 37 | messages: { 38 | declareClass: 39 | 'Declare a "{{class}}" class and move this declaration of "{{declaration}}" into it.' 40 | } 41 | }, 42 | create(context) { 43 | const services = context.sourceCode.parserServices; 44 | const isFunction = isParserWithTypeInformation(services) ? isFunctionType : isFunctionLike; 45 | return { 46 | AssignmentExpression({ left, right }) { 47 | if (left.type === AST_NODE_TYPES.MemberExpression && isFunction(right, services)) { 48 | const [member, prototype] = [left.object, left.property]; 49 | if (member.type === AST_NODE_TYPES.MemberExpression && prototype.type === AST_NODE_TYPES.Identifier) { 50 | const [klass, property] = [member.object, member.property]; 51 | if ( 52 | klass.type === AST_NODE_TYPES.Identifier 53 | && property.type === AST_NODE_TYPES.Identifier 54 | && property.name === 'prototype' 55 | ) { 56 | context.report({ 57 | messageId: 'declareClass', 58 | data: { 59 | class: klass.name, 60 | declaration: prototype.name 61 | }, 62 | node: left 63 | }); 64 | } 65 | } 66 | } 67 | } 68 | }; 69 | } 70 | }); 71 | 72 | function isFunctionType(node: TSESTree.Node, services: Partial | undefined) { 73 | ensureParserWithTypeInformation(services); 74 | const type = services.program.getTypeChecker().getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(node)); 75 | 76 | return !!type.symbol && (type.symbol.flags & tsSymbolFlags.Function) !== 0; 77 | } 78 | 79 | const FUNCTION_TYPES = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression']); 80 | 81 | export function isFunctionLike(node: TSESTree.Node): node is TSESTree.FunctionExpression | TSESTree.FunctionDeclaration | TSESTree.ArrowFunctionExpression { 82 | return FUNCTION_TYPES.has( 83 | node.type 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-for-in-iterable/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | import mod from '.'; 21 | import { runTest } from '@eslint-sukka/internal'; 22 | 23 | runTest({ 24 | module: mod, 25 | valid: [ 26 | { 27 | code: ` 28 | for (const x of [3, 4, 5]) { 29 | console.log(x); 30 | } 31 | ` 32 | }, 33 | { 34 | code: ` 35 | const object = { fst: 1, snd: 2 }; 36 | for (let value in object) console.log(value);` 37 | } 38 | ], 39 | invalid: [ 40 | { 41 | code: ` 42 | const array = [1, 2, 3]; 43 | for (let value in array) console.log(value);`, 44 | errors: [ 45 | { 46 | messageId: 'useForOf', 47 | line: 3, 48 | column: 9, 49 | endLine: 3, 50 | endColumn: 12 51 | } 52 | ] 53 | }, 54 | { 55 | code: ` 56 | const array = new Int8Array(5); 57 | for (let value in array) console.log(value);`, 58 | errors: [{ messageId: 'useForOf' }] 59 | }, 60 | { 61 | code: ` 62 | const set = new Set([1, 2, 3, 4, 5]); 63 | for (let value in set) console.log(value);`, 64 | errors: [{ messageId: 'useForOf' }] 65 | }, 66 | { 67 | code: ` 68 | const map = new Map(); map.set('zero', 0); 69 | for (let value in map) console.log(value);`, 70 | errors: [{ messageId: 'useForOf' }] 71 | }, 72 | { 73 | code: ` 74 | const string = 'Hello'; 75 | for (let value in string) console.log(value);`, 76 | errors: [{ messageId: 'useForOf' }] 77 | }, 78 | { 79 | code: ` 80 | const string = new String('Hello'); 81 | for (let value in string) console.log(value);`, 82 | errors: [{ messageId: 'useForOf' }] 83 | }, 84 | { 85 | code: ` 86 | for (const x in [3, 4, 5]) { 87 | console.log(x); 88 | } 89 | `, 90 | errors: [{ messageId: 'useForOf' }] 91 | }, 92 | { 93 | code: ` 94 | const z = [3, 4, 5]; 95 | for (const x in z) { 96 | console.log(x); 97 | } 98 | `, 99 | errors: [{ messageId: 'useForOf' }] 100 | }, 101 | { 102 | code: ` 103 | const fn = (arr: number[]) => { 104 | for (const x in arr) { 105 | console.log(x); 106 | } 107 | }; 108 | `, 109 | errors: [{ messageId: 'useForOf' }] 110 | }, 111 | { 112 | code: ` 113 | const fn = (arr: number[] | string[]) => { 114 | for (const x in arr) { 115 | console.log(x); 116 | } 117 | }; 118 | `, 119 | errors: [{ messageId: 'useForOf' }] 120 | }, 121 | { 122 | code: ` 123 | const fn = (arr: T) => { 124 | for (const x in arr) { 125 | console.log(x); 126 | } 127 | }; 128 | `, 129 | errors: [{ messageId: 'useForOf' }] 130 | } 131 | ] 132 | }); 133 | -------------------------------------------------------------------------------- /packages/internal/eslint-plugin-tester.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { RuleTester } from '@typescript-eslint/rule-tester'; 3 | import type { InvalidTestCase, ValidTestCase, TestCaseError } from '@typescript-eslint/rule-tester'; 4 | 5 | import type { ExportedRuleModule } from '@eslint-sukka/shared'; 6 | import { after, describe, it } from 'mocha'; 7 | import type { TSESLint } from '@typescript-eslint/utils'; 8 | import { identity } from 'foxts/identity'; 9 | 10 | // import { globals } from '@eslint-sukka/shared'; 11 | 12 | RuleTester.afterAll = after; 13 | RuleTester.it = it; 14 | RuleTester.itOnly = it.only; 15 | RuleTester.itSkip = it.skip; 16 | RuleTester.describe = describe; 17 | RuleTester.describeSkip = describe.skip; 18 | 19 | const $tester = new RuleTester({ 20 | languageOptions: { 21 | parserOptions: { 22 | ecmaFeatures: { jsx: true }, 23 | project: 'tsconfig.json', 24 | projectService: true, 25 | tsconfigRootDir: path.join(__dirname, '..', '..', 'tests', 'fixtures'), 26 | warnOnUnsupportedTypeScriptVersion: false 27 | } 28 | }, 29 | linterOptions: { 30 | reportUnusedDisableDirectives: false 31 | } 32 | }); 33 | 34 | type TestCaseGenerator = ((cast: (input: T) => T) => Generator) | (readonly R[]); 35 | 36 | interface InvalidTestCaseWithNumberFormOfErrors extends Omit, 'errors'> { 37 | errors: number | ReadonlyArray> 38 | } 39 | 40 | interface RunOptions { 41 | module: ExportedRuleModule, 42 | valid?: TestCaseGenerator, string | ValidTestCase>, 43 | invalid?: TestCaseGenerator> 44 | } 45 | 46 | export function runTest( 47 | { module: mod, valid, invalid }: RunOptions, 48 | extraRules?: Record 49 | ) { 50 | const $valid = typeof valid === 'function' 51 | ? Array.from(valid(identity)) 52 | : (valid ?? []); 53 | const $invalid = typeof invalid === 'function' 54 | ? Array.from(invalid(identity)) 55 | : (invalid ?? []); 56 | 57 | const tester = extraRules 58 | ? (() => { 59 | const tester = new RuleTester({ 60 | languageOptions: { 61 | parserOptions: { 62 | ecmaFeatures: { jsx: true }, 63 | project: 'tsconfig.json', 64 | tsconfigRootDir: path.join(__dirname, '..', '..', 'tests', 'fixtures'), 65 | warnOnUnsupportedTypeScriptVersion: false 66 | } 67 | }, 68 | linterOptions: { 69 | reportUnusedDisableDirectives: false 70 | } 71 | }); 72 | 73 | Object.entries(extraRules).forEach(([name, rule]) => tester.defineRule(name, rule)); 74 | 75 | return tester; 76 | })() 77 | : $tester; 78 | 79 | // eslint-disable-next-line sukka/type/no-force-cast-via-top-type -- mismatch between me and typescript-eslint 80 | tester.run(mod.name, mod as unknown as TSESLint.RuleModule, { 81 | valid: $valid.flat().map((item, index) => { 82 | if (typeof item === 'string') { 83 | return item; 84 | } 85 | 86 | return { 87 | ...item, 88 | name: `${item.name || 'valid'} #${index}` 89 | }; 90 | }), 91 | invalid: $invalid.flat().map((item, index) => ({ 92 | ...item, 93 | name: `${item.name || 'invalid'} #${index}` 94 | })) as any 95 | }); 96 | } 97 | 98 | runTest.skip = ( 99 | _args: RunOptions 100 | ) => { 101 | // noop 102 | }; 103 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-same-line-conditional/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | // https://sonarsource.github.io/rspec/#/rspec/S3972 21 | 22 | import { AST_NODE_TYPES } from '@typescript-eslint/types'; 23 | import type { TSESTree } from '@typescript-eslint/types'; 24 | import type { TSESLint } from '@typescript-eslint/utils'; 25 | import { createRule } from '@eslint-sukka/shared'; 26 | 27 | interface SiblingIfStatement { 28 | first: TSESTree.IfStatement, 29 | following: TSESTree.IfStatement 30 | } 31 | 32 | export default createRule({ 33 | name: 'no-same-line-conditional', 34 | meta: { 35 | schema: [], 36 | type: 'layout', 37 | docs: { 38 | description: 'Conditionals should start on new lines', 39 | recommended: 'recommended', 40 | url: 'https://sonarsource.github.io/rspec/#/rspec/S3972/javascript' 41 | }, 42 | fixable: 'whitespace', 43 | messages: { 44 | sameLineCondition: 'Move this "if" to a new line or add the missing "else".' 45 | } 46 | }, 47 | create(context) { 48 | function checkStatements(statements: TSESTree.Node[]) { 49 | const { sourceCode } = context; 50 | const siblingIfStatements = getSiblingIfStatements(statements); 51 | 52 | siblingIfStatements.forEach(siblingIfStatement => { 53 | const precedingIf = siblingIfStatement.first; 54 | const followingIf = siblingIfStatement.following; 55 | if ( 56 | precedingIf.loc && followingIf.loc 57 | && precedingIf.loc.end.line === followingIf.loc.start.line 58 | && precedingIf.loc.start.line !== followingIf.loc.end.line 59 | ) { 60 | // const precedingIfLastToken = sourceCode.getLastToken( 61 | // precedingIf as TSESTree.Node 62 | // ) as TSESLint.AST.Token; 63 | const followingIfToken = sourceCode.getFirstToken( 64 | followingIf as TSESTree.Node 65 | ) as TSESLint.AST.Token; 66 | 67 | context.report({ 68 | messageId: 'sameLineCondition', 69 | loc: followingIfToken.loc, 70 | fix: fixer => fixer.replaceTextRange( 71 | [precedingIf.range[1], followingIf.range[0]], 72 | '\n' + ' '.repeat(precedingIf.loc.start.column) 73 | ) 74 | }); 75 | } 76 | }); 77 | } 78 | 79 | return { 80 | Program: (node: TSESTree.Program) => checkStatements(node.body), 81 | BlockStatement: (node: TSESTree.BlockStatement) => checkStatements(node.body), 82 | SwitchCase: (node: TSESTree.SwitchCase) => checkStatements(node.consequent) 83 | }; 84 | } 85 | }); 86 | 87 | function getSiblingIfStatements(statements: TSESTree.Node[]): SiblingIfStatement[] { 88 | return statements.reduce((siblingsArray, statement, currentIndex) => { 89 | const previousStatement = statements[currentIndex - 1] as TSESTree.Node | undefined; 90 | if ( 91 | statement.type === AST_NODE_TYPES.IfStatement 92 | && !!previousStatement 93 | && previousStatement.type === AST_NODE_TYPES.IfStatement 94 | ) { 95 | return [{ first: previousStatement, following: statement }, ...siblingsArray]; 96 | } 97 | return siblingsArray; 98 | }, []); 99 | } 100 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/comma-or-logical-or-case/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | // https://sonarsource.github.io/rspec/#/rspec/S3616/javascript 21 | 22 | import { createRule } from '@eslint-sukka/shared'; 23 | import { AST_NODE_TYPES } from '@typescript-eslint/types'; 24 | import type { TSESTree } from '@typescript-eslint/types'; 25 | 26 | export default createRule({ 27 | name: 'comma-or-logical-or-case', 28 | meta: { 29 | type: 'problem', 30 | docs: { 31 | description: 'Comma and logical OR operators should not be used in switch cases', 32 | recommended: 'recommended', 33 | url: 'https://sonarsource.github.io/rspec/#/rspec/S3616/javascript' 34 | }, 35 | schema: [], 36 | messages: { 37 | specifyCase: 'Explicitly specify {{nesting}} separate cases that fall through; currently this case clause only works for "{{expression}}".' 38 | } 39 | }, 40 | create(context) { 41 | function reportIssue(node: TSESTree.Node, clause: TSESTree.Node, nestingLvl: number) { 42 | context.report({ 43 | messageId: 'specifyCase', 44 | data: { 45 | nesting: nestingLvl.toString(), 46 | expression: String(getTextFromNode(clause)) 47 | }, 48 | node 49 | }); 50 | } 51 | 52 | function getTextFromNode(node: TSESTree.Node) { 53 | if (node.type === AST_NODE_TYPES.Literal) { 54 | return node.value; 55 | } 56 | return context.sourceCode.getText(node); 57 | } 58 | 59 | function getEnclosingSwitchStatement( 60 | node: TSESTree.Node 61 | ): TSESTree.SwitchStatement { 62 | const ancestors = context.sourceCode.getAncestors(node); 63 | for (let i = ancestors.length - 1; i >= 0; i--) { 64 | if (ancestors[i].type === AST_NODE_TYPES.SwitchStatement) { 65 | return ancestors[i] as TSESTree.SwitchStatement; 66 | } 67 | } 68 | throw new Error('A switch case should have an enclosing switch statement'); 69 | } 70 | 71 | return { 72 | 'SwitchCase > SequenceExpression': function (node: TSESTree.SequenceExpression) { 73 | const expressions = node.expressions; 74 | reportIssue(node, expressions[expressions.length - 1], expressions.length); 75 | }, 76 | 'SwitchCase > LogicalExpression': function (node: TSESTree.LogicalExpression) { 77 | if (!isSwitchTrue(getEnclosingSwitchStatement(node))) { 78 | const firstElemAndNesting = getFirstElementAndNestingLevel( 79 | node, 80 | 0 81 | ); 82 | if (firstElemAndNesting) { 83 | reportIssue(node, firstElemAndNesting[0], firstElemAndNesting[1] + 1); 84 | } 85 | } 86 | } 87 | }; 88 | } 89 | }); 90 | 91 | function isSwitchTrue(node: TSESTree.SwitchStatement) { 92 | return node.discriminant.type === AST_NODE_TYPES.Literal && node.discriminant.value === true; 93 | } 94 | 95 | function getFirstElementAndNestingLevel( 96 | logicalExpression: TSESTree.LogicalExpression, 97 | currentLvl: number 98 | ): [TSESTree.Node, number] | undefined { 99 | if (logicalExpression.operator === '||') { 100 | if (logicalExpression.left.type === AST_NODE_TYPES.LogicalExpression) { 101 | return getFirstElementAndNestingLevel(logicalExpression.left, currentLvl + 1); 102 | } 103 | return [logicalExpression.left, currentLvl + 1]; 104 | } 105 | return undefined; 106 | } 107 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/track-todo-fixme-comment/index.test.ts: -------------------------------------------------------------------------------- 1 | import mod from '.'; 2 | import { runTest } from '@eslint-sukka/internal'; 3 | import { createFixedArray } from 'foxts/create-fixed-array'; 4 | 5 | runTest({ 6 | module: mod, 7 | valid: [ 8 | { 9 | code: '// Just a regular comment' 10 | }, 11 | { 12 | code: ` 13 | // This is not aFIXME comment 14 | 15 | // notafixme comment 16 | 17 | // a fixmeal 18 | ` 19 | }, 20 | { 21 | code: '// Just a regular comment' 22 | }, 23 | { 24 | code: ` 25 | // This is not aTODO comment 26 | 27 | // notatodo comment 28 | 29 | // a todolist 30 | 31 | // método 32 | ` 33 | }, 34 | { 35 | code: '// todos' 36 | }, 37 | { 38 | code: '// todos ' 39 | } 40 | ], 41 | invalid: [ 42 | { 43 | code: '// FIXME', 44 | errors: [ 45 | { 46 | messageId: 'fixme', 47 | line: 1, 48 | endLine: 1, 49 | column: 4, 50 | endColumn: 9 51 | } 52 | ] 53 | }, 54 | 55 | { 56 | code: `/*FIXME Multiline comment 57 | FIXME: another fixme 58 | (this line is not highlighted) 59 | with three fixme 60 | */`, 61 | errors: [ 62 | { 63 | messageId: 'fixme', 64 | line: 1, 65 | endLine: 1, 66 | column: 3, 67 | endColumn: 8 68 | }, 69 | { 70 | messageId: 'fixme', 71 | line: 2, 72 | endLine: 2, 73 | column: 7, 74 | endColumn: 12 75 | }, 76 | { 77 | messageId: 'fixme', 78 | line: 4, 79 | endLine: 4, 80 | column: 18, 81 | endColumn: 23 82 | } 83 | ] 84 | }, 85 | { 86 | code: '// FIXME FIXME', 87 | errors: [{ messageId: 'fixme' }] 88 | }, 89 | { 90 | code: ` 91 | // FIXME just fix me 92 | 93 | // FixMe just fix me 94 | 95 | //fixme comment 96 | 97 | // This is a FIXME just fix me 98 | 99 | // fixme: things to do 100 | 101 | // :FIXME: things to do 102 | 103 | // valid end of line fixme 104 | 105 | /* 106 | FIXME Multiline comment 107 | */ 108 | 109 | /* 110 | FIXME Multiline comment 111 | 112 | with two fixme 113 | */ 114 | 115 | // valid end of file FIXME 116 | `, 117 | errors: createFixedArray(11).map(() => ({ messageId: 'fixme' } as const)) 118 | }, 119 | { 120 | code: '// TODO', 121 | errors: [ 122 | { 123 | messageId: 'todo', 124 | line: 1, 125 | endLine: 1, 126 | column: 4, 127 | endColumn: 8 128 | } 129 | ] 130 | }, 131 | 132 | { 133 | code: `/*TODO Multiline comment 134 | TODO: another todo 135 | (this line is not highlighted) 136 | with three todo 137 | */`, 138 | errors: [ 139 | { 140 | messageId: 'todo', 141 | line: 1, 142 | endLine: 1, 143 | column: 3, 144 | endColumn: 7 145 | }, 146 | { 147 | messageId: 'todo', 148 | line: 2, 149 | endLine: 2, 150 | column: 7, 151 | endColumn: 11 152 | }, 153 | { 154 | messageId: 'todo', 155 | line: 4, 156 | endLine: 4, 157 | column: 18, 158 | endColumn: 22 159 | } 160 | ] 161 | }, 162 | { 163 | code: '// TODO TODO', 164 | errors: [{ messageId: 'todo' }] 165 | }, 166 | { 167 | code: ` 168 | // TODO just do it 169 | 170 | // Todo just do it 171 | 172 | //todo comment 173 | 174 | // This is a TODO just do it 175 | 176 | // todo: things to do 177 | 178 | // :TODO: things to do 179 | 180 | // valid end of line todo 181 | 182 | /* 183 | TODO Multiline comment 184 | */ 185 | 186 | /* 187 | TODO Multiline comment 188 | 189 | with two todo 190 | */ 191 | 192 | // valid end of file TODO 193 | `, 194 | errors: createFixedArray(11).map(() => ({ messageId: 'todo' } as const)) 195 | } 196 | ] 197 | }); 198 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/call-argument-line/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | // https://sonarsource.github.io/rspec/#/rspec/S1472/javascript 21 | 22 | import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/types'; 23 | import type { TSESTree } from '@typescript-eslint/types'; 24 | import { createRule } from '@eslint-sukka/shared'; 25 | 26 | export default createRule({ 27 | name: 'call-argument-line', 28 | meta: { 29 | type: 'suggestion', 30 | docs: { 31 | description: 'Function call arguments should not start on new lines', 32 | recommended: 'recommended' 33 | }, 34 | schema: [], 35 | messages: { 36 | moveArguments: 'Make those call arguments start on line {{line}}.', 37 | moveTemplateLiteral: 'Make this template literal start on line {{line}}.' 38 | } 39 | }, 40 | create(context) { 41 | const sourceCode = context.sourceCode; 42 | 43 | return { 44 | CallExpression(call) { 45 | if (call.callee.type !== AST_NODE_TYPES.CallExpression && call.arguments.length === 1) { 46 | const callee = getCallee(call); 47 | const parenthesis = sourceCode.getLastTokenBetween( 48 | callee, 49 | call.arguments[0], 50 | isClosingParen 51 | ); 52 | const calleeLastLine = (parenthesis ?? sourceCode.getLastToken(callee))!.loc.end.line; 53 | const { start } = sourceCode.getTokenAfter(callee, isNotClosingParen)!.loc; 54 | if (calleeLastLine !== start.line) { 55 | const { end } = sourceCode.getLastToken(call)!.loc; 56 | if (end.line === start.line) { 57 | context.report({ 58 | messageId: 'moveArguments', 59 | data: { 60 | line: calleeLastLine.toString() 61 | }, 62 | loc: { start, end } 63 | }); 64 | } else { 65 | // If arguments span multiple lines, we only report the first one 66 | context.report({ 67 | messageId: 'moveArguments', 68 | data: { 69 | line: calleeLastLine.toString() 70 | }, 71 | loc: start 72 | }); 73 | } 74 | } 75 | } 76 | }, 77 | TaggedTemplateExpression(node) { 78 | const { quasi } = node; 79 | const tokenBefore = sourceCode.getTokenBefore(quasi) as TSESTree.Token | null; 80 | if (tokenBefore && quasi.loc && tokenBefore.loc.end.line !== quasi.loc.start.line) { 81 | const loc = { 82 | start: quasi.loc.start, 83 | end: { 84 | line: quasi.loc.start.line, 85 | column: quasi.loc.start.column + 1 86 | } 87 | }; 88 | context.report({ 89 | messageId: 'moveTemplateLiteral', 90 | data: { 91 | line: tokenBefore.loc.start.line.toString() 92 | }, 93 | loc 94 | }); 95 | } 96 | } 97 | }; 98 | } 99 | }); 100 | 101 | function getCallee(call: TSESTree.CallExpression) { 102 | const node = call; 103 | return (node.typeArguments ?? node.callee) as TSESTree.Node; 104 | } 105 | 106 | function isClosingParen(token: TSESTree.Token): token is TSESTree.PunctuatorToken { 107 | return token.type === AST_TOKEN_TYPES.Punctuator && token.value === ')'; 108 | } 109 | 110 | function isNotClosingParen(token: TSESTree.Token): boolean { 111 | return !isClosingParen(token); 112 | } 113 | -------------------------------------------------------------------------------- /packages/node/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { FlatESLintConfigItem } from '@eslint-sukka/shared'; 2 | 3 | import { RESTRICTED_IMPORT_NODE_REQUIRE, getPackageJson, globals } from '@eslint-sukka/shared'; 4 | 5 | import eslint_plugin_sukka from '@eslint-sukka/eslint-plugin-sukka-full'; 6 | import eslint_plugin_n from 'eslint-plugin-n'; 7 | import { UNSAFE_excludeJsonYamlFiles } from '@eslint-sukka/shared'; 8 | 9 | export interface OptionsNode { 10 | strict?: boolean, 11 | module?: boolean, 12 | files?: FlatESLintConfigItem['files'], 13 | hasTypeScript?: boolean, 14 | hasReact?: boolean 15 | } 16 | 17 | export function node(options: OptionsNode = {}): FlatESLintConfigItem[] { 18 | const isModule = options.module ?? (getPackageJson()?.type === 'module'); 19 | 20 | // this is safe because Node.js specific rules should not apply to JSON/YAML files 21 | const configs: FlatESLintConfigItem[] = UNSAFE_excludeJsonYamlFiles([ 22 | eslint_plugin_sukka.configs.node, 23 | { 24 | name: '@eslint-sukka/node base', 25 | plugins: { 26 | n: eslint_plugin_n 27 | }, 28 | rules: { 29 | 'n/no-unsupported-features/es-syntax': 'off', 30 | 31 | // enforces error handling in callbacks (node environment) 32 | 'handle-callback-err': 'off', 33 | 34 | // disallow use of new operator with the require function 35 | // replaced by eslint-plugin-n 36 | 'no-new-require': 'off', 37 | 'n/no-new-require': 'error', 38 | 39 | // disallow string concatenation with __dirname and __filename 40 | // https://eslint.org/docs/rules/no-path-concat 41 | // replaced by eslint-plugin-n 42 | 'no-path-concat': 'off', 43 | 'n/no-path-concat': 'error', 44 | 45 | // disallow use of process.env 46 | 'no-process-env': 'off', 47 | 48 | // disallow process.exit() 49 | // replaced by sukka/unicorn/no-process-exit 50 | 'n/no-process-exit': 'off', // replaced by sukka/unicorn/no-process-exit 51 | 52 | // restrict usage of specified node modules 53 | 'no-restricted-modules': 'off', // covered by ts presets 54 | 55 | // disallow use of synchronous methods (off by default) 56 | 'no-sync': 'off', 57 | 58 | // I still use them 59 | 'n/no-deprecated-api': ['error', { 60 | ignoreModuleItems: ['url.parse', 'url.resolve'] 61 | }], 62 | 63 | // eslint-plugin-i & eslint-plugin-unused-import will get me covered 64 | 'n/no-missing-import': 'off', 65 | 'n/no-missing-require': 'off', 66 | // replaced by i/no-extraneous-dependencies 67 | 'n/no-extraneous-import': 'off', 68 | 'n/no-extraneous-require': 'off', 69 | 70 | 'n/no-restricted-require': options.hasTypeScript ? 'off' /** covered by ts presets */ : ['error', RESTRICTED_IMPORT_NODE_REQUIRE], 71 | 'n/prefer-node-protocol': 'off', // slower 72 | 73 | 'n/process-exit-as-throw': 'error', 74 | 75 | // prefer-global 76 | 'n/prefer-global/buffer': ['error', 'never'], // bundler can easily catch this to prevent runtime error 77 | 'n/prefer-global/console': ['error', 'always'], // console is generally available 78 | 'n/prefer-global/process': options.hasReact ? 'off' : ['error', 'never'], // bundler can easily catch this to prevent runtime error 79 | 'n/prefer-global/text-decoder': ['error', 'always'], // text-decoder is generally available 80 | 'n/prefer-global/text-encoder': ['error', 'always'], // text-encoder is generally available 81 | 'n/prefer-global/url': ['error', 'always'], // url is generally available 82 | 'n/prefer-global/url-search-params': ['error', 'always'], // url-search-params is generally available 83 | 84 | // 'n/no-top-level-await': 'error', -- antfu's rule was used instead (applies to all projects) 85 | 86 | // prefer-promise 87 | 'n/prefer-promises/dns': 'error' 88 | }, 89 | languageOptions: { 90 | globals: globals.node 91 | } 92 | } 93 | ]); 94 | 95 | if (options.strict !== false) { 96 | configs.push({ 97 | name: '@eslint-sukka/node use strict', 98 | files: options.files ?? (isModule ? ['*.cjs', '.*.cjs'] : ['*.cjs', '.*.cjs', '*.js', '.*.js']), 99 | rules: { 100 | // enable strict mode for cjs 101 | strict: ['warn', 'global'] 102 | } 103 | }); 104 | } 105 | 106 | return configs; 107 | } 108 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/only-await-thenable/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | // https://sonarsource.github.io/rspec/#/rspec/S4123/javascript 21 | 22 | import type ts from 'typescript'; 23 | import { SyntaxKind as tsSyntaxKind, TypeFlags as tsTypeFlags } from 'typescript'; 24 | import { createRule, ensureParserWithTypeInformation } from '@eslint-sukka/shared'; 25 | import { AST_NODE_TYPES } from '@typescript-eslint/types'; 26 | import type { TSESTree } from '@typescript-eslint/types'; 27 | import type { ParserServicesWithTypeInformation } from '@typescript-eslint/utils'; 28 | import { getTypeFromTreeNode } from '../no-for-in-iterable'; 29 | 30 | export default createRule({ 31 | name: 'only-await-thenable', 32 | meta: { 33 | schema: [], 34 | type: 'suggestion', 35 | docs: { 36 | description: '"await" should only be used with promises', 37 | recommended: 'recommended', 38 | url: 'https://sonarsource.github.io/rspec/#/rspec/S4123/javascript' 39 | }, 40 | messages: { 41 | refactorAwait: 'Refactor this redundant \'await\' on a non-promise.' 42 | } 43 | }, 44 | create(context) { 45 | const services = context.sourceCode.parserServices; 46 | ensureParserWithTypeInformation(services); 47 | return { 48 | AwaitExpression(node: TSESTree.AwaitExpression) { 49 | const awaitedType = getTypeFromTreeNode(node.argument, services); 50 | if ( 51 | !isException(node, services) 52 | && !isThenable(awaitedType) 53 | && !isAny(awaitedType) 54 | && !isUnknown(awaitedType) 55 | && !isUnion(awaitedType) 56 | ) { 57 | context.report({ 58 | messageId: 'refactorAwait', 59 | node 60 | }); 61 | } 62 | } 63 | }; 64 | } 65 | }); 66 | 67 | /** 68 | * If the awaited expression is a call expression, check if it is a call to a function with 69 | * a JSDoc containing a return tag. 70 | */ 71 | function isException(node: TSESTree.AwaitExpression, services: ParserServicesWithTypeInformation) { 72 | if (node.argument.type !== AST_NODE_TYPES.CallExpression) { 73 | return false; 74 | } 75 | const signature = getSignatureFromCallee(node.argument, services); 76 | return signature?.declaration && hasJsDocReturn(signature.declaration); 77 | } 78 | 79 | const RETURN_TAGS = new Set(['return', 'returns']); 80 | 81 | function hasJsDocReturn(declaration: ts.Declaration & { jsDoc?: ts.JSDoc[] }) { 82 | if (!declaration.jsDoc) { 83 | return false; 84 | } 85 | for (const jsDoc of declaration.jsDoc) { 86 | if (jsDoc.tags?.some(tag => RETURN_TAGS.has(tag.tagName.escapedText.toString()))) { 87 | return true; 88 | } 89 | } 90 | return false; 91 | } 92 | 93 | export function getSignatureFromCallee(node: TSESTree.Node, services: ParserServicesWithTypeInformation) { 94 | const checker = services.program.getTypeChecker(); 95 | return checker.getResolvedSignature( 96 | services.esTreeNodeToTSNodeMap.get(node) as ts.CallLikeExpression 97 | ); 98 | } 99 | 100 | function isThenable(type: ts.Type) { 101 | const thenProperty = type.getProperty('then'); 102 | return thenProperty?.declarations?.some( 103 | d => d.kind === tsSyntaxKind.MethodSignature 104 | || d.kind === tsSyntaxKind.MethodDeclaration 105 | || d.kind === tsSyntaxKind.PropertyDeclaration 106 | ); 107 | } 108 | 109 | function isAny(type: ts.Type) { 110 | return Boolean(type.flags & tsTypeFlags.Any); 111 | } 112 | 113 | function isUnknown(type: ts.Type) { 114 | return Boolean(type.flags & tsTypeFlags.Unknown); 115 | } 116 | 117 | function isUnion(type: ts.Type) { 118 | return Boolean(type.flags & tsTypeFlags.Union); 119 | } 120 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/comma-or-logical-or-case/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | import { runTest } from '@eslint-sukka/internal'; 21 | import mod from '.'; 22 | import { dedent } from 'ts-dedent'; 23 | 24 | runTest({ 25 | module: mod, 26 | valid: [ 27 | dedent` 28 | switch (a) { 29 | case 0: // OK 30 | case 1: // OK 31 | case "a" && "b": // OK, no logical or 32 | foo1(); 33 | case "a" && "b" || "c": // OK 34 | foo2(); 35 | break; 36 | } 37 | `, 38 | dedent` 39 | switch (true) { 40 | case cond1() || cond2(): 41 | break; 42 | } 43 | `, 44 | dedent` 45 | switch (a) { 46 | case 2: 47 | switch (true) { 48 | case cond3() || cond4(): 49 | break; 50 | } 51 | break; 52 | } 53 | ` 54 | ], 55 | invalid: [ 56 | { 57 | code: `switch (a) { 58 | case 2,3: // Noncompliant 59 | foo2(); 60 | break; 61 | case "a","b","c","d": // Noncompliant 62 | foo3(); 63 | break; 64 | case bar(), baz(): // Noncompliant 65 | foo(); 66 | break; 67 | default: 68 | foo4(); 69 | }`, 70 | errors: [ 71 | { 72 | messageId: 'specifyCase', 73 | data: { 74 | nesting: 2, 75 | expression: '3' 76 | }, 77 | line: 2, 78 | endLine: 2, 79 | column: 19, 80 | endColumn: 22 81 | }, 82 | { 83 | messageId: 'specifyCase', 84 | data: { 85 | nesting: 4, 86 | expression: 'd' 87 | }, 88 | line: 5, 89 | endLine: 5, 90 | column: 19, 91 | endColumn: 34 92 | }, 93 | { 94 | messageId: 'specifyCase', 95 | data: { 96 | nesting: 2, 97 | expression: 'baz()' 98 | }, 99 | line: 8, 100 | endLine: 8, 101 | column: 19, 102 | endColumn: 31 103 | } 104 | ] 105 | }, 106 | { 107 | code: `switch (a) { 108 | case 2 || 3: // Noncompliant 109 | foo2(); 110 | break; 111 | case "a" || "b" || "c" || "d": // Noncompliant 112 | foo3(); 113 | break; 114 | case bar() || baz(): // Noncompliant 115 | foo(); 116 | break; 117 | default: 118 | foo4(); 119 | }`, 120 | errors: [ 121 | { 122 | messageId: 'specifyCase', 123 | data: { 124 | nesting: 2, 125 | expression: '2' 126 | }, 127 | line: 2, 128 | endLine: 2, 129 | column: 19, 130 | endColumn: 25 131 | }, 132 | { 133 | messageId: 'specifyCase', 134 | data: { 135 | nesting: 4, 136 | expression: 'a' 137 | }, 138 | line: 5, 139 | endLine: 5, 140 | column: 19, 141 | endColumn: 43 142 | }, 143 | { 144 | messageId: 'specifyCase', 145 | data: { 146 | nesting: 2, 147 | expression: 'bar()' 148 | }, 149 | line: 8, 150 | endLine: 8, 151 | column: 19, 152 | endColumn: 33 153 | } 154 | ] 155 | }, 156 | { 157 | code: `switch (true) { 158 | case cond1() || cond2(): 159 | switch (a) { 160 | case cond3() || cond4(): 161 | break; 162 | } 163 | break; 164 | }`, 165 | errors: [{ messageId: 'specifyCase' }] 166 | } 167 | ] 168 | }); 169 | -------------------------------------------------------------------------------- /packages/eslint-config-sukka/scripts/codegen.ts: -------------------------------------------------------------------------------- 1 | import { javascript as eslint_config_sukka_js } from '../src/modules/javascript'; 2 | 3 | import ts_eslint_plugin from '@typescript-eslint/eslint-plugin'; 4 | 5 | import fs from 'node:fs'; 6 | import path from 'node:path'; 7 | 8 | const DISABLED_RULES = new Set([ 9 | 'no-redeclare', 10 | 'no-dupe-class-members' 11 | ]); 12 | 13 | (async () => { 14 | const { default: stringifyObject } = await import('stringify-object'); 15 | 16 | const TS_ESLINT_BASE_RULES_TO_BE_OVERRIDDEN = new Map( 17 | Object.entries(ts_eslint_plugin.rules) 18 | // https://github.com/sweepline/eslint-plugin-unused-imports/blob/2563edf7d7894e0cc05163d9e9180bc3c56471cc/lib/rules/no-unused-imports.js#L15 19 | .reduce>((acc, [ruleName, rule]) => { 20 | if ( 21 | 'meta' in rule 22 | && 'docs' in rule.meta && typeof rule.meta.docs === 'object' 23 | && 'extendsBaseRule' in rule.meta.docs 24 | && rule.meta.docs.extendsBaseRule != null 25 | ) { 26 | acc.push([ 27 | typeof rule.meta.docs.extendsBaseRule === 'string' 28 | ? rule.meta.docs.extendsBaseRule 29 | : ruleName, 30 | ruleName 31 | ]); 32 | } 33 | return acc; 34 | }, []) 35 | ); 36 | 37 | const rules = Object.fromEntries( 38 | Object.entries( 39 | (await eslint_config_sukka_js()) 40 | .reduce((acc, cur) => ({ ...acc, ...cur.rules }), {}) 41 | ) 42 | // .filter(([, value]) => { 43 | // if (typeof value === 'string') { 44 | // return value !== 'off'; 45 | // } 46 | // if (typeof value === 'number') { 47 | // return value !== 0; 48 | // } 49 | // if (Array.isArray(value)) { 50 | // return value.length !== 0 && value[0] !== 'off'; 51 | // } 52 | // return true; 53 | // }) 54 | .reduce((acc, [baseRuleName, value]) => { 55 | switch (baseRuleName) { 56 | case 'camelcase': 57 | case 'no-restricted-imports': { 58 | // disable camelcase directly, use custom @typescript-eslint/naming-convention instead 59 | // disable no-restricted-imports directly, use @typescript-eslint/no-restricted-imports instead 60 | 61 | // @ts-expect-error -- no type overlap between eslint and typescript-eslint 62 | acc.push([baseRuleName, 'off']); 63 | 64 | break; 65 | } 66 | case 'sukka/no-return-await': { 67 | acc.push( 68 | // @ts-expect-error -- no type overlap between eslint and typescript-eslint 69 | [baseRuleName, 'off'], 70 | ['@typescript-eslint/return-await', ['error', 'in-try-catch']] 71 | ); 72 | 73 | break; 74 | } 75 | case 'no-loss-of-precision': { 76 | // do nothing 77 | 78 | // @typescript-eslint/no-loss-of-precision is deprecated 79 | // The original rule is recommended instead 80 | 81 | break; 82 | } 83 | default: if (TS_ESLINT_BASE_RULES_TO_BE_OVERRIDDEN.has(baseRuleName)) { 84 | const replacementRulename = TS_ESLINT_BASE_RULES_TO_BE_OVERRIDDEN.get(baseRuleName)!; 85 | acc.push( 86 | // @ts-expect-error -- no type overlap between eslint and typescript-eslint 87 | [baseRuleName, 'off'], 88 | [`@typescript-eslint/${replacementRulename}`, DISABLED_RULES.has(baseRuleName) ? 'off' : value] 89 | ); 90 | } else if ( 91 | baseRuleName.startsWith('autofix/') 92 | && TS_ESLINT_BASE_RULES_TO_BE_OVERRIDDEN.has(baseRuleName.slice(8))) { 93 | const replacementRulename = TS_ESLINT_BASE_RULES_TO_BE_OVERRIDDEN.get(baseRuleName.slice(8))!; 94 | 95 | acc.push( 96 | // @ts-expect-error -- no type overlap between eslint and typescript-eslint 97 | [baseRuleName, 'off'], 98 | [`autofix/${baseRuleName}`, 'off'], 99 | [`@typescript-eslint/${replacementRulename}`, value] 100 | ); 101 | } 102 | } 103 | return acc; 104 | }, []) 105 | ); 106 | 107 | fs.writeFileSync( 108 | path.resolve(__dirname, '../src/modules/_generated_typescript_overrides.ts'), 109 | [ 110 | '// This file is generated by scripts/codegen.ts', 111 | '// DO NOT EDIT THIS FILE MANUALLY', 112 | 'import type { SukkaESLintRuleConfig } from \'@eslint-sukka/shared\';', 113 | '', 114 | 'export const generated_typescript_overrides: SukkaESLintRuleConfig = {', 115 | ` rules: ${stringifyObject(rules, { indent: ' ', singleQuotes: true }).split('\n').map((line) => ` ${line}`).join('\n').trimStart()}`, 116 | '};', 117 | '' 118 | ].join('\n') 119 | ); 120 | })(); 121 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/object-format/index.ts: -------------------------------------------------------------------------------- 1 | import type { RuleFix } from '@typescript-eslint/utils/ts-eslint'; 2 | import { createRule } from '@eslint-sukka/shared'; 3 | import { detectEol } from 'foxts/detect-eol'; 4 | import type { TSESTree } from '@typescript-eslint/types'; 5 | 6 | export interface Options { 7 | maxLineLength?: number, 8 | maxObjectSize?: number 9 | }; 10 | 11 | export type MessageIds = 'preferMultiline' | 'preferSingleLine'; 12 | 13 | export default createRule({ 14 | name: 'object-format', 15 | meta: { 16 | fixable: 'whitespace', 17 | schema: [{ 18 | type: 'object', 19 | properties: { 20 | maxLineLength: { 21 | type: 'number', 22 | default: 80 23 | }, 24 | maxObjectSize: { 25 | type: 'number', 26 | default: 3 27 | } 28 | }, 29 | additionalProperties: false 30 | }] as const, 31 | docs: { 32 | description: 'Requires multiline or single-line object format.', 33 | recommended: 'stylistic' 34 | }, 35 | messages: { 36 | preferMultiline: 'Prefer multiline object literal', 37 | preferSingleLine: 'Prefer single-line object literal' 38 | }, 39 | type: 'layout' 40 | }, 41 | create(context, options = {}) { 42 | const code = context.sourceCode.getText(); 43 | 44 | function getText( 45 | mixed: /* [number, number] | */ TSESTree.Comment | TSESTree.Node | number 46 | ): string { 47 | if (typeof mixed === 'number') return code.slice(mixed); 48 | // if (Array.isArray(mixed)) return code.slice(...mixed); 49 | return code.slice(...mixed.range); 50 | } 51 | const getFullText = (node: TSESTree.Node) => code.slice( 52 | Math.min(node.range[0], ...context.sourceCode.getCommentsBefore(node).map(comment => comment.range[0])), 53 | node.range[1] 54 | ); 55 | 56 | const eol = detectEol(code); 57 | const comma = ','; 58 | const commaEol = `,${eol}`; 59 | 60 | const { maxLineLength = 80, maxObjectSize = 3 } = options; 61 | 62 | const hasTrailingComment = createHasTrailingComment(code); 63 | 64 | return { 65 | ObjectExpression(node: TSESTree.ObjectExpression) { 66 | const texts = node.properties.map(property => getFullText(property).trim()); 67 | 68 | if (texts.length > 0) { 69 | const text = context.sourceCode.getText(node); 70 | 71 | const expectMultiline = texts.length > maxObjectSize 72 | || texts.some(isMultilineString) 73 | || node.properties.some(hasTrailingComment); 74 | 75 | const expectSingleLine = !expectMultiline; 76 | 77 | const gotMultiline = isMultilineString(text); 78 | const gotSingleLine = isSingleLineString(text); 79 | 80 | if (expectMultiline && gotSingleLine) { 81 | context.report({ 82 | fix(): RuleFix { 83 | return { 84 | range: node.range, 85 | text: `{${eol}${texts.join(commaEol)}${eol}}` 86 | }; 87 | }, 88 | messageId: 'preferMultiline', 89 | node 90 | }); 91 | } 92 | 93 | if ( 94 | expectSingleLine 95 | && gotMultiline 96 | && predictedLength() <= maxLineLength 97 | ) { 98 | context.report({ 99 | fix(): RuleFix { 100 | return { 101 | range: node.range, 102 | text: `{${texts.join(comma)}}` 103 | }; 104 | }, 105 | messageId: 'preferSingleLine', 106 | node 107 | }); 108 | } 109 | } 110 | 111 | function predictedLength(): number { 112 | const headPosition: TSESTree.Position = context.sourceCode.getLocFromIndex(node.range[0]); 113 | const contents = texts.reduce((acc, cur) => acc + cur.length, 0); 114 | 115 | const commas = 2 * (texts.length - 1); 116 | 117 | const brackets = 4; 118 | 119 | const tail = getText(node.range[1]) 120 | .split(/\r\n|\n/u)[0] 121 | // eslint-disable-next-line regexp/optimal-quantifier-concatenation -- Wait for https://github.com/ota-meshi/eslint-plugin-regexp/issues/451 122 | .replace(/^((?: as const)?\S*).*/u, '$1') 123 | .length; 124 | 125 | return headPosition.column + contents + commas + brackets + tail; 126 | } 127 | } 128 | }; 129 | } 130 | }); 131 | 132 | function isMultilineString(str: string): boolean { 133 | return str.includes('\n'); 134 | } 135 | 136 | function isSingleLineString(str: string): boolean { 137 | return !str.includes('\n'); 138 | } 139 | 140 | function createHasTrailingComment(code: string) { 141 | return (node: TSESTree.BaseNode) => code 142 | .slice(node.range[1]) 143 | .trimStart() 144 | .startsWith('//'); 145 | }; 146 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-equals-in-for-termination/index.test.ts: -------------------------------------------------------------------------------- 1 | import { runTest } from '@eslint-sukka/internal'; 2 | import mod from '.'; 3 | 4 | runTest({ 5 | module: mod, 6 | valid: [ 7 | 'for (var i = 0; i > 10; i += 1) { }', // Compliant, not an equality in condition 8 | 'for (var i = 0; i != 10; i *= 1) { }', // Compliant, not an inc/dec update 9 | 10 | 'for (var i = 0; i != 10; i++) { }', // Compliant, trivial update operation increasing from 0 to 10 11 | 'for (var i = 10; i != 0; i--) { }', // Compliant, trivial update operation decreasing from 10 to 0 12 | 'for (var i = 0; i != 10; i += 1) { }', // Compliant, trivial update operation 13 | 'for (var i = 10; i != 0; i -= 1) { }', // Compliant, trivial update operation 14 | 'for (var i = 10; i !== 0; i -= 1) { }', // Compliant, trivial update operation 15 | 16 | 'var j = 20; for (j = 0; j != 10; j++) { }', // Compliant, trivial update operation 17 | 18 | // Compliant tests: non trivial condition exception 19 | 'for (i = 0; checkSet[i] != null; i++) { }', 20 | 'for (i = 0, k = 0; j != null; i++, k--) { }', // Non trivial, j is not updated 21 | 'for (; checkSet[i] != null; i++) { }', 22 | 'for (i = 0; foo(i) == 42; i++) { }', 23 | 'for (cur = event.target; cur != this; cur = cur.parentNode || this) { }', 24 | 'for (var i = 0; ; i += 1) { }', // Compliant, no condition 25 | 'for (var i = 0; i != 10;) { }', // Compliant, no update 26 | 'for (var i = 0; i >= 10;) { }' // Compliant, no update 27 | ], 28 | invalid: [ 29 | { 30 | // Noncompliant {{Replace '!=' operator with one of '<=', '>=', '<', or '>' comparison operators.}} 31 | code: 'for (var i = 0; i != 2; i += 2) { }', 32 | errors: [{ 33 | messageId: 'replaceOperator', 34 | data: { operator: '!=' } 35 | }] 36 | }, 37 | { 38 | code: 'for (i = 0; i == 2; i += 2) { }', // Noncompliant 39 | errors: [{ 40 | messageId: 'replaceOperator', 41 | data: { operator: '==' } 42 | }] 43 | }, 44 | { 45 | code: 'for (i = 10; i == 0; i--) { }', // Noncompliant 46 | errors: [{ 47 | messageId: 'replaceOperator', 48 | data: { operator: '==' } 49 | }] 50 | }, 51 | { 52 | code: 'for (let i = 0; i === 10; i++) { }', // Noncompliant 53 | errors: [{ 54 | messageId: 'replaceOperator', 55 | data: { operator: '===' } 56 | }] 57 | }, 58 | { 59 | code: 'for (i = from, j = 0; i != to; i += dir, j++) { }', // Noncompliant 60 | errors: [{ 61 | messageId: 'replaceOperator', 62 | data: { operator: '!=' } 63 | }] 64 | }, 65 | 66 | // even if trivial update operation, we have equality in condition 67 | 68 | // not a trivial update 69 | { 70 | code: 'for (let i = 0; i !== 2; i += 2) { }', // Noncompliant 71 | errors: [{ 72 | messageId: 'replaceOperator', 73 | data: { operator: '!==' } 74 | }] 75 | }, 76 | 77 | // trivial update, but init is higher than stop and update is increasing 78 | { 79 | code: 'for (let i = 10; i != 0; i++) { }', // Noncompliant 80 | errors: [{ 81 | messageId: 'replaceOperator', 82 | data: { operator: '!=' } 83 | }] 84 | }, 85 | 86 | // trivial update, but init is lower than stop and update is decreasing 87 | { 88 | code: 'for (let i = 0; i != 10; i -= 1) { }', // Noncompliant 89 | errors: [{ 90 | messageId: 'replaceOperator', 91 | data: { operator: '!=' } 92 | }] 93 | }, 94 | 95 | // trivial update operation with wrong init 96 | { 97 | code: 'for (let i = \'a\'; i != 0; i -= 1) { }', // Noncompliant 98 | errors: [{ 99 | messageId: 'replaceOperator', 100 | data: { operator: '!=' } 101 | }] 102 | }, 103 | 104 | // trivial update, but init is lower than stop 105 | 106 | { 107 | code: 'let j = 20; for (j = 0; j != 10; j--) { }', // Noncompliant 108 | errors: [{ 109 | messageId: 'replaceOperator', 110 | data: { operator: '!=' } 111 | }] 112 | }, 113 | 114 | // not a non-trivial condition exception, updated counter is not in the condition 115 | { 116 | code: 'for (i = 0, k = 0; k != null; i++, k--) { }', // Noncompliant 117 | errors: [{ 118 | messageId: 'replaceOperator', 119 | data: { operator: '!=' } 120 | }] 121 | }, 122 | 123 | { 124 | code: 'for (let i = 0; i != 10; i += 1) { i++; }', // Noncompliant changes to counter -> no exception 125 | errors: [{ 126 | messageId: 'replaceOperator', 127 | data: { operator: '!=' } 128 | }] 129 | }, 130 | 131 | { 132 | code: 'let iii = 0; for (let i = 0; iii != 10; iii += 1) { iii++; }', // Noncompliant changes to counter -> no exception 133 | errors: [{ 134 | messageId: 'replaceOperator', 135 | data: { operator: '!=' } 136 | }] 137 | } 138 | ] 139 | }); 140 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/call-argument-line/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | import mod from '.'; 21 | import { runTest } from '@eslint-sukka/internal'; 22 | 23 | runTest({ 24 | module: mod, 25 | valid: [ 26 | { 27 | code: ` 28 | foo(bar); 29 | ` 30 | }, 31 | { 32 | code: ` 33 | foo(bar)(baz)(qux); 34 | ` 35 | }, 36 | { 37 | code: ` 38 | foo(bar) 39 | (baz) 40 | (qux); 41 | ` 42 | }, 43 | { 44 | code: ` 45 | foo 46 | (bar, baz, qux); 47 | ` 48 | }, 49 | { 50 | code: ` 51 | (foo 52 | )((bar)); 53 | ` 54 | }, 55 | { 56 | code: ` 57 | const MyContext = React.createContext<{ 58 | foo: number, 59 | bar: number, 60 | }>({ foo: 1, bar: 2 }); 61 | ` 62 | }, 63 | { 64 | code: ` 65 | const MyContext = React.createContext 66 | ('foo'); 67 | ` 68 | } 69 | ], 70 | invalid: [ 71 | { 72 | code: ` 73 | foo 74 | (bar);`, 75 | errors: [ 76 | { 77 | messageId: 'moveArguments', 78 | data: { 79 | line: 2 80 | }, 81 | line: 3, 82 | endLine: 3, 83 | column: 9, 84 | endColumn: 14 85 | } 86 | ] 87 | }, 88 | { 89 | code: `(function iieflike(factory){} 90 | (function () { 91 | //A lot of code... 92 | } 93 | ))`, 94 | errors: [ 95 | { 96 | messageId: 'moveArguments', 97 | data: { 98 | line: 1 99 | }, 100 | line: 2, 101 | column: 7 102 | } 103 | ] 104 | }, 105 | { 106 | code: ` 107 | foo(bar)[baz] 108 | (qux); 109 | `, 110 | errors: [{ messageId: 'moveArguments' }] 111 | }, 112 | { 113 | code: ` 114 | var a = b 115 | (x || y).doSomething() 116 | `, 117 | errors: [{ messageId: 'moveArguments' }] 118 | }, 119 | { 120 | code: ` 121 | var a = (a || b) 122 | (x || y).doSomething() 123 | `, 124 | errors: [{ messageId: 'moveArguments' }] 125 | }, 126 | { 127 | code: ` 128 | var a = (a || b) 129 | (x).doSomething() 130 | `, 131 | errors: [{ messageId: 'moveArguments' }] 132 | }, 133 | { 134 | code: ` 135 | var a = b 136 | (x || y).doSomething() 137 | `, 138 | errors: [{ messageId: 'moveArguments' }] 139 | }, 140 | { 141 | code: 'let x = function() {}\n `hello`', 142 | errors: [ 143 | { 144 | messageId: 'moveTemplateLiteral', 145 | data: { 146 | line: 1 147 | }, 148 | line: 2, 149 | endLine: 2, 150 | column: 2, 151 | endColumn: 3 152 | } 153 | ] 154 | }, 155 | { 156 | code: 'let x = function() {}\nx\n`hello`', 157 | errors: [ 158 | { 159 | messageId: 'moveTemplateLiteral', 160 | data: { 161 | line: 2 162 | }, 163 | line: 3, 164 | endLine: 3, 165 | column: 1, 166 | endColumn: 2 167 | } 168 | ] 169 | }, 170 | { 171 | code: ` 172 | const MyContext = React.createContext<{ 173 | foo: number, 174 | bar: number, 175 | }> 176 | ({ foo: 1, bar: 2 }); 177 | `, 178 | errors: [ 179 | { 180 | messageId: 'moveArguments', 181 | data: { 182 | line: 5 183 | }, 184 | line: 6, 185 | endLine: 6, 186 | column: 7, 187 | endColumn: 27 188 | } 189 | ] 190 | }, 191 | { 192 | code: ` 193 | const MyContext = React.createContext 194 | ('foo'); 195 | `, 196 | errors: [ 197 | { 198 | messageId: 'moveArguments', 199 | data: { 200 | line: 2 201 | }, 202 | line: 3, 203 | endLine: 3, 204 | column: 7, 205 | endColumn: 14 206 | } 207 | ] 208 | } 209 | ] 210 | }); 211 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-for-in-iterable/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | // https://sonarsource.github.io/rspec/#/rspec/S4139/javascript 21 | 22 | import { createRule, ensureParserWithTypeInformation } from '@eslint-sukka/shared'; 23 | import type ts from 'typescript'; 24 | import { TypeFlags as tsTypeFlags } from 'typescript'; 25 | import type { ParserServicesWithTypeInformation, TSESTree } from '@typescript-eslint/utils'; 26 | 27 | export default createRule({ 28 | name: 'no-for-in-iterable', 29 | meta: { 30 | schema: [], 31 | type: 'suggestion', 32 | docs: { 33 | description: '"for in" should not be used with iterables', 34 | recommended: 'strict', 35 | url: 'https://sonarsource.github.io/rspec/#/rspec/S4139/javascript' 36 | }, 37 | messages: { 38 | useForOf: 'Use "for...of" to iterate over this "{{iterable}}".' 39 | } 40 | }, 41 | create(context) { 42 | const services = context.sourceCode.parserServices; 43 | const isIterable = (type: ts.Type) => { 44 | ensureParserWithTypeInformation(services); 45 | return isCollection(type) || isStringType(type) || isArrayLikeType(type, services); 46 | }; 47 | 48 | return { 49 | ForInStatement(node) { 50 | ensureParserWithTypeInformation(services); 51 | const type = getTypeFromTreeNode(node.right, services); 52 | if (isIterable(type)) { 53 | const iterable = (type.symbol as ts.Symbol | undefined) ? type.symbol.name : 'String'; 54 | context.report({ 55 | messageId: 'useForOf', 56 | data: { iterable }, 57 | loc: context.sourceCode.getFirstToken(node)!.loc 58 | }); 59 | } 60 | } 61 | }; 62 | } 63 | }); 64 | 65 | export function isStringType(type: ts.Type) { 66 | return (type.flags & tsTypeFlags.StringLike) > 0 || (type.symbol as ts.Symbol | undefined)?.name === 'String'; 67 | } 68 | 69 | /** 70 | * This function checks if a type may correspond to an array type. Beyond simple array types, it will also 71 | * consider the union of array types and generic types extending an array type. 72 | * @param type A type to check 73 | * @param services The services used to get access to the TypeScript type checker 74 | */ 75 | export function isArrayLikeType(type: ts.Type, services: ParserServicesWithTypeInformation) { 76 | const checker = services.program.getTypeChecker(); 77 | const constrained = checker.getBaseConstraintOfType(type); 78 | return isArrayOrUnionOfArrayType(constrained ?? type, services); 79 | } 80 | 81 | function isArrayOrUnionOfArrayType(type: ts.Type, services: ParserServicesWithTypeInformation): boolean { 82 | for (const part of getUnionTypes(type)) { 83 | if (!isArrayType(part, services)) { 84 | return false; 85 | } 86 | } 87 | 88 | return true; 89 | } 90 | 91 | // Internal TS API 92 | function isArrayType(type: ts.Type, services: ParserServicesWithTypeInformation): type is ts.TypeReference { 93 | const checker = services.program.getTypeChecker(); 94 | return ( 95 | 'isArrayType' in checker 96 | && typeof checker.isArrayType === 'function' 97 | && checker.isArrayType(type) 98 | ); 99 | } 100 | 101 | /** 102 | * Returns an array of the union types if the provided type is a union. 103 | * Otherwise, returns an array containing the provided type as its unique element. 104 | * @param type A TypeScript type. 105 | * @return An array of types. It's never empty. 106 | */ 107 | export function getUnionTypes(type: ts.Type): ts.Type[] { 108 | return type.isUnion() ? type.types : [type]; 109 | } 110 | 111 | export function getTypeFromTreeNode(node: TSESTree.Node, services: ParserServicesWithTypeInformation) { 112 | const checker = services.program.getTypeChecker(); 113 | return checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(node)); 114 | } 115 | 116 | const collectionTypes = new Set([ 117 | 'Array', 118 | 'Int8Array', 119 | 'Uint8Array', 120 | 'Uint8ClampedArray', 121 | 'Int16Array', 122 | 'Uint16Array', 123 | 'Int32Array', 124 | 'Uint32Array', 125 | 'Float32Array', 126 | 'Float64Array', 127 | 'BigInt64Array', 128 | 'BigUint64Array', 129 | 'Set', 130 | 'Map' 131 | ]); 132 | 133 | function isCollection(type: ts.Type) { 134 | return ( 135 | type.symbol 136 | && collectionTypes.has(type.symbol.name) 137 | ); 138 | } 139 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-undefined-optional-parameters/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | // https://sonarsource.github.io/rspec/#/rspec/S4623/javascript 21 | 22 | import { AST_NODE_TYPES } from '@typescript-eslint/types'; 23 | import type { TSESTree } from '@typescript-eslint/types'; 24 | import type { ParserServicesWithTypeInformation } from '@typescript-eslint/utils'; 25 | import type ts from 'typescript'; 26 | import { SyntaxKind as tsSyntaxKind } from 'typescript'; 27 | import { createRule, ensureParserWithTypeInformation } from '@eslint-sukka/shared'; 28 | 29 | export default createRule({ 30 | name: 'no-undefined-optional-parameters', 31 | meta: { 32 | schema: [], 33 | type: 'suggestion', 34 | docs: { 35 | description: '"undefined" should not be passed as the value of optional parameters', 36 | recommended: 'recommended', 37 | url: 'https://sonarsource.github.io/rspec/#/rspec/S4623/javascript', 38 | requiresTypeChecking: true 39 | }, 40 | fixable: 'code', 41 | hasSuggestions: true, 42 | messages: { 43 | removeUndefined: 'Remove this redundant "undefined".', 44 | suggestRemoveUndefined: 'Remove this redundant argument' 45 | } 46 | }, 47 | create(context) { 48 | const services = context.sourceCode.parserServices; 49 | ensureParserWithTypeInformation(services); 50 | 51 | return { 52 | CallExpression(call: TSESTree.CallExpression) { 53 | const { arguments: args } = call; 54 | if (args.length === 0) { 55 | return; 56 | } 57 | 58 | const lastArgument = args[args.length - 1]; 59 | if (isUndefined(lastArgument) && isOptionalParameter(args.length - 1, call, services)) { 60 | context.report({ 61 | messageId: 'removeUndefined', 62 | node: lastArgument, 63 | suggest: [ 64 | { 65 | messageId: 'suggestRemoveUndefined', 66 | fix(fixer) { 67 | // eslint-disable-next-line sukka/unicorn/consistent-destructuring -- not necessary 68 | if (call.arguments.length === 1) { 69 | // eslint-disable-next-line sukka/unicorn/consistent-destructuring -- not necessary 70 | const openingParen = context.sourceCode.getTokenAfter(call.callee)! as TSESTree.Token; 71 | const closingParen = context.sourceCode.getLastToken(call)! as TSESTree.Token; 72 | const [, begin] = openingParen.range; 73 | const [end] = closingParen.range; 74 | return fixer.removeRange([begin, end]); 75 | } 76 | const [, begin] = args[args.length - 2].range; 77 | const [, end] = lastArgument.range; 78 | return fixer.removeRange([begin, end]); 79 | } 80 | } 81 | ] 82 | }); 83 | } 84 | } 85 | }; 86 | } 87 | }); 88 | 89 | function isOptionalParameter( 90 | paramIndex: number, 91 | node: TSESTree.CallExpression, 92 | services: ParserServicesWithTypeInformation 93 | ) { 94 | const signature = services.program 95 | .getTypeChecker() 96 | .getResolvedSignature( 97 | services.esTreeNodeToTSNodeMap.get(node as TSESTree.Node) as ts.CallLikeExpression 98 | ); 99 | if (signature) { 100 | const declaration = signature.declaration; 101 | if (declaration && isFunctionLikeDeclaration(declaration)) { 102 | const { parameters } = declaration; 103 | const parameter = parameters[paramIndex] as ts.ParameterDeclaration | undefined; 104 | return parameter && (parameter.initializer || parameter.questionToken); 105 | } 106 | } 107 | return false; 108 | } 109 | 110 | function isUndefined(node: TSESTree.Node): boolean { 111 | return node.type === AST_NODE_TYPES.Identifier && node.name === 'undefined'; 112 | } 113 | 114 | const functionSyntaxKind = new Set([ 115 | tsSyntaxKind.FunctionDeclaration, 116 | tsSyntaxKind.FunctionExpression, 117 | tsSyntaxKind.ArrowFunction, 118 | tsSyntaxKind.MethodDeclaration, 119 | tsSyntaxKind.Constructor, 120 | tsSyntaxKind.GetAccessor, 121 | tsSyntaxKind.SetAccessor 122 | ]); 123 | 124 | function isFunctionLikeDeclaration(declaration: ts.Declaration): declaration is ts.FunctionLikeDeclarationBase { 125 | return functionSyntaxKind.has(declaration.kind); 126 | } 127 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | import { GLOB_ALL_JSON, GLOB_YML } from './constants'; 2 | 3 | export * as constants from './constants'; 4 | export { getPackageJson, isDirectDependency } from './get-package-json'; 5 | export * from './restricted-import'; 6 | export { memo } from './memoize-eslint-plugin'; 7 | export type * from './types'; 8 | 9 | export { createRule, ensureParserWithTypeInformation, isParserWithTypeInformation } from './create-eslint-rule'; 10 | export type { RuleModule, ExportedRuleModule, RuleContext } from './create-eslint-rule'; 11 | 12 | import { EnforceExtension, ResolverFactory } from 'oxc-resolver'; 13 | import type { FlatESLintConfigItem } from './types'; 14 | 15 | import { castArray } from 'foxts/cast-array'; 16 | import { appendArrayInPlace } from 'foxts/append-array-in-place'; 17 | 18 | export const packageResolver = new ResolverFactory({ 19 | extensions: ['.mjs', '.cjs', '.js', '.json', '.node'], 20 | enforceExtension: EnforceExtension.Auto, 21 | conditionNames: ['node', 'import', 'require', 'default'], 22 | mainFields: ['module', 'main'], 23 | builtinModules: true 24 | }); 25 | 26 | export function isPackageExists(pkg: string, parent = process.cwd()) { 27 | const result = packageResolver.sync(parent, pkg); 28 | 29 | return Boolean(result.builtin || result.path); 30 | } 31 | 32 | export * as globals from './globals'; 33 | 34 | export function withFiles(configs: FlatESLintConfigItem, files: string | string[] | undefined | null): FlatESLintConfigItem; 35 | export function withFiles(configs: FlatESLintConfigItem[], files: string | string[] | undefined | null): FlatESLintConfigItem[]; 36 | export function withFiles(configs: FlatESLintConfigItem | FlatESLintConfigItem[], files: string | string[] | undefined | null) { 37 | if (files == null) { 38 | return configs; 39 | } 40 | 41 | files = castArray(files); 42 | 43 | if (!Array.isArray(configs)) { 44 | configs.files = files; 45 | return configs; 46 | } 47 | 48 | for (let i = 0, len = configs.length; i < len; i++) { 49 | configs[i].files = files; 50 | } 51 | 52 | return configs; 53 | } 54 | 55 | function addIgnore(config: FlatESLintConfigItem, ignores: string[]) { 56 | if (config.ignores) { 57 | appendArrayInPlace(config.ignores, ignores); 58 | } else if (Object.isExtensible(config)) { // @eslint/js Object.freeze before export 59 | config.ignores = ignores; 60 | } else { 61 | config = { 62 | ...config, 63 | ignores 64 | }; 65 | } 66 | return config; 67 | } 68 | 69 | export function withIgnores(configs: FlatESLintConfigItem, ignores: string | string[]): FlatESLintConfigItem; 70 | export function withIgnores(configs: FlatESLintConfigItem[], ignores: string | string[]): FlatESLintConfigItem[]; 71 | export function withIgnores(configs: FlatESLintConfigItem | FlatESLintConfigItem[], ignores: string | string[]) { 72 | ignores = castArray(ignores); 73 | 74 | if (!Array.isArray(configs)) { 75 | return addIgnore(configs, ignores); 76 | } 77 | 78 | for (let i = 0, len = configs.length; i < len; i++) { 79 | configs[i] = addIgnore(configs[i], ignores); 80 | } 81 | 82 | return configs; 83 | } 84 | 85 | const GLOB_NON_JS_TS = [ 86 | ...GLOB_ALL_JSON, 87 | ...GLOB_YML 88 | ]; 89 | const jsonYamlExcluded = new WeakSet(); 90 | /** 91 | * This ignores JSON and YAML files from config item(s). USE WITH CAUTION. 92 | * 93 | * Markdown file is not excluded, so that @eslint/markdown can lint codeblocks within markdown files 94 | * 95 | * Most of the rules should be run with JSON/YAML files, because: 96 | * 97 | * JSON files are not excluded, so stylistic rules, other text-based rules like "eol-last", do run on them (powered by eslint-plugin-jsonc and jsonc-eslint-parser) 98 | * YAML file is not excluded, so stylistic rules, other text-based rules like "eol-last", do run on YAML files (powered by eslint-plugin-yml and yaml-eslint-parser) 99 | */ 100 | // eslint-disable-next-line @typescript-eslint/naming-convention -- UNSAFE_ 101 | export function UNSAFE_excludeJsonYamlFiles(configs: FlatESLintConfigItem): FlatESLintConfigItem; 102 | // eslint-disable-next-line @typescript-eslint/naming-convention -- UNSAFE_ 103 | export function UNSAFE_excludeJsonYamlFiles(configs: FlatESLintConfigItem[]): FlatESLintConfigItem[]; 104 | // eslint-disable-next-line @typescript-eslint/naming-convention -- UNSAFE_ 105 | export function UNSAFE_excludeJsonYamlFiles(configs: FlatESLintConfigItem | FlatESLintConfigItem[]): FlatESLintConfigItem | FlatESLintConfigItem[] { 106 | if (!Array.isArray(configs)) { 107 | if (jsonYamlExcluded.has(configs)) { 108 | return configs; 109 | } 110 | configs = addIgnore(configs, GLOB_NON_JS_TS); 111 | // Mark processed as excluded, because next time the .has would be call against processed one 112 | // And `addIgnore` may return a new object 113 | jsonYamlExcluded.add(configs); 114 | return configs; 115 | } 116 | 117 | for (let i = 0, len = configs.length; i < len; i++) { 118 | if (jsonYamlExcluded.has(configs[i])) { 119 | continue; 120 | } 121 | 122 | configs[i] = addIgnore(configs[i], GLOB_NON_JS_TS); 123 | // Mark processed as excluded, because next time the .has would be call against processed one 124 | // And `addIgnore` may return a new object 125 | jsonYamlExcluded.add(configs[i]); 126 | } 127 | 128 | return configs; 129 | } 130 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-same-line-conditional/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JavaScript Plugin 3 | * Copyright (C) 2011-2024 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | import { dedent } from 'ts-dedent'; 21 | import mod from '.'; 22 | import { runTest } from '@eslint-sukka/internal'; 23 | 24 | runTest({ 25 | module: mod, 26 | valid: [ 27 | { 28 | code: ` 29 | if (cond1) 30 | if (cond2) { 31 | if (cond3) { 32 | } 33 | }` 34 | }, 35 | { 36 | code: ` 37 | if (cond1) { 38 | } else if (cond2) { 39 | } else if (cond3) { 40 | }` 41 | }, 42 | { 43 | code: ` 44 | if (cond1) { 45 | } 46 | if (cond2) { 47 | } else if (cond3) { 48 | }` 49 | }, 50 | { 51 | code: ` 52 | if (cond1) 53 | doSomething(); 54 | if (cond2) { 55 | }` 56 | }, 57 | { 58 | code: 'foo(); if (cond) bar();' 59 | }, 60 | { 61 | // OK if everything is on one line 62 | code: 'if (cond1) foo(); if (cond2) bar();' 63 | }, 64 | { 65 | code: ` 66 | function myFunc() { 67 | if (cond1) { 68 | } else if (cond2) { 69 | } else if (cond3) { 70 | } 71 | }` 72 | }, 73 | { 74 | code: ` 75 | switch(x) { 76 | case 1: 77 | if (cond1) { 78 | } else if (cond2) { 79 | } else if (cond3) { 80 | } 81 | break; 82 | default: 83 | if (cond1) { 84 | } else if (cond2) { 85 | } else if (cond3) { 86 | } 87 | break; 88 | }` 89 | } 90 | ], 91 | invalid: [ 92 | { 93 | code: ` 94 | if (cond1) { 95 | } if (cond2) { 96 | }`, 97 | errors: [ 98 | { 99 | messageId: 'sameLineCondition', 100 | line: 3, 101 | endLine: 3, 102 | column: 9, 103 | endColumn: 11 104 | } 105 | ], 106 | output: ` 107 | if (cond1) { 108 | } 109 | if (cond2) { 110 | }` 111 | }, 112 | { 113 | code: ` 114 | switch(x) { 115 | case 1: 116 | if (cond1) { 117 | } else if (cond2) { 118 | } if (cond3) { 119 | } 120 | break; 121 | default: 122 | if (cond1) { 123 | } if (cond2) { 124 | } else if (cond3) { 125 | } 126 | break; 127 | }`, 128 | errors: [ 129 | { 130 | messageId: 'sameLineCondition', 131 | line: 6, 132 | endLine: 6, 133 | column: 13, 134 | endColumn: 15 135 | }, 136 | { 137 | messageId: 'sameLineCondition', 138 | line: 11, 139 | endLine: 11, 140 | column: 13, 141 | endColumn: 15 142 | } 143 | ], 144 | output: ` 145 | switch(x) { 146 | case 1: 147 | if (cond1) { 148 | } else if (cond2) { 149 | } 150 | if (cond3) { 151 | } 152 | break; 153 | default: 154 | if (cond1) { 155 | } 156 | if (cond2) { 157 | } else if (cond3) { 158 | } 159 | break; 160 | }` 161 | }, 162 | { 163 | code: ` 164 | if (cond1) { 165 | } else if (cond2) { 166 | } if (cond3) { 167 | }`, 168 | 169 | errors: [ 170 | { 171 | messageId: 'sameLineCondition' 172 | } 173 | ], 174 | 175 | output: ` 176 | if (cond1) { 177 | } else if (cond2) { 178 | } 179 | if (cond3) { 180 | }` 181 | }, 182 | { 183 | code: dedent` 184 | if (cond1) 185 | if (cond2) { 186 | if (cond3) { 187 | } if (cond4) { 188 | } 189 | } 190 | `, 191 | errors: [ 192 | { 193 | messageId: 'sameLineCondition' 194 | } 195 | ], 196 | output: dedent` 197 | if (cond1) 198 | if (cond2) { 199 | if (cond3) { 200 | } 201 | if (cond4) { 202 | } 203 | } 204 | ` 205 | }, 206 | { 207 | code: dedent` 208 | function myFunc() { 209 | if (cond1) { 210 | } else if (cond2) { 211 | } if (cond3) { 212 | } 213 | } 214 | `, 215 | errors: [ 216 | { 217 | messageId: 'sameLineCondition' 218 | } 219 | ], 220 | output: dedent` 221 | function myFunc() { 222 | if (cond1) { 223 | } else if (cond2) { 224 | } 225 | if (cond3) { 226 | } 227 | } 228 | ` 229 | } 230 | ] 231 | }); 232 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/no-return-await/index.ts: -------------------------------------------------------------------------------- 1 | import { Linter } from 'eslint'; 2 | import { nullthrow } from 'foxts/guard'; 3 | import { createRule } from '@eslint-sukka/shared'; 4 | 5 | const baseRule = nullthrow( 6 | new Linter({ configType: 'eslintrc' }).getRules().get('no-return-await'), 7 | '[eslint-plugin-sukka] no-return-await rule is removed from ESLint!' 8 | ); 9 | 10 | export default createRule({ 11 | ...baseRule, 12 | meta: { 13 | docs: { 14 | ...baseRule.meta?.docs, 15 | // @ts-expect-error -- fuck @eslint/core types 16 | recommended: undefined 17 | }, 18 | // also we put it above so it can be overriden 19 | ...baseRule.meta, 20 | deprecated: false 21 | } 22 | }); 23 | 24 | // import { createRule } from '@eslint-sukka/shared'; 25 | // import type { TSESTree } from '@typescript-eslint/types'; 26 | // import { AST_NODE_TYPES } from '@typescript-eslint/types'; 27 | // import { ASTUtils } from '@typescript-eslint/utils'; 28 | 29 | // export default createRule({ 30 | // name: 'no-return-await', 31 | 32 | // meta: { 33 | // type: 'suggestion', 34 | 35 | // hasSuggestions: true, 36 | 37 | // docs: { 38 | // description: 'Disallows unnecessary `return await`' 39 | // }, 40 | 41 | // fixable: undefined, 42 | 43 | // deprecated: false, 44 | 45 | // schema: [], 46 | 47 | // messages: { 48 | // removeAwait: 'Remove redundant `await`.', 49 | // redundantUseOfAwait: 'Redundant use of `await` on a return value.' 50 | // } 51 | // }, 52 | 53 | // create(context) { 54 | // /** 55 | // * Reports a found unnecessary `await` expression. 56 | // * @param The node representing the `await` expression to report 57 | // */ 58 | // function reportUnnecessaryAwait(node: TSESTree.Node) { 59 | // context.report({ 60 | // node: context.sourceCode.getFirstToken(node)!, 61 | // loc: node.loc, 62 | // messageId: 'redundantUseOfAwait', 63 | // suggest: [ 64 | // { 65 | // messageId: 'removeAwait', 66 | // fix(fixer) { 67 | // const sourceCode = context.sourceCode; 68 | // const [awaitToken, tokenAfterAwait] = sourceCode.getFirstTokens(node, 2); 69 | 70 | // const areAwaitAndAwaitedExpressionOnTheSameLine = awaitToken.loc.start.line === tokenAfterAwait.loc.start.line; 71 | 72 | // if (!areAwaitAndAwaitedExpressionOnTheSameLine) { 73 | // return null; 74 | // } 75 | 76 | // const [startOfAwait, endOfAwait] = awaitToken.range; 77 | 78 | // const characterAfterAwait = sourceCode.text[endOfAwait]; 79 | // const trimLength = characterAfterAwait === ' ' ? 1 : 0; 80 | 81 | // const range: [number, number] = [startOfAwait, endOfAwait + trimLength]; 82 | 83 | // return fixer.removeRange(range); 84 | // } 85 | // } 86 | // ] 87 | 88 | // }); 89 | // } 90 | 91 | // return { 92 | // AwaitExpression(node) { 93 | // if (isInTailCallPosition(node) && !hasErrorHandler(node)) { 94 | // reportUnnecessaryAwait(node); 95 | // } 96 | // } 97 | // }; 98 | // } 99 | // }); 100 | 101 | // /** 102 | // * Checks if a node is placed in tail call position. Once `return` arguments (or arrow function expressions) can be a complex expression, 103 | // * an `await` expression could or could not be unnecessary by the definition of this rule. So we're looking for `await` expressions that are in tail position. 104 | // * @param node A node representing the `await` expression to check 105 | // * @returns The checking result 106 | // */ 107 | // function isInTailCallPosition(node: TSESTree.Node) { 108 | // if (node.parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) { 109 | // return true; 110 | // } 111 | // if (node.parent?.type === AST_NODE_TYPES.ReturnStatement) { 112 | // return !hasErrorHandler(node.parent); 113 | // } 114 | // if (node.parent?.type === AST_NODE_TYPES.ConditionalExpression && (node === node.parent.consequent || node === node.parent.alternate)) { 115 | // return isInTailCallPosition(node.parent); 116 | // } 117 | // if (node.parent?.type === AST_NODE_TYPES.LogicalExpression && node === node.parent.right) { 118 | // return isInTailCallPosition(node.parent); 119 | // } 120 | // if (node.parent?.type === AST_NODE_TYPES.SequenceExpression && node === node.parent.expressions.at(-1)) { 121 | // return isInTailCallPosition(node.parent); 122 | // } 123 | // return false; 124 | // } 125 | 126 | // /** 127 | // * Determines whether a thrown error from this node will be caught/handled within this function rather than immediately halting 128 | // * this function. For example, a statement in a `try` block will always have an error handler. A statement in 129 | // * a `catch` block will only have an error handler if there is also a `finally` block. 130 | // * @param A node representing a location where an could be thrown 131 | // * @returns `true` if a thrown error will be caught/handled in this function 132 | // */ 133 | // function hasErrorHandler(node: TSESTree.Node) { 134 | // let ancestor = node; 135 | 136 | // while (!ASTUtils.isFunction(ancestor) && ancestor.type !== AST_NODE_TYPES.Program) { 137 | // if (ancestor.parent.type === AST_NODE_TYPES.TryStatement && (ancestor === ancestor.parent.block || (ancestor === ancestor.parent.handler && ancestor.parent.finalizer))) { 138 | // return true; 139 | // } 140 | // ancestor = ancestor.parent; 141 | // } 142 | // return false; 143 | // } 144 | -------------------------------------------------------------------------------- /packages/eslint-plugin-sukka/src/rules/prefer-single-boolean-return/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * eslint-plugin-sonarjs 3 | * Copyright (C) 2018-2021 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | // https://sonarsource.github.io/rspec/#/rspec/S1126 21 | import { AST_NODE_TYPES } from '@typescript-eslint/types'; 22 | import type { TSESTree } from '@typescript-eslint/types'; 23 | import type { TSESLint } from '@typescript-eslint/utils'; 24 | import { createRule } from '@eslint-sukka/shared'; 25 | 26 | export default createRule({ 27 | name: 'prefer-single-boolean-return', 28 | meta: { 29 | messages: { 30 | replaceIfThenElseByReturn: 'Replace this if-then-else flow by a single return statement.', 31 | suggest: 'Replace with single return statement', 32 | suggestCast: 'Replace with single return statement using "!!" cast', 33 | suggestBoolean: 34 | 'Replace with single return statement without cast (condition should be boolean!)' 35 | }, 36 | schema: [], 37 | type: 'suggestion', 38 | hasSuggestions: true, 39 | docs: { 40 | description: 'Return of boolean expressions should not be wrapped into an "if-then-else" statement', 41 | recommended: 'recommended' 42 | } 43 | }, 44 | create(context) { 45 | return { 46 | IfStatement(node: TSESTree.IfStatement) { 47 | if ( 48 | // ignore `else if` 49 | node.parent.type !== AST_NODE_TYPES.IfStatement 50 | && returnsBoolean(node.consequent) 51 | && alternateReturnsBoolean(node) 52 | ) { 53 | context.report({ 54 | messageId: 'replaceIfThenElseByReturn', 55 | node, 56 | suggest: getSuggestion(node) 57 | }); 58 | } 59 | } 60 | }; 61 | 62 | function getSuggestion(ifStmt: TSESTree.IfStatement) { 63 | const getFix = (condition: string) => (fixer: TSESLint.RuleFixer) => { 64 | const singleReturn = `return ${condition};`; 65 | if (ifStmt.alternate) { 66 | return fixer.replaceText(ifStmt, singleReturn); 67 | } 68 | const parent = ifStmt.parent as TSESTree.BlockStatement; 69 | const ifStmtIndex = parent.body.indexOf(ifStmt); 70 | const returnStmt = parent.body[ifStmtIndex + 1]; 71 | const range: [number, number] = [ifStmt.range[0], returnStmt.range[1]]; 72 | return fixer.replaceTextRange(range, singleReturn); 73 | }; 74 | const shouldNegate = isReturningFalse(ifStmt.consequent); 75 | const shouldCast = !isBooleanExpression(ifStmt.test); 76 | const testText = context.sourceCode.getText(ifStmt.test); 77 | 78 | if (shouldNegate) { 79 | return [{ messageId: 'suggest', fix: getFix(`!(${testText})`) }] as const; 80 | } 81 | if (!shouldCast) { 82 | return [{ messageId: 'suggest', fix: getFix(testText) }] as const; 83 | } 84 | return [ 85 | { messageId: 'suggestCast', fix: getFix(`!!(${testText})`) }, 86 | { messageId: 'suggestBoolean', fix: getFix(testText) } 87 | ] as const; 88 | } 89 | } 90 | }); 91 | 92 | function isSimpleReturnBooleanLiteral(statement: TSESTree.Node | undefined) { 93 | // `statement.argument` can be `null`, replace it with `undefined` in this case 94 | return statement?.type === AST_NODE_TYPES.ReturnStatement && statement.argument?.type === AST_NODE_TYPES.Literal && typeof statement.argument.value === 'boolean'; 95 | } 96 | 97 | function isReturningFalse(stmt: TSESTree.Statement): boolean { 98 | const returnStmt = ( 99 | stmt.type === AST_NODE_TYPES.BlockStatement ? stmt.body[0] : stmt 100 | ) as TSESTree.ReturnStatement; 101 | return (returnStmt.argument as TSESTree.Literal).value === false; 102 | } 103 | 104 | function isBooleanExpression(expr: TSESTree.Expression) { 105 | return ( 106 | (expr.type === AST_NODE_TYPES.UnaryExpression || expr.type === AST_NODE_TYPES.BinaryExpression) 107 | && ['!', '==', '===', '!=', '!==', '<', '<=', '>', '>=', 'in', 'instanceof'].includes( 108 | expr.operator 109 | ) 110 | ); 111 | } 112 | 113 | function isBlockReturningBooleanLiteral(statement: TSESTree.Statement) { 114 | return ( 115 | statement.type === AST_NODE_TYPES.BlockStatement 116 | && statement.body.length === 1 117 | && isSimpleReturnBooleanLiteral(statement.body[0]) 118 | ); 119 | } 120 | 121 | function returnsBoolean(statement: TSESTree.Statement | undefined) { 122 | return ( 123 | statement !== undefined 124 | && (isBlockReturningBooleanLiteral(statement) || isSimpleReturnBooleanLiteral(statement)) 125 | ); 126 | } 127 | 128 | function alternateReturnsBoolean(node: TSESTree.IfStatement) { 129 | if (node.alternate) { 130 | return returnsBoolean(node.alternate); 131 | } 132 | 133 | const { parent } = node; 134 | if (parent.type === AST_NODE_TYPES.BlockStatement) { 135 | const ifStmtIndex = parent.body.indexOf(node); 136 | return isSimpleReturnBooleanLiteral(parent.body[ifStmtIndex + 1]); 137 | } 138 | 139 | return false; 140 | } 141 | --------------------------------------------------------------------------------