├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .nvmrc ├── .vscode ├── extensions.json └── settings.json ├── .yarn ├── releases │ └── yarn-3.1.1.cjs └── sdks │ ├── eslint │ ├── bin │ │ └── eslint.js │ ├── lib │ │ └── api.js │ └── package.json │ ├── integrations.yml │ └── typescript │ ├── bin │ ├── tsc │ └── tsserver │ ├── lib │ ├── tsc.js │ ├── tsserver.js │ ├── tsserverlibrary.js │ └── typescript.js │ └── package.json ├── .yarnrc.yml ├── README.md ├── ambient.d.ts ├── docs └── rules │ ├── curly.md │ ├── import-absolutes.md │ ├── import-align.md │ ├── import-ordering.md │ ├── import-quotes.md │ ├── jsx-import-react.md │ ├── jsx-no-html-attrs.md │ ├── jsx-no-string-styles.md │ ├── melted-constructs.md │ ├── newline-after-import-section.md │ └── no-default-export.md ├── package.json ├── rollup.config.js ├── sources ├── constants.ts ├── index.ts ├── misc │ └── possibleStandardNames.ts ├── rules │ ├── curly.ts │ ├── import-absolutes.ts │ ├── import-align.ts │ ├── import-ordering.ts │ ├── import-quotes.ts │ ├── jsx-import-react.ts │ ├── jsx-longhand-props.ts │ ├── jsx-no-html-attrs.ts │ ├── jsx-no-string-styles.ts │ ├── melted-constructs.ts │ ├── newline-after-import-section.ts │ └── no-default-export.ts ├── utils.ts └── utils │ └── variableUtils.ts ├── tests ├── rules │ ├── curly.test.ts │ ├── import-absolutes.test.ts │ ├── import-align.test.ts │ ├── import-ordering.test.ts │ ├── import-quotes.test.ts │ ├── jsx-import-react.test.ts │ ├── jsx-longhand-props.test.ts │ ├── jsx-no-html-attrs.test.ts │ ├── jsx-no-string-styles.test.ts │ ├── melted-constructs.test.ts │ ├── newline-after-import-section.test.ts │ └── no-default-export.test.ts └── workspace │ ├── lib.js │ └── package.json ├── tsconfig.dist.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome! 2 | 3 | # Mark this as the root editorconfig file 4 | root = true 5 | 6 | # Base ruleset for all files 7 | [*] 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # Override rules for markdown 14 | [*.md] 15 | # trailing whitespace is significant in markdown -> do not remove 16 | trim_trailing_whitespace = false 17 | 18 | [*.patch] 19 | trim_trailing_whitespace = false 20 | insert_final_newline = false 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | `@yarnpkg` 4 | ] 5 | }; 6 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Install dependencies 17 | run: yarn install 18 | 19 | - name: Run test 20 | run: yarn test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .yarn/* 2 | !.yarn/releases 3 | !.yarn/sdks 4 | .pnp.* 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | node 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs", 4 | "dbaeumer.vscode-eslint" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.autoFixOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true 5 | }, 6 | "editor.formatOnSave": false, 7 | "search.exclude": { 8 | "**/.yarn": true, 9 | "**/.pnp.*": true 10 | }, 11 | "eslint.nodePath": ".yarn/sdks", 12 | "typescript.tsdk": ".yarn/sdks/typescript/lib", 13 | "typescript.enablePromptUseWorkspaceTsdk": true 14 | } 15 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/bin/eslint.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint/bin/eslint.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint/bin/eslint.js your application uses 20 | module.exports = absRequire(`eslint/bin/eslint.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/lib/api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint/lib/api.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint/lib/api.js your application uses 20 | module.exports = absRequire(`eslint/lib/api.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint", 3 | "version": "7.29.0-sdk", 4 | "main": "./lib/api.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarn/sdks/integrations.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by @yarnpkg/sdks. 2 | # Manual changes might be lost! 3 | 4 | integrations: 5 | - vscode 6 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/bin/tsc 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/bin/tsc your application uses 20 | module.exports = absRequire(`typescript/bin/tsc`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsserver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/bin/tsserver 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/bin/tsserver your application uses 20 | module.exports = absRequire(`typescript/bin/tsserver`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsc.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/lib/tsc.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/lib/tsc.js your application uses 20 | module.exports = absRequire(`typescript/lib/tsc.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsserver.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | const moduleWrapper = tsserver => { 13 | if (!process.versions.pnp) { 14 | return tsserver; 15 | } 16 | 17 | const {isAbsolute} = require(`path`); 18 | const pnpApi = require(`pnpapi`); 19 | 20 | const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); 21 | const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); 22 | 23 | const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { 24 | return `${locator.name}@${locator.reference}`; 25 | })); 26 | 27 | // VSCode sends the zip paths to TS using the "zip://" prefix, that TS 28 | // doesn't understand. This layer makes sure to remove the protocol 29 | // before forwarding it to TS, and to add it back on all returned paths. 30 | 31 | function toEditorPath(str) { 32 | // We add the `zip:` prefix to both `.zip/` paths and virtual paths 33 | if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { 34 | // We also take the opportunity to turn virtual paths into physical ones; 35 | // this makes it much easier to work with workspaces that list peer 36 | // dependencies, since otherwise Ctrl+Click would bring us to the virtual 37 | // file instances instead of the real ones. 38 | // 39 | // We only do this to modules owned by the the dependency tree roots. 40 | // This avoids breaking the resolution when jumping inside a vendor 41 | // with peer dep (otherwise jumping into react-dom would show resolution 42 | // errors on react). 43 | // 44 | const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; 45 | if (resolved) { 46 | const locator = pnpApi.findPackageLocator(resolved); 47 | if (locator && dependencyTreeRoots.has(`${locator.name}@${locator.reference}`)) { 48 | str = resolved; 49 | } 50 | } 51 | 52 | str = normalize(str); 53 | 54 | if (str.match(/\.zip\//)) { 55 | switch (hostInfo) { 56 | // Absolute VSCode `Uri.fsPath`s need to start with a slash. 57 | // VSCode only adds it automatically for supported schemes, 58 | // so we have to do it manually for the `zip` scheme. 59 | // The path needs to start with a caret otherwise VSCode doesn't handle the protocol 60 | // 61 | // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 62 | // 63 | // Update Oct 8 2021: VSCode changed their format in 1.61. 64 | // Before | ^zip:/c:/foo/bar.zip/package.json 65 | // After | ^/zip//c:/foo/bar.zip/package.json 66 | // 67 | case `vscode <1.61`: { 68 | str = `^zip:${str}`; 69 | } break; 70 | 71 | case `vscode`: { 72 | str = `^/zip/${str}`; 73 | } break; 74 | 75 | // To make "go to definition" work, 76 | // We have to resolve the actual file system path from virtual path 77 | // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) 78 | case `coc-nvim`: { 79 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 80 | str = resolve(`zipfile:${str}`); 81 | } break; 82 | 83 | // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) 84 | // We have to resolve the actual file system path from virtual path, 85 | // everything else is up to neovim 86 | case `neovim`: { 87 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 88 | str = `zipfile:${str}`; 89 | } break; 90 | 91 | default: { 92 | str = `zip:${str}`; 93 | } break; 94 | } 95 | } 96 | } 97 | 98 | return str; 99 | } 100 | 101 | function fromEditorPath(str) { 102 | switch (hostInfo) { 103 | case `coc-nvim`: 104 | case `neovim`: { 105 | str = str.replace(/\.zip::/, `.zip/`); 106 | // The path for coc-nvim is in format of //zipfile://.yarn/... 107 | // So in order to convert it back, we use .* to match all the thing 108 | // before `zipfile:` 109 | return process.platform === `win32` 110 | ? str.replace(/^.*zipfile:\//, ``) 111 | : str.replace(/^.*zipfile:/, ``); 112 | } break; 113 | 114 | case `vscode`: 115 | default: { 116 | return process.platform === `win32` 117 | ? str.replace(/^\^?(zip:|\/zip)\/+/, ``) 118 | : str.replace(/^\^?(zip:|\/zip)\/+/, `/`); 119 | } break; 120 | } 121 | } 122 | 123 | // Force enable 'allowLocalPluginLoads' 124 | // TypeScript tries to resolve plugins using a path relative to itself 125 | // which doesn't work when using the global cache 126 | // https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238 127 | // VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but 128 | // TypeScript already does local loads and if this code is running the user trusts the workspace 129 | // https://github.com/microsoft/vscode/issues/45856 130 | const ConfiguredProject = tsserver.server.ConfiguredProject; 131 | const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; 132 | ConfiguredProject.prototype.enablePluginsWithOptions = function() { 133 | this.projectService.allowLocalPluginLoads = true; 134 | return originalEnablePluginsWithOptions.apply(this, arguments); 135 | }; 136 | 137 | // And here is the point where we hijack the VSCode <-> TS communications 138 | // by adding ourselves in the middle. We locate everything that looks 139 | // like an absolute path of ours and normalize it. 140 | 141 | const Session = tsserver.server.Session; 142 | const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; 143 | let hostInfo = `unknown`; 144 | 145 | Object.assign(Session.prototype, { 146 | onMessage(/** @type {string} */ message) { 147 | const parsedMessage = JSON.parse(message) 148 | 149 | if ( 150 | parsedMessage != null && 151 | typeof parsedMessage === `object` && 152 | parsedMessage.arguments && 153 | typeof parsedMessage.arguments.hostInfo === `string` 154 | ) { 155 | hostInfo = parsedMessage.arguments.hostInfo; 156 | if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK && process.env.VSCODE_IPC_HOOK.match(/Code\/1\.([1-5][0-9]|60)\./)) { 157 | hostInfo += ` <1.61`; 158 | } 159 | } 160 | 161 | return originalOnMessage.call(this, JSON.stringify(parsedMessage, (key, value) => { 162 | return typeof value === `string` ? fromEditorPath(value) : value; 163 | })); 164 | }, 165 | 166 | send(/** @type {any} */ msg) { 167 | return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { 168 | return typeof value === `string` ? toEditorPath(value) : value; 169 | }))); 170 | } 171 | }); 172 | 173 | return tsserver; 174 | }; 175 | 176 | if (existsSync(absPnpApiPath)) { 177 | if (!process.versions.pnp) { 178 | // Setup the environment to be able to require typescript/lib/tsserver.js 179 | require(absPnpApiPath).setup(); 180 | } 181 | } 182 | 183 | // Defer to the real typescript/lib/tsserver.js your application uses 184 | module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`)); 185 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsserverlibrary.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | const moduleWrapper = tsserver => { 13 | if (!process.versions.pnp) { 14 | return tsserver; 15 | } 16 | 17 | const {isAbsolute} = require(`path`); 18 | const pnpApi = require(`pnpapi`); 19 | 20 | const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); 21 | const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); 22 | 23 | const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { 24 | return `${locator.name}@${locator.reference}`; 25 | })); 26 | 27 | // VSCode sends the zip paths to TS using the "zip://" prefix, that TS 28 | // doesn't understand. This layer makes sure to remove the protocol 29 | // before forwarding it to TS, and to add it back on all returned paths. 30 | 31 | function toEditorPath(str) { 32 | // We add the `zip:` prefix to both `.zip/` paths and virtual paths 33 | if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { 34 | // We also take the opportunity to turn virtual paths into physical ones; 35 | // this makes it much easier to work with workspaces that list peer 36 | // dependencies, since otherwise Ctrl+Click would bring us to the virtual 37 | // file instances instead of the real ones. 38 | // 39 | // We only do this to modules owned by the the dependency tree roots. 40 | // This avoids breaking the resolution when jumping inside a vendor 41 | // with peer dep (otherwise jumping into react-dom would show resolution 42 | // errors on react). 43 | // 44 | const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; 45 | if (resolved) { 46 | const locator = pnpApi.findPackageLocator(resolved); 47 | if (locator && dependencyTreeRoots.has(`${locator.name}@${locator.reference}`)) { 48 | str = resolved; 49 | } 50 | } 51 | 52 | str = normalize(str); 53 | 54 | if (str.match(/\.zip\//)) { 55 | switch (hostInfo) { 56 | // Absolute VSCode `Uri.fsPath`s need to start with a slash. 57 | // VSCode only adds it automatically for supported schemes, 58 | // so we have to do it manually for the `zip` scheme. 59 | // The path needs to start with a caret otherwise VSCode doesn't handle the protocol 60 | // 61 | // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 62 | // 63 | // Update Oct 8 2021: VSCode changed their format in 1.61. 64 | // Before | ^zip:/c:/foo/bar.zip/package.json 65 | // After | ^/zip//c:/foo/bar.zip/package.json 66 | // 67 | case `vscode <1.61`: { 68 | str = `^zip:${str}`; 69 | } break; 70 | 71 | case `vscode`: { 72 | str = `^/zip/${str}`; 73 | } break; 74 | 75 | // To make "go to definition" work, 76 | // We have to resolve the actual file system path from virtual path 77 | // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) 78 | case `coc-nvim`: { 79 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 80 | str = resolve(`zipfile:${str}`); 81 | } break; 82 | 83 | // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) 84 | // We have to resolve the actual file system path from virtual path, 85 | // everything else is up to neovim 86 | case `neovim`: { 87 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 88 | str = `zipfile:${str}`; 89 | } break; 90 | 91 | default: { 92 | str = `zip:${str}`; 93 | } break; 94 | } 95 | } 96 | } 97 | 98 | return str; 99 | } 100 | 101 | function fromEditorPath(str) { 102 | switch (hostInfo) { 103 | case `coc-nvim`: 104 | case `neovim`: { 105 | str = str.replace(/\.zip::/, `.zip/`); 106 | // The path for coc-nvim is in format of //zipfile://.yarn/... 107 | // So in order to convert it back, we use .* to match all the thing 108 | // before `zipfile:` 109 | return process.platform === `win32` 110 | ? str.replace(/^.*zipfile:\//, ``) 111 | : str.replace(/^.*zipfile:/, ``); 112 | } break; 113 | 114 | case `vscode`: 115 | default: { 116 | return process.platform === `win32` 117 | ? str.replace(/^\^?(zip:|\/zip)\/+/, ``) 118 | : str.replace(/^\^?(zip:|\/zip)\/+/, `/`); 119 | } break; 120 | } 121 | } 122 | 123 | // Force enable 'allowLocalPluginLoads' 124 | // TypeScript tries to resolve plugins using a path relative to itself 125 | // which doesn't work when using the global cache 126 | // https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238 127 | // VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but 128 | // TypeScript already does local loads and if this code is running the user trusts the workspace 129 | // https://github.com/microsoft/vscode/issues/45856 130 | const ConfiguredProject = tsserver.server.ConfiguredProject; 131 | const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; 132 | ConfiguredProject.prototype.enablePluginsWithOptions = function() { 133 | this.projectService.allowLocalPluginLoads = true; 134 | return originalEnablePluginsWithOptions.apply(this, arguments); 135 | }; 136 | 137 | // And here is the point where we hijack the VSCode <-> TS communications 138 | // by adding ourselves in the middle. We locate everything that looks 139 | // like an absolute path of ours and normalize it. 140 | 141 | const Session = tsserver.server.Session; 142 | const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; 143 | let hostInfo = `unknown`; 144 | 145 | Object.assign(Session.prototype, { 146 | onMessage(/** @type {string} */ message) { 147 | const parsedMessage = JSON.parse(message) 148 | 149 | if ( 150 | parsedMessage != null && 151 | typeof parsedMessage === `object` && 152 | parsedMessage.arguments && 153 | typeof parsedMessage.arguments.hostInfo === `string` 154 | ) { 155 | hostInfo = parsedMessage.arguments.hostInfo; 156 | if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK && process.env.VSCODE_IPC_HOOK.match(/Code\/1\.([1-5][0-9]|60)\./)) { 157 | hostInfo += ` <1.61`; 158 | } 159 | } 160 | 161 | return originalOnMessage.call(this, JSON.stringify(parsedMessage, (key, value) => { 162 | return typeof value === `string` ? fromEditorPath(value) : value; 163 | })); 164 | }, 165 | 166 | send(/** @type {any} */ msg) { 167 | return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { 168 | return typeof value === `string` ? toEditorPath(value) : value; 169 | }))); 170 | } 171 | }); 172 | 173 | return tsserver; 174 | }; 175 | 176 | if (existsSync(absPnpApiPath)) { 177 | if (!process.versions.pnp) { 178 | // Setup the environment to be able to require typescript/lib/tsserverlibrary.js 179 | require(absPnpApiPath).setup(); 180 | } 181 | } 182 | 183 | // Defer to the real typescript/lib/tsserverlibrary.js your application uses 184 | module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`)); 185 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/typescript.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/lib/typescript.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/lib/typescript.js your application uses 20 | module.exports = absRequire(`typescript/lib/typescript.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "4.3.5-sdk", 4 | "main": "./lib/typescript.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-3.1.1.cjs 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-arca 2 | 3 | > *The @arcanis personal collection of ESLint rules.* 4 | 5 | [![](https://img.shields.io/npm/v/eslint-plugin-arca.svg)]() [![](https://img.shields.io/npm/l/eslint-plugin-arca.svg)]() [![](https://img.shields.io/badge/developed%20with-Yarn%202-blue)](https://github.com/yarnpkg/berry) 6 | 7 | I tend to have strong personal preferences about what readable code should look like, and they don't always match how the Prettier rules would work. This repo contains some ESLint rules that help enforce my style at no cost for other contributors, as they are all intended to be autofixable. 8 | 9 | Most of these rules are available as a preset via [`@yarnpkg/eslint-config`](https://github.com/yarnpkg/berry/tree/master/packages/eslint-config). 10 | 11 | ## Installation 12 | 13 | Assuming you have [ESLint](http://eslint.org) installed, just install `eslint-plugin-arca`: 14 | 15 | ``` 16 | yarn add -D eslint-plugin-arca 17 | ``` 18 | 19 | ## Usage 20 | 21 | Add `arca` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix: 22 | 23 | ```json 24 | { 25 | "plugins": [ 26 | "arca" 27 | ] 28 | } 29 | ``` 30 | 31 | 32 | Then configure the rules you want to use under the rules section. 33 | 34 | ```json 35 | { 36 | "rules": { 37 | "arca/curly": 2, 38 | "arca/import-absolutes": 2, 39 | "arca/import-align": 2, 40 | "arca/import-ordering": 2, 41 | "arca/jsx-longhand-props": 2, 42 | "arca/melted-constructs": 2, 43 | "arca/newline-after-import-section": 2, 44 | "arca/no-default-export": 2 45 | } 46 | } 47 | ``` 48 | 49 | ## Supported Rules 50 | 51 | Note: all these rules are autofixed. This is why some may look duplicate with others that aren't (for instance `arca/jsx-import-react` is autofixable, but `react/react-in-jsx-scope` [isn't](https://github.com/yannickcr/eslint-plugin-react/issues/2093)). 52 | 53 | * [`arca/curly`](https://github.com/arcanis/eslint-plugin-arca/blob/master/docs/rules/curly.md) - ensure that curly braces keep the code flow easy to read 54 | * [`arca/import-absolutes`](https://github.com/arcanis/eslint-plugin-arca/blob/master/docs/rules/import-absolutes.md) - ensure that imports are always package-absolute 55 | * [`arca/import-align`](https://github.com/arcanis/eslint-plugin-arca/blob/master/docs/rules/import-align.md) - require `from` keywords to be aligned 56 | * [`arca/import-ordering`](https://github.com/arcanis/eslint-plugin-arca/blob/master/docs/rules/import-ordering.md) - ensure that each import in the file is correctly ordered relative to the others 57 | * [`arca/jsx-import-react`](https://github.com/arcanis/eslint-plugin-arca/blob/master/docs/rules/jsx-import-react.md) - require JSX files to import `React` 58 | * [`arca/jsx-longhand-props`](https://github.com/arcanis/eslint-plugin-arca/blob/master/docs/rules/jsx-longhand-props.md) - require JSX props to be passed using the longhand syntax 59 | * [`arca/jsx-no-html-attrs`](https://github.com/arcanis/eslint-plugin-arca/blob/master/docs/rules/jsx-no-html-attrs.md) - autofix HTML attribute names into their React props 60 | * [`arca/jsx-no-string-styles`](https://github.com/arcanis/eslint-plugin-arca/blob/master/docs/rules/jsx-no-string-styles.md) - autofix string `styles` props into objects 61 | * [`arca/melted-constructs`](https://github.com/arcanis/eslint-plugin-arca/blob/master/docs/rules/melted-constructs.md) - enforce the use of melted constructs when possible 62 | * [`arca/newline-after-import-section`](https://github.com/arcanis/eslint-plugin-arca/blob/master/docs/rules/newline-after-var.md) - require an empty newline after an import section 63 | * [`arca/no-default-export`](https://github.com/arcanis/eslint-plugin-arca/blob/master/docs/rules/no-default-export.md) - disallow default exports 64 | 65 | ## License 66 | 67 | > **Copyright © 2016 Maël Nison** 68 | > 69 | > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 70 | > 71 | > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 72 | > 73 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 74 | -------------------------------------------------------------------------------- /ambient.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'estree' { 2 | interface BaseNode { 3 | parent: Node; 4 | } 5 | } 6 | 7 | export {}; 8 | -------------------------------------------------------------------------------- /docs/rules/curly.md: -------------------------------------------------------------------------------- 1 | # Ensure that curly braces keep the code flow easy to read (curly) 2 | 3 | ## Rule Details 4 | 5 | The rule of thumb is that curly braces should only be used in three cases: 6 | 7 | - To pack multiple statements together 8 | - To avoid jumping multiple indentation levels at once 9 | - To keep consistency (if an `if` needs braces, its `else` counterpart needs it too, and vice versa) 10 | 11 | Note that this rule doesn't implement the regular options from the core `eslint` package. Use it or not, there is no middle ground. 12 | 13 | The following patterns are considered warnings: 14 | 15 | ```js 16 | for (let test of test) { 17 | test(); 18 | } 19 | 20 | if (test) 21 | test = { 22 | test: test 23 | }; 24 | 25 | if (test) 26 | if (test) { 27 | test(); 28 | } 29 | 30 | if (test) { 31 | test(); 32 | } 33 | 34 | if (test) { 35 | test(); 36 | } else for (var test in test) { 37 | test(); 38 | } 39 | 40 | var test = function () { 41 | 42 | if (test) 43 | test(); 44 | 45 | }; 46 | 47 | var test = function () { 48 | 49 | if (test) 50 | test(); 51 | else if (test) 52 | test(); 53 | 54 | }; 55 | 56 | var test = function () { 57 | 58 | if (test) 59 | test(); 60 | else (test) 61 | test(); 62 | 63 | if (test) { 64 | test(); 65 | } 66 | 67 | }; 68 | 69 | if (test) { 70 | test(); 71 | } else 72 | for (let test of tests) 73 | test(); 74 | ``` 75 | 76 | The following patterns are not warnings: 77 | 78 | ```js 79 | for (let test of tests) 80 | test(); 81 | 82 | if (test) { 83 | test = { 84 | test: test 85 | }; 86 | } 87 | 88 | if (test) 89 | if (test) 90 | test(); 91 | 92 | if (test) 93 | test(); 94 | 95 | if (test) 96 | test(); 97 | else for (var test in test) 98 | test(); 99 | 100 | var test = function () { 101 | 102 | if (test) { 103 | test(); 104 | } 105 | 106 | }; 107 | 108 | let test = function () { 109 | 110 | if (test) { 111 | test(); 112 | } else if (test) { 113 | test(); 114 | } 115 | 116 | }; 117 | 118 | let test = function () { 119 | 120 | if (test) 121 | test(); 122 | else 123 | test(); 124 | 125 | if (test) { 126 | test(); 127 | } 128 | 129 | }; 130 | ``` 131 | -------------------------------------------------------------------------------- /docs/rules/import-absolutes.md: -------------------------------------------------------------------------------- 1 | # Mandate that imports are absolute to their package (import-absolutes) 2 | 3 | ## Rule Details 4 | 5 | Yarn 2 supports self-reference, making it possible for your local package to reference itself by using its own name in its imports, like it would for any other dependency. The default behaviour of this rule leverages that to always use absolute imports rather than relative. It also properly supports workspaces. 6 | 7 | The following patterns are considered warnings: 8 | 9 | ```js 10 | import foo from "./test"; 11 | ``` 12 | 13 | The following patterns are not warnings: 14 | 15 | ```js 16 | import foo from 'pkg/test'; 17 | ``` 18 | 19 | 20 | ## Options 21 | 22 | ### `preferRelative` 23 | 24 | A regex pattern specifying which import paths should be kept or made relative. 25 | 26 | The following patterns are not considered warning when `preferRelative` is set to `^\\.\\/[^\\/]*$`: 27 | 28 | ```js 29 | import foo from './foo'; 30 | import hello from './hello'; 31 | ``` 32 | 33 | If an absolute import comes from the current package and the relative path to its target matches `preferRelative`, this rule will produce a warning. 34 | 35 | Inside a package named `pkg`, and when `preferRelative` is set to `^\\.\\/[^\\/]*$`, the following patterns are considered errors: 36 | 37 | ```js 38 | // importing from a file located in 'pkg/bar' 39 | import foo from 'pkg/bar/foo'; 40 | ``` 41 | 42 | ### `replaceAbsolutePathStart` 43 | 44 | An array of objects specifying alternatives for absolute paths starts. When 45 | specified, and the rule finds that an absolute import should be used, each 46 | element of the array is used until a matching one is found (if any). 47 | 48 | Each element of the array must be an object with two properties: 49 | 50 | - `from` is what the absolute import should start with for it to match, 51 | - `to` is what to replace `from` with in the absolute import. 52 | 53 | The behaviour is undefined unless `from` and `to` are absolute import paths. 54 | 55 | The following patterns are considered warnings when `replaceAbsolutePathStart` is set to `[{ from: 'foo/bar', to: 'fff' }]`: 56 | 57 | ```js 58 | import baz from 'foo/bar/baz'; 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/rules/import-align.md: -------------------------------------------------------------------------------- 1 | # Require `from` keywords to be aligned (import-align) 2 | 3 | ## Rule Details 4 | 5 | Note that this rule doesn't affect multiline imports. 6 | 7 | The following patterns are considered warnings: 8 | 9 | ```js 10 | import foo from 'foo'; 11 | import supercalifragilisticexpialidocious from 'supercalifragilisticexpialidocious'; 12 | ``` 13 | 14 | The following patterns are not warnings: 15 | 16 | ```js 17 | import foo from 'foo'; 18 | import supercalifragilisticexpialidocious from 'supercalifragilisticexpialidocious'; 19 | 20 | import { 21 | SOME_KEY, 22 | SOME_OTHER_KEY 23 | } from 'config'; 24 | ``` 25 | 26 | ## Options 27 | 28 | ### `collapseExtraSpaces` 29 | 30 | If true, the imports won't be allowed to have any amount of spaces between the symbol list and the `from` keyword except for the minimum required to make this rule pass. 31 | 32 | The following patterns are considered warnings when `collapseExtraSpaces` is on: 33 | 34 | ```js 35 | import foo from 'foo'; 36 | import hello from 'hello'; 37 | ``` 38 | 39 | The following patterns are not warnings: 40 | 41 | ```js 42 | import foo from 'foo'; 43 | import hello from 'hello'; 44 | ``` 45 | 46 | ### `minColumnWidth` 47 | 48 | If set, ensures that the right half of each import doesn't start before the desired minimum column width. If the longest import exceeds this value, the minimum column width will be ignored and the longer value will be used for alignment. 49 | -------------------------------------------------------------------------------- /docs/rules/import-ordering.md: -------------------------------------------------------------------------------- 1 | # Ensure that each import in the file is correctly ordered relative to the others (import-ordering) 2 | 3 | ## Rule Details 4 | 5 | * First, global side-effect imports go before any other import 6 | * Then third-party imports (possibly split into sections) 7 | * Then named local imports 8 | * Finally, local side-effect imports go last 9 | 10 | Also note that within a same category, imports are sorted: 11 | 12 | * First by path (a subdirectory goes before its index) 13 | * Then by lexicographic order 14 | 15 | The following patterns are considered warnings: 16 | 17 | ```js 18 | import foo from 'foo/foo'; 19 | import bar from 'foo/bar'; 20 | ``` 21 | 22 | ```js 23 | import foo from 'common/foo'; 24 | import foo from 'foo'; 25 | ``` 26 | 27 | ```js 28 | import foo from 'app/foo'; 29 | import foo from 'common/foo'; 30 | ``` 31 | 32 | ```js 33 | import foo from 'common/foo'; 34 | import 'foo.less'; 35 | ``` 36 | 37 | ```js 38 | import './foo.less'; 39 | import 'foo.less'; 40 | ``` 41 | 42 | The following patterns are not warnings: 43 | 44 | ```js 45 | import 'foo.less'; 46 | 47 | import bar from 'bar/bar'; 48 | import bar from 'bar'; 49 | import foo from 'foo'; 50 | 51 | import bar from 'common/bar/bar'; 52 | import bar from 'common/bar'; 53 | import foo from 'common/foo/foo'; 54 | import foo from 'common/foo'; 55 | 56 | import bar from 'app/bar/bar'; 57 | import bar from 'app/bar'; 58 | import foo from 'app/foo/foo'; 59 | import foo from 'app/foo'; 60 | 61 | import {Bar} from './Bar'; 62 | import {Foo} from './Foo'; 63 | 64 | import './Component.less'; 65 | ``` 66 | 67 | ### Option 68 | 69 | An array of the sections patterns. Defaults to `["^common/", "^app/"]`. 70 | 71 | Any module that doesn't match one of these regexps will be regarded as a vendor. 72 | 73 | ## Further Reading 74 | 75 | * [`arca/newline-after-import-section`](https://github.com/arcanis/eslint-plugin-arca/blob/master/docs/rules/newline-after-var.md) - require an empty newline after an import section 76 | -------------------------------------------------------------------------------- /docs/rules/import-quotes.md: -------------------------------------------------------------------------------- 1 | # Enforce the use of single quotes for import statements (import-quotes) 2 | 3 | ## Rule Details 4 | 5 | The default [`quotes` rule](https://eslint.org/docs/rules/quotes) allows to enforce backticks everywhere but, since backticks aren't allowed in import statements, simply ignore those (making it possible to use either simple or double quotes). This rule fixes that by enforcing the use of single quotes. 6 | 7 | The following patterns are considered warnings: 8 | 9 | ```js 10 | import foo from "foo"; 11 | ``` 12 | 13 | The following patterns are not warnings: 14 | 15 | ```js 16 | import foo from 'foo'; 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/rules/jsx-import-react.md: -------------------------------------------------------------------------------- 1 | # Require JSX syntax to import React (jsx-import-react) 2 | 3 | ## Rule Details 4 | 5 | Old-style JSX transforms turned JSX into calls to `React.createElement` and, as a result, any file using JSX had to import `React` into the scope. While the new transform doesn't require this anymore (it automatically injects the right import into the scope), it can still be useful to follow this rule in some fringe cases (for example, Esbuild doesn't implement the new transform at this time). 6 | 7 | The following patterns are considered warnings: 8 | 9 | ```js 10 | const node =
; 11 | ``` 12 | 13 | The following patterns are not warnings: 14 | 15 | ```js 16 | import * as React from 'react'; 17 | 18 | const node =
; 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/rules/jsx-no-html-attrs.md: -------------------------------------------------------------------------------- 1 | # Prevent the use of HTML attributes inside JSX components (jsx-no-html-attrs) 2 | 3 | ## Rule Details 4 | 5 | Copy-pasting HTML code inside a React application typically require to update all attributes to match the React uppercased names (plus turn `class` into `className`), which can be difficult as they are not always statically analyzable (for instance, `srcset` must be specified as `srcSet`, which a simple camelcase function wouldn't detect). This rule updates them for you. 6 | 7 | The following patterns are considered warnings: 8 | 9 | ```js 10 | ; 11 | ``` 12 | 13 | The following patterns are not warnings: 14 | 15 | ```js 16 | ; 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/rules/jsx-no-string-styles.md: -------------------------------------------------------------------------------- 1 | # Prevent the use of string styles inside JSX components (jsx-no-string-styles) 2 | 3 | ## Rule Details 4 | 5 | Copy-pasting HTML code inside a React application typically require to migrate the `style` properties from literal strings into valid React objects. This can be a frustrating task, so this rule does it for you (note that it prints those objects quite crudely using `JSON.stringify`; you may want to use additional rules to remove unnecessary key quotes, add spaces, etc). 6 | 7 | The following patterns are considered warnings: 8 | 9 | ```js 10 |
; 11 | ``` 12 | 13 | The following patterns are not warnings: 14 | 15 | ```js 16 |
; 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/rules/melted-constructs.md: -------------------------------------------------------------------------------- 1 | # Enforce the use of melted constructs when possible (melted-constructs) 2 | 3 | ## Rule Details 4 | 5 | The "compatible constructs" are `if`, `while`, `do`, `switch`, `try`, and `with`. 6 | 7 | Not melting the `else` branch is allowed if the last statement of the `if` branch is also a compatible construct (in such a case, it is assumed that you may want your code to look similar). 8 | 9 | The following patterns are considered warnings: 10 | 11 | ```js 12 | if (!Array.isArray(result)) { 13 | // do something 14 | } else { 15 | for (var item of results) { 16 | // do something else 17 | } 18 | } 19 | ``` 20 | 21 | The following patterns are not warnings: 22 | 23 | ```js 24 | if (!Array.isArray(results)) { 25 | // do something 26 | } else for (var item of results) { 27 | // do something else 28 | } 29 | 30 | if (test) { 31 | if (anotherTest) { 32 | // do something 33 | } 34 | } else { 35 | if (yetAnotherTest) { 36 | // do something else 37 | } 38 | } 39 | 40 | if (test) { 41 | if (anotherTest) { 42 | // do something 43 | } 44 | } else if (yetAnotherTest) { 45 | // do something else 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/rules/newline-after-import-section.md: -------------------------------------------------------------------------------- 1 | # Require an empty newline after an import section (newline-after-import-section) 2 | 3 | ## Rule Details 4 | 5 | Complement of [`arca/import-ordering`](https://github.com/arcanis/eslint-plugin-arca/blob/master/docs/rules/import-ordering.md), this rule also makes sections more readable by adding extra lines between each. 6 | 7 | The following patterns are considered warnings: 8 | 9 | ```js 10 | import bar from 'bar'; 11 | import foo from 'foo'; 12 | import bar from 'common/bar'; 13 | import foo from 'common/foo'; 14 | import bar from 'app/bar'; 15 | import foo from 'app/foo'; 16 | ``` 17 | 18 | The following patterns are not warnings: 19 | 20 | ```js 21 | import bar from 'bar'; 22 | import foo from 'foo'; 23 | 24 | import bar from 'common/bar'; 25 | import foo from 'common/foo'; 26 | 27 | import bar from 'app/bar'; 28 | import foo from 'app/foo'; 29 | ``` 30 | 31 | ### Options 32 | 33 | An array of the sections patterns. Defaults to `["^common/", "^app/"]`. 34 | 35 | Any module that doesn't match one of these regexps will be regarded as a vendor. 36 | 37 | ## Further Reading 38 | 39 | * [`arca/import-ordering`](https://github.com/arcanis/eslint-plugin-arca/blob/master/docs/rules/import-ordering.md) - ensure that each import in the file is correctly ordered relative to the others 40 | -------------------------------------------------------------------------------- /docs/rules/no-default-export.md: -------------------------------------------------------------------------------- 1 | # Disallow default exports (no-default-export) 2 | 3 | ## Rule Details 4 | 5 | As the title hints, this rule prevents you from using default exports. 6 | 7 | The following patterns are considered warnings: 8 | 9 | ```js 10 | export default function () { 11 | } 12 | ``` 13 | 14 | The following patterns are not warnings: 15 | 16 | ```js 17 | export function foo() { 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-arca", 3 | "version": "0.16.0", 4 | "description": "A plugin to make Maël happy", 5 | "keywords": [ 6 | "eslint", 7 | "eslintplugin", 8 | "eslint-plugin" 9 | ], 10 | "workspaces": [ 11 | "tests/workspace" 12 | ], 13 | "author": "Maël Nison", 14 | "main": "sources/index.ts", 15 | "scripts": { 16 | "prepack": "rm -rf lib && rollup -c", 17 | "postpack": "rm -rf lib", 18 | "_test": "yarn mocha --require $(node -p 'require.resolve(\"ts-node/register/transpile-only\")') --reporter dot", 19 | "test": "yarn _test **/*.test.ts", 20 | "preversion": "npm test", 21 | "postversion": "git push && git push --tags" 22 | }, 23 | "devDependencies": { 24 | "@rollup/plugin-node-resolve": "^10.0.0", 25 | "@rollup/plugin-typescript": "^6.1.0", 26 | "@types/eslint": "^7.2.13", 27 | "@types/estree": "^0.0.49", 28 | "@types/estree-jsx": "^0.0.1", 29 | "@types/node": "^15.14.0", 30 | "@yarnpkg/eslint-config": "^0.3.0-rc.6", 31 | "eslint": "^7.29.0", 32 | "eslint-plugin-arca": "link:./", 33 | "mocha": "^9.0.2", 34 | "rollup": "^2.16.1", 35 | "ts-node": "^10.0.0", 36 | "tslib": "^2.3.1", 37 | "typescript": "^4.3.5" 38 | }, 39 | "engines": { 40 | "node": ">=0.10.0" 41 | }, 42 | "files": [ 43 | "lib" 44 | ], 45 | "publishConfig": { 46 | "main": "lib/index.js" 47 | }, 48 | "license": "ISC" 49 | } 50 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import ts from '@rollup/plugin-typescript'; 2 | 3 | // eslint-disable-next-line arca/no-default-export 4 | export default { 5 | input: `./sources/index.ts`, 6 | output: [ 7 | { 8 | dir: `lib`, 9 | entryFileNames: `[name].mjs`, 10 | format: `es`, 11 | }, 12 | { 13 | dir: `lib`, 14 | entryFileNames: `[name].js`, 15 | format: `cjs`, 16 | }, 17 | ], 18 | plugins: [ 19 | ts({ 20 | tsconfig: `tsconfig.dist.json`, 21 | }), 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /sources/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_SECTIONS_PATTERNS = [ 2 | `^common/`, 3 | `^app/`, 4 | `^\\.\\.($|/)`, 5 | `^\\.($|/)`, 6 | ]; 7 | -------------------------------------------------------------------------------- /sources/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A plugin to make Maël happy 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import curlyRule from './rules/curly'; 9 | import importAbsolutesRule from './rules/import-absolutes'; 10 | import importAlignRule from './rules/import-align'; 11 | import importOrderingRule from './rules/import-ordering'; 12 | import importQuotesRule from './rules/import-quotes'; 13 | import jsxImportReact from './rules/jsx-import-react'; 14 | import jsxLonghandPropsRule from './rules/jsx-longhand-props'; 15 | import jsxNoHtmlAttrs from './rules/jsx-no-html-attrs'; 16 | import jsxNoStringStyles from './rules/jsx-no-string-styles'; 17 | import meltedConstructsRule from './rules/melted-constructs'; 18 | import newlineAfterImportSectionRule from './rules/newline-after-import-section'; 19 | import noDefaultExportRule from './rules/no-default-export'; 20 | 21 | export const rules = { 22 | [`curly`]: curlyRule, 23 | [`import-absolutes`]: importAbsolutesRule, 24 | [`import-align`]: importAlignRule, 25 | [`import-ordering`]: importOrderingRule, 26 | [`import-quotes`]: importQuotesRule, 27 | [`jsx-import-react`]: jsxImportReact, 28 | [`jsx-longhand-props`]: jsxLonghandPropsRule, 29 | [`jsx-no-html-attrs`]: jsxNoHtmlAttrs, 30 | [`jsx-no-string-styles`]: jsxNoStringStyles, 31 | [`melted-constructs`]: meltedConstructsRule, 32 | [`newline-after-import-section`]: newlineAfterImportSectionRule, 33 | [`no-default-export`]: noDefaultExportRule, 34 | }; 35 | -------------------------------------------------------------------------------- /sources/misc/possibleStandardNames.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // https://github.com/facebook/react/blob/cae635054e17a6f107a39d328649137b83f25972/packages/react-dom/src/shared/possibleStandardNames.js 9 | export const possibleStandardNames: Record = { 10 | accept: `accept`, 11 | acceptcharset: `acceptCharset`, 12 | 'accept-charset': `acceptCharset`, 13 | accesskey: `accessKey`, 14 | action: `action`, 15 | allowfullscreen: `allowFullScreen`, 16 | alt: `alt`, 17 | as: `as`, 18 | async: `async`, 19 | autocapitalize: `autoCapitalize`, 20 | autocomplete: `autoComplete`, 21 | autocorrect: `autoCorrect`, 22 | autofocus: `autoFocus`, 23 | autoplay: `autoPlay`, 24 | autosave: `autoSave`, 25 | capture: `capture`, 26 | cellpadding: `cellPadding`, 27 | cellspacing: `cellSpacing`, 28 | challenge: `challenge`, 29 | charset: `charSet`, 30 | checked: `checked`, 31 | children: `children`, 32 | cite: `cite`, 33 | class: `className`, 34 | classid: `classID`, 35 | classname: `className`, 36 | cols: `cols`, 37 | colspan: `colSpan`, 38 | content: `content`, 39 | contenteditable: `contentEditable`, 40 | contextmenu: `contextMenu`, 41 | controls: `controls`, 42 | controlslist: `controlsList`, 43 | coords: `coords`, 44 | crossorigin: `crossOrigin`, 45 | dangerouslysetinnerhtml: `dangerouslySetInnerHTML`, 46 | data: `data`, 47 | datetime: `dateTime`, 48 | default: `default`, 49 | defaultchecked: `defaultChecked`, 50 | defaultvalue: `defaultValue`, 51 | defer: `defer`, 52 | dir: `dir`, 53 | disabled: `disabled`, 54 | disablepictureinpicture: `disablePictureInPicture`, 55 | disableremoteplayback: `disableRemotePlayback`, 56 | download: `download`, 57 | draggable: `draggable`, 58 | enctype: `encType`, 59 | enterkeyhint: `enterKeyHint`, 60 | for: `htmlFor`, 61 | form: `form`, 62 | formmethod: `formMethod`, 63 | formaction: `formAction`, 64 | formenctype: `formEncType`, 65 | formnovalidate: `formNoValidate`, 66 | formtarget: `formTarget`, 67 | frameborder: `frameBorder`, 68 | headers: `headers`, 69 | height: `height`, 70 | hidden: `hidden`, 71 | high: `high`, 72 | href: `href`, 73 | hreflang: `hrefLang`, 74 | htmlfor: `htmlFor`, 75 | httpequiv: `httpEquiv`, 76 | 'http-equiv': `httpEquiv`, 77 | icon: `icon`, 78 | id: `id`, 79 | innerhtml: `innerHTML`, 80 | inputmode: `inputMode`, 81 | integrity: `integrity`, 82 | is: `is`, 83 | itemid: `itemID`, 84 | itemprop: `itemProp`, 85 | itemref: `itemRef`, 86 | itemscope: `itemScope`, 87 | itemtype: `itemType`, 88 | keyparams: `keyParams`, 89 | keytype: `keyType`, 90 | kind: `kind`, 91 | label: `label`, 92 | lang: `lang`, 93 | list: `list`, 94 | loop: `loop`, 95 | low: `low`, 96 | manifest: `manifest`, 97 | marginwidth: `marginWidth`, 98 | marginheight: `marginHeight`, 99 | max: `max`, 100 | maxlength: `maxLength`, 101 | media: `media`, 102 | mediagroup: `mediaGroup`, 103 | method: `method`, 104 | min: `min`, 105 | minlength: `minLength`, 106 | multiple: `multiple`, 107 | muted: `muted`, 108 | name: `name`, 109 | nomodule: `noModule`, 110 | nonce: `nonce`, 111 | novalidate: `noValidate`, 112 | open: `open`, 113 | optimum: `optimum`, 114 | pattern: `pattern`, 115 | placeholder: `placeholder`, 116 | playsinline: `playsInline`, 117 | poster: `poster`, 118 | preload: `preload`, 119 | profile: `profile`, 120 | radiogroup: `radioGroup`, 121 | readonly: `readOnly`, 122 | referrerpolicy: `referrerPolicy`, 123 | rel: `rel`, 124 | required: `required`, 125 | reversed: `reversed`, 126 | role: `role`, 127 | rows: `rows`, 128 | rowspan: `rowSpan`, 129 | sandbox: `sandbox`, 130 | scope: `scope`, 131 | scoped: `scoped`, 132 | scrolling: `scrolling`, 133 | seamless: `seamless`, 134 | selected: `selected`, 135 | shape: `shape`, 136 | size: `size`, 137 | sizes: `sizes`, 138 | span: `span`, 139 | spellcheck: `spellCheck`, 140 | src: `src`, 141 | srcdoc: `srcDoc`, 142 | srclang: `srcLang`, 143 | srcset: `srcSet`, 144 | start: `start`, 145 | step: `step`, 146 | style: `style`, 147 | summary: `summary`, 148 | tabindex: `tabIndex`, 149 | target: `target`, 150 | title: `title`, 151 | type: `type`, 152 | usemap: `useMap`, 153 | value: `value`, 154 | width: `width`, 155 | wmode: `wmode`, 156 | wrap: `wrap`, 157 | 158 | // SVG 159 | about: `about`, 160 | accentheight: `accentHeight`, 161 | 'accent-height': `accentHeight`, 162 | accumulate: `accumulate`, 163 | additive: `additive`, 164 | alignmentbaseline: `alignmentBaseline`, 165 | 'alignment-baseline': `alignmentBaseline`, 166 | allowreorder: `allowReorder`, 167 | alphabetic: `alphabetic`, 168 | amplitude: `amplitude`, 169 | arabicform: `arabicForm`, 170 | 'arabic-form': `arabicForm`, 171 | ascent: `ascent`, 172 | attributename: `attributeName`, 173 | attributetype: `attributeType`, 174 | autoreverse: `autoReverse`, 175 | azimuth: `azimuth`, 176 | basefrequency: `baseFrequency`, 177 | baselineshift: `baselineShift`, 178 | 'baseline-shift': `baselineShift`, 179 | baseprofile: `baseProfile`, 180 | bbox: `bbox`, 181 | begin: `begin`, 182 | bias: `bias`, 183 | by: `by`, 184 | calcmode: `calcMode`, 185 | capheight: `capHeight`, 186 | 'cap-height': `capHeight`, 187 | clip: `clip`, 188 | clippath: `clipPath`, 189 | 'clip-path': `clipPath`, 190 | clippathunits: `clipPathUnits`, 191 | cliprule: `clipRule`, 192 | 'clip-rule': `clipRule`, 193 | color: `color`, 194 | colorinterpolation: `colorInterpolation`, 195 | 'color-interpolation': `colorInterpolation`, 196 | colorinterpolationfilters: `colorInterpolationFilters`, 197 | 'color-interpolation-filters': `colorInterpolationFilters`, 198 | colorprofile: `colorProfile`, 199 | 'color-profile': `colorProfile`, 200 | colorrendering: `colorRendering`, 201 | 'color-rendering': `colorRendering`, 202 | contentscripttype: `contentScriptType`, 203 | contentstyletype: `contentStyleType`, 204 | cursor: `cursor`, 205 | cx: `cx`, 206 | cy: `cy`, 207 | d: `d`, 208 | datatype: `datatype`, 209 | decelerate: `decelerate`, 210 | descent: `descent`, 211 | diffuseconstant: `diffuseConstant`, 212 | direction: `direction`, 213 | display: `display`, 214 | divisor: `divisor`, 215 | dominantbaseline: `dominantBaseline`, 216 | 'dominant-baseline': `dominantBaseline`, 217 | dur: `dur`, 218 | dx: `dx`, 219 | dy: `dy`, 220 | edgemode: `edgeMode`, 221 | elevation: `elevation`, 222 | enablebackground: `enableBackground`, 223 | 'enable-background': `enableBackground`, 224 | end: `end`, 225 | exponent: `exponent`, 226 | externalresourcesrequired: `externalResourcesRequired`, 227 | fill: `fill`, 228 | fillopacity: `fillOpacity`, 229 | 'fill-opacity': `fillOpacity`, 230 | fillrule: `fillRule`, 231 | 'fill-rule': `fillRule`, 232 | filter: `filter`, 233 | filterres: `filterRes`, 234 | filterunits: `filterUnits`, 235 | floodopacity: `floodOpacity`, 236 | 'flood-opacity': `floodOpacity`, 237 | floodcolor: `floodColor`, 238 | 'flood-color': `floodColor`, 239 | focusable: `focusable`, 240 | fontfamily: `fontFamily`, 241 | 'font-family': `fontFamily`, 242 | fontsize: `fontSize`, 243 | 'font-size': `fontSize`, 244 | fontsizeadjust: `fontSizeAdjust`, 245 | 'font-size-adjust': `fontSizeAdjust`, 246 | fontstretch: `fontStretch`, 247 | 'font-stretch': `fontStretch`, 248 | fontstyle: `fontStyle`, 249 | 'font-style': `fontStyle`, 250 | fontvariant: `fontVariant`, 251 | 'font-variant': `fontVariant`, 252 | fontweight: `fontWeight`, 253 | 'font-weight': `fontWeight`, 254 | format: `format`, 255 | from: `from`, 256 | fx: `fx`, 257 | fy: `fy`, 258 | g1: `g1`, 259 | g2: `g2`, 260 | glyphname: `glyphName`, 261 | 'glyph-name': `glyphName`, 262 | glyphorientationhorizontal: `glyphOrientationHorizontal`, 263 | 'glyph-orientation-horizontal': `glyphOrientationHorizontal`, 264 | glyphorientationvertical: `glyphOrientationVertical`, 265 | 'glyph-orientation-vertical': `glyphOrientationVertical`, 266 | glyphref: `glyphRef`, 267 | gradienttransform: `gradientTransform`, 268 | gradientunits: `gradientUnits`, 269 | hanging: `hanging`, 270 | horizadvx: `horizAdvX`, 271 | 'horiz-adv-x': `horizAdvX`, 272 | horizoriginx: `horizOriginX`, 273 | 'horiz-origin-x': `horizOriginX`, 274 | ideographic: `ideographic`, 275 | imagerendering: `imageRendering`, 276 | 'image-rendering': `imageRendering`, 277 | in2: `in2`, 278 | in: `in`, 279 | inlist: `inlist`, 280 | intercept: `intercept`, 281 | k1: `k1`, 282 | k2: `k2`, 283 | k3: `k3`, 284 | k4: `k4`, 285 | k: `k`, 286 | kernelmatrix: `kernelMatrix`, 287 | kernelunitlength: `kernelUnitLength`, 288 | kerning: `kerning`, 289 | keypoints: `keyPoints`, 290 | keysplines: `keySplines`, 291 | keytimes: `keyTimes`, 292 | lengthadjust: `lengthAdjust`, 293 | letterspacing: `letterSpacing`, 294 | 'letter-spacing': `letterSpacing`, 295 | lightingcolor: `lightingColor`, 296 | 'lighting-color': `lightingColor`, 297 | limitingconeangle: `limitingConeAngle`, 298 | local: `local`, 299 | markerend: `markerEnd`, 300 | 'marker-end': `markerEnd`, 301 | markerheight: `markerHeight`, 302 | markermid: `markerMid`, 303 | 'marker-mid': `markerMid`, 304 | markerstart: `markerStart`, 305 | 'marker-start': `markerStart`, 306 | markerunits: `markerUnits`, 307 | markerwidth: `markerWidth`, 308 | mask: `mask`, 309 | maskcontentunits: `maskContentUnits`, 310 | maskunits: `maskUnits`, 311 | mathematical: `mathematical`, 312 | mode: `mode`, 313 | numoctaves: `numOctaves`, 314 | offset: `offset`, 315 | opacity: `opacity`, 316 | operator: `operator`, 317 | order: `order`, 318 | orient: `orient`, 319 | orientation: `orientation`, 320 | origin: `origin`, 321 | overflow: `overflow`, 322 | overlineposition: `overlinePosition`, 323 | 'overline-position': `overlinePosition`, 324 | overlinethickness: `overlineThickness`, 325 | 'overline-thickness': `overlineThickness`, 326 | paintorder: `paintOrder`, 327 | 'paint-order': `paintOrder`, 328 | panose1: `panose1`, 329 | 'panose-1': `panose1`, 330 | pathlength: `pathLength`, 331 | patterncontentunits: `patternContentUnits`, 332 | patterntransform: `patternTransform`, 333 | patternunits: `patternUnits`, 334 | pointerevents: `pointerEvents`, 335 | 'pointer-events': `pointerEvents`, 336 | points: `points`, 337 | pointsatx: `pointsAtX`, 338 | pointsaty: `pointsAtY`, 339 | pointsatz: `pointsAtZ`, 340 | prefix: `prefix`, 341 | preservealpha: `preserveAlpha`, 342 | preserveaspectratio: `preserveAspectRatio`, 343 | primitiveunits: `primitiveUnits`, 344 | property: `property`, 345 | r: `r`, 346 | radius: `radius`, 347 | refx: `refX`, 348 | refy: `refY`, 349 | renderingintent: `renderingIntent`, 350 | 'rendering-intent': `renderingIntent`, 351 | repeatcount: `repeatCount`, 352 | repeatdur: `repeatDur`, 353 | requiredextensions: `requiredExtensions`, 354 | requiredfeatures: `requiredFeatures`, 355 | resource: `resource`, 356 | restart: `restart`, 357 | result: `result`, 358 | results: `results`, 359 | rotate: `rotate`, 360 | rx: `rx`, 361 | ry: `ry`, 362 | scale: `scale`, 363 | security: `security`, 364 | seed: `seed`, 365 | shaperendering: `shapeRendering`, 366 | 'shape-rendering': `shapeRendering`, 367 | slope: `slope`, 368 | spacing: `spacing`, 369 | specularconstant: `specularConstant`, 370 | specularexponent: `specularExponent`, 371 | speed: `speed`, 372 | spreadmethod: `spreadMethod`, 373 | startoffset: `startOffset`, 374 | stddeviation: `stdDeviation`, 375 | stemh: `stemh`, 376 | stemv: `stemv`, 377 | stitchtiles: `stitchTiles`, 378 | stopcolor: `stopColor`, 379 | 'stop-color': `stopColor`, 380 | stopopacity: `stopOpacity`, 381 | 'stop-opacity': `stopOpacity`, 382 | strikethroughposition: `strikethroughPosition`, 383 | 'strikethrough-position': `strikethroughPosition`, 384 | strikethroughthickness: `strikethroughThickness`, 385 | 'strikethrough-thickness': `strikethroughThickness`, 386 | string: `string`, 387 | stroke: `stroke`, 388 | strokedasharray: `strokeDasharray`, 389 | 'stroke-dasharray': `strokeDasharray`, 390 | strokedashoffset: `strokeDashoffset`, 391 | 'stroke-dashoffset': `strokeDashoffset`, 392 | strokelinecap: `strokeLinecap`, 393 | 'stroke-linecap': `strokeLinecap`, 394 | strokelinejoin: `strokeLinejoin`, 395 | 'stroke-linejoin': `strokeLinejoin`, 396 | strokemiterlimit: `strokeMiterlimit`, 397 | 'stroke-miterlimit': `strokeMiterlimit`, 398 | strokewidth: `strokeWidth`, 399 | 'stroke-width': `strokeWidth`, 400 | strokeopacity: `strokeOpacity`, 401 | 'stroke-opacity': `strokeOpacity`, 402 | suppresscontenteditablewarning: `suppressContentEditableWarning`, 403 | suppresshydrationwarning: `suppressHydrationWarning`, 404 | surfacescale: `surfaceScale`, 405 | systemlanguage: `systemLanguage`, 406 | tablevalues: `tableValues`, 407 | targetx: `targetX`, 408 | targety: `targetY`, 409 | textanchor: `textAnchor`, 410 | 'text-anchor': `textAnchor`, 411 | textdecoration: `textDecoration`, 412 | 'text-decoration': `textDecoration`, 413 | textlength: `textLength`, 414 | textrendering: `textRendering`, 415 | 'text-rendering': `textRendering`, 416 | to: `to`, 417 | transform: `transform`, 418 | typeof: `typeof`, 419 | u1: `u1`, 420 | u2: `u2`, 421 | underlineposition: `underlinePosition`, 422 | 'underline-position': `underlinePosition`, 423 | underlinethickness: `underlineThickness`, 424 | 'underline-thickness': `underlineThickness`, 425 | unicode: `unicode`, 426 | unicodebidi: `unicodeBidi`, 427 | 'unicode-bidi': `unicodeBidi`, 428 | unicoderange: `unicodeRange`, 429 | 'unicode-range': `unicodeRange`, 430 | unitsperem: `unitsPerEm`, 431 | 'units-per-em': `unitsPerEm`, 432 | unselectable: `unselectable`, 433 | valphabetic: `vAlphabetic`, 434 | 'v-alphabetic': `vAlphabetic`, 435 | values: `values`, 436 | vectoreffect: `vectorEffect`, 437 | 'vector-effect': `vectorEffect`, 438 | version: `version`, 439 | vertadvy: `vertAdvY`, 440 | 'vert-adv-y': `vertAdvY`, 441 | vertoriginx: `vertOriginX`, 442 | 'vert-origin-x': `vertOriginX`, 443 | vertoriginy: `vertOriginY`, 444 | 'vert-origin-y': `vertOriginY`, 445 | vhanging: `vHanging`, 446 | 'v-hanging': `vHanging`, 447 | videographic: `vIdeographic`, 448 | 'v-ideographic': `vIdeographic`, 449 | viewbox: `viewBox`, 450 | viewtarget: `viewTarget`, 451 | visibility: `visibility`, 452 | vmathematical: `vMathematical`, 453 | 'v-mathematical': `vMathematical`, 454 | vocab: `vocab`, 455 | widths: `widths`, 456 | wordspacing: `wordSpacing`, 457 | 'word-spacing': `wordSpacing`, 458 | writingmode: `writingMode`, 459 | 'writing-mode': `writingMode`, 460 | x1: `x1`, 461 | x2: `x2`, 462 | x: `x`, 463 | xchannelselector: `xChannelSelector`, 464 | xheight: `xHeight`, 465 | 'x-height': `xHeight`, 466 | xlinkactuate: `xlinkActuate`, 467 | 'xlink:actuate': `xlinkActuate`, 468 | xlinkarcrole: `xlinkArcrole`, 469 | 'xlink:arcrole': `xlinkArcrole`, 470 | xlinkhref: `xlinkHref`, 471 | 'xlink:href': `xlinkHref`, 472 | xlinkrole: `xlinkRole`, 473 | 'xlink:role': `xlinkRole`, 474 | xlinkshow: `xlinkShow`, 475 | 'xlink:show': `xlinkShow`, 476 | xlinktitle: `xlinkTitle`, 477 | 'xlink:title': `xlinkTitle`, 478 | xlinktype: `xlinkType`, 479 | 'xlink:type': `xlinkType`, 480 | xmlbase: `xmlBase`, 481 | 'xml:base': `xmlBase`, 482 | xmllang: `xmlLang`, 483 | 'xml:lang': `xmlLang`, 484 | xmlns: `xmlns`, 485 | 'xml:space': `xmlSpace`, 486 | xmlnsxlink: `xmlnsXlink`, 487 | 'xmlns:xlink': `xmlnsXlink`, 488 | xmlspace: `xmlSpace`, 489 | y1: `y1`, 490 | y2: `y2`, 491 | y: `y`, 492 | ychannelselector: `yChannelSelector`, 493 | z: `z`, 494 | zoomandpan: `zoomAndPan`, 495 | }; 496 | -------------------------------------------------------------------------------- /sources/rules/curly.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensure that curly braces keep the code flow easy to read 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import type {Rule} from 'eslint'; 9 | import type * as ESTree from 'estree'; 10 | 11 | const rule: Rule.RuleModule = { 12 | meta: { 13 | fixable: `code`, 14 | }, 15 | 16 | create(context) { 17 | function isMeltedIf(node: Rule.Node): node is ESTree.IfStatement { 18 | if (node.type !== `IfStatement`) 19 | return false; 20 | 21 | if (!node.parent || node.parent.type !== `IfStatement` || !node.parent.alternate) 22 | return false; 23 | 24 | const sourceCode = context.getSourceCode(); 25 | 26 | const elseToken = getElseKeyword(node.parent)!; 27 | const elseLine = elseToken.loc.end.line; 28 | 29 | const checkToken = sourceCode.getFirstToken(node)!; 30 | const checkLine = checkToken.loc.start.line; 31 | 32 | return elseLine === checkLine; 33 | } 34 | 35 | function getElseKeyword(node: Rule.Node) { 36 | if (node.type !== `IfStatement`) 37 | return null; 38 | 39 | const sourceCode = context.getSourceCode(); 40 | 41 | let token = sourceCode.getTokenAfter(node.consequent)!; 42 | while (token.type !== `Keyword` || token.value !== `else`) 43 | token = sourceCode.getTokenAfter(token)!; 44 | 45 | return token; 46 | } 47 | 48 | function reportExpectedBraceError(node: Rule.Node, body: Rule.Node, name: string, suffix: string = ``) { 49 | context.report({ 50 | node, 51 | loc: (name !== `else` ? node : getElseKeyword(node)!).loc!.start, 52 | message: `Expected { after '{{name}}'{{suffix}}.`, 53 | 54 | data: { 55 | name, 56 | suffix: (suffix ? ` ${suffix}` : ``), 57 | }, 58 | 59 | fix(fixer) { 60 | const sourceCode = context.getSourceCode(); 61 | fixer.replaceText(body, `{${sourceCode.getText(body)}}`); 62 | 63 | // TODO: This is actually a bug, as we should return the fix instead. 64 | // However, it breaks the tests and I don't have the time to investigate atm. 65 | return null; 66 | }, 67 | }); 68 | } 69 | 70 | function reportUnnecessaryBraceError(node: Rule.Node, body: Rule.Node, name: string, suffix: string = ``) { 71 | context.report({ 72 | node, 73 | loc: (name !== `else` ? node : getElseKeyword(node)!).loc!.start, 74 | message: `Unnecessary { after '{{name}}'{{suffix}}.`, 75 | 76 | data: { 77 | name, 78 | suffix: (suffix ? ` ${suffix}` : ``), 79 | }, 80 | 81 | fix(fixer) { 82 | const sourceCode = context.getSourceCode(); 83 | 84 | const openingBracket = sourceCode.getFirstToken(body)!; 85 | const closingBracket = sourceCode.getLastToken(body)!; 86 | const lastTokenInBlock = sourceCode.getTokenBefore(closingBracket)!; 87 | 88 | const resultingBodyText = [ 89 | sourceCode.getText().slice(openingBracket.range[1], lastTokenInBlock.range[0]), 90 | lastTokenInBlock.value, 91 | sourceCode.getText().slice(lastTokenInBlock.range[1], closingBracket.range[0]), 92 | ].join(``); 93 | 94 | return fixer.replaceText(body, resultingBodyText); 95 | }, 96 | }); 97 | } 98 | 99 | function checkCurly(expectation: boolean, checkNode: Rule.Node, name: string, suffix?: string) { 100 | const hasCurlyBraces = checkNode.type === `BlockStatement`; 101 | if (expectation === hasCurlyBraces) 102 | return; 103 | 104 | if (expectation) { 105 | reportExpectedBraceError(checkNode.parent, checkNode, name, suffix); 106 | } else { 107 | reportUnnecessaryBraceError(checkNode.parent, checkNode, name, suffix); 108 | } 109 | } 110 | 111 | function isFatBlockStatement(node: Rule.Node) { 112 | if (node.type !== `BlockStatement`) 113 | return false; 114 | 115 | if (node.body.length <= 1) 116 | return false; 117 | 118 | return true; 119 | } 120 | 121 | function isLastBlockStatement(node: Rule.Node) { 122 | if (!node.parent) 123 | return false; 124 | 125 | if (node.parent.type === `BlockStatement`) 126 | return node.parent.body[node.parent.body.length - 1] === node; 127 | 128 | return false; 129 | } 130 | 131 | function containsDedent(node: Rule.Node) { 132 | if (node.type === `BlockStatement` && node.body.length === 0) 133 | return false; 134 | 135 | const sourceCode = context.getSourceCode(); 136 | 137 | const firstNode = node.type === `BlockStatement` ? node.body[0] : node; 138 | const lastNode = node.type === `BlockStatement` ? node.body[node.body.length - 1] : node; 139 | 140 | const firstToken = sourceCode.getFirstToken(firstNode)!; 141 | const lastToken = sourceCode.getLastToken(lastNode)!; 142 | 143 | const startLine = firstToken.loc.start.line - 1; 144 | const endLine = lastToken.loc.end.line; 145 | 146 | const text = sourceCode.lines.slice(startLine, endLine); 147 | 148 | for (let indentLevel = 0, t = 0, T = text.length; t < T; ++ t) { 149 | const lineLevel = text[t].match(/^([ \t]*)/)![0].length; 150 | 151 | if (lineLevel >= indentLevel) { 152 | indentLevel = lineLevel; 153 | } else { 154 | return true; 155 | } 156 | } 157 | 158 | return false; 159 | } 160 | 161 | function getExpectation(node: Rule.Node) { 162 | // If the node contains multiple statements in a single node 163 | // (because then we just *have* to use a block) 164 | if (isFatBlockStatement(node)) 165 | return true; 166 | 167 | // If the "if" that contains the node is the last one of its 168 | // parent block (because otherwise it would put multiple 169 | // dedents between a line and the closing '}', which I find 170 | // perturbing) 171 | if (isLastBlockStatement(node.parent)) 172 | return true; 173 | 174 | // If the node contains dedents (because otherwise it would make 175 | // it harder to see that the node isn't a block, especially if it 176 | // spans multiple lines like with a function definition) 177 | if (containsDedent(node)) 178 | return true; 179 | 180 | return false; 181 | } 182 | 183 | function resolveMeltedConstruct(node: ESTree.IfStatement & Rule.NodeParentExtension) { 184 | const nodes = [node.consequent]; 185 | 186 | while (node.alternate) { 187 | const alternate = node.alternate as Rule.Node; 188 | if (!isMeltedIf(alternate)) 189 | break; 190 | 191 | node = alternate; 192 | nodes.push(node.consequent); 193 | } 194 | 195 | if (node.alternate) 196 | nodes.push(node.alternate); 197 | 198 | return nodes as Array; 199 | } 200 | 201 | return { 202 | IfStatement(node) { 203 | // Melted constructs have already been processed along with their 204 | // root node 205 | if (isMeltedIf(node)) 206 | return; 207 | 208 | const nodes = resolveMeltedConstruct(node); 209 | 210 | const expectations = nodes.map((node, i) => { 211 | return getExpectation(node); 212 | }); 213 | 214 | // If at least one node requires curly braces, all of them do 215 | const curlyAreRequired = expectations.some(expectation => { 216 | return expectation; 217 | }); 218 | 219 | for (let t = 0; t < nodes.length; ++t) { 220 | if (t === 0 || t < nodes.length - 1) { 221 | checkCurly(curlyAreRequired, nodes[t], `if`, `condition`); 222 | } else { 223 | checkCurly(curlyAreRequired, nodes[t], `else`); 224 | } 225 | } 226 | }, 227 | 228 | WhileStatement(node) { 229 | checkCurly(getExpectation(node.body), node.body, `while`, `condition`); 230 | }, 231 | 232 | DoWhileStatement(node) { 233 | // We enforce the use of curly braces with do-while because 234 | // they're confusing enough as it is, plus they would make the 235 | // fixer more complex 236 | checkCurly(true, node.body, `do`); 237 | }, 238 | 239 | ForStatement(node) { 240 | checkCurly(getExpectation(node.body), node.body, `for`, `condition`); 241 | }, 242 | 243 | ForInStatement(node) { 244 | checkCurly(getExpectation(node.body), node.body, `for-in`); 245 | }, 246 | 247 | ForOfStatement(node) { 248 | checkCurly(getExpectation(node.body), node.body, `for-of`); 249 | }, 250 | }; 251 | }, 252 | }; 253 | 254 | // eslint-disable-next-line arca/no-default-export 255 | export default rule; 256 | -------------------------------------------------------------------------------- /sources/rules/import-absolutes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensure that each import in the file is correctly ordered relative to the others 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import type {Rule} from 'eslint'; 9 | import fs from 'fs'; 10 | import path from 'path'; 11 | 12 | type ImportInfo = { 13 | relativePath: string ; 14 | absolutePath: string; 15 | }; 16 | 17 | let pnp; 18 | try { 19 | pnp = require(`pnpapi`); 20 | } catch {} 21 | 22 | const isAbsolute = (path: string): boolean => !path.match(/^\.{0,2}(\/|$)/); 23 | const isRelative = (path: string): boolean => !isAbsolute(path); 24 | 25 | const withEndSep = (input: string) => input.endsWith(path.sep) ? input : `${input}${path.sep}`; 26 | 27 | const getBuiltinReplacementSpecs = () => { 28 | const replacementSpecs: Array<{from: string, to: string}> = []; 29 | 30 | if (typeof pnp !== `undefined`) { 31 | const rootInfo = pnp.getPackageInformation(pnp.topLevel); 32 | 33 | const workspaces = pnp.getDependencyTreeRoots().map(locator => ({ 34 | locator, 35 | pkg: pnp.getPackageInformation(locator), 36 | })); 37 | 38 | const root = workspaces.find(workspace => { 39 | return workspace.pkg.packageLocation === rootInfo.packageLocation; 40 | }); 41 | 42 | for (const workspace of workspaces) { 43 | const relative = path.relative(root.pkg.packageLocation, workspace.pkg.packageLocation); 44 | if (!relative) 45 | continue; 46 | 47 | replacementSpecs.push({ 48 | from: path.posix.join(root.locator.name, relative.replace(/\\/g, `/`)), 49 | to: `${workspace.locator.name}`, 50 | }); 51 | } 52 | } 53 | 54 | return replacementSpecs; 55 | }; 56 | 57 | const getReplaceAbsolutePathStart = ( 58 | replacementSpecs: Array<{from: string, to: string}> 59 | ): (path: string) => {adjustedPath: string, from: string, to: string} | null => { 60 | const validatedSpec = replacementSpecs.map(({from, to}) => { 61 | if (!isAbsolute(from) || !isAbsolute(to)) 62 | throw new Error(`'from' specifier must be an absolute path instead of ${from}`); 63 | 64 | const sep = from.endsWith(`/`) 65 | ? `/` 66 | : `/|$`; 67 | 68 | from = from.replace(/\/$/, ``); 69 | to = to.replace(/\/$/, ``); 70 | 71 | return { 72 | from, 73 | to, 74 | fromRegex: new RegExp(`^${from}(${sep})`), 75 | }; 76 | }); 77 | 78 | return path => { 79 | for (const {from, fromRegex, to} of validatedSpec) { 80 | const candidate = path.replace(fromRegex, `${to}$1`); 81 | if (candidate === path) 82 | continue; 83 | 84 | if (candidate === to) { 85 | return {adjustedPath: candidate, from, to}; 86 | } else { 87 | return {adjustedPath: candidate, from: `${from}/`, to: `${to}/`}; 88 | } 89 | } 90 | 91 | return null; 92 | }; 93 | }; 94 | 95 | const rule: Rule.RuleModule = { 96 | meta: { 97 | fixable: `code`, 98 | 99 | schema: [{ 100 | type: `object`, 101 | 102 | properties: { 103 | preferRelative: { 104 | type: `string`, 105 | }, 106 | replaceAbsolutePathStart: { 107 | type: `array`, 108 | 109 | items: [{ 110 | type: `object`, 111 | 112 | properties: { 113 | from: {type: `string`}, 114 | to: {type: `string`}, 115 | }, 116 | }], 117 | }, 118 | }, 119 | 120 | additionalProperties: false, 121 | }], 122 | }, 123 | 124 | create(context) { 125 | const options = context.options[0] || {}; 126 | 127 | const preferRelativeRegex = options.preferRelative ? new RegExp(options.preferRelative) : undefined; 128 | const preferRelative = preferRelativeRegex ? (path: string) => preferRelativeRegex.test(path) : () => false; 129 | 130 | const replaceAbsolutePathStart = getReplaceAbsolutePathStart([ 131 | ...getBuiltinReplacementSpecs(), 132 | ...options.replaceAbsolutePathStart ?? [], 133 | ]); 134 | 135 | const sourceDirName = withEndSep(path.dirname(context.getFilename())); 136 | 137 | const packageDir = getPackagePath(sourceDirName); 138 | const packageInfo = packageDir && require(path.join(packageDir, `package.json`)) || {}; 139 | 140 | function isPackageImport(importPath: string): boolean { 141 | return isRelative(importPath) || importPath.startsWith(`${packageInfo.name}/`); 142 | } 143 | 144 | function getImportInfo(importPath: string): ImportInfo | null { 145 | if (!isPackageImport(importPath) || packageDir === null || packageInfo.name === null) 146 | return null; 147 | 148 | const targetPath = isRelative(importPath) 149 | ? path.resolve(sourceDirName, importPath) 150 | : path.join(packageDir, importPath.slice(packageInfo.name.length)); 151 | 152 | let relativePath = path.relative(sourceDirName, targetPath) || `.`; 153 | 154 | if (isAbsolute(relativePath)) 155 | relativePath = `./${relativePath}`; 156 | if (relativePath.match(/(^|\/)\.{0,2}$/)) 157 | relativePath = `${relativePath}/index`; 158 | 159 | return { 160 | absolutePath: `${packageInfo.name}/${path.relative(packageDir, targetPath)}`, 161 | relativePath, 162 | }; 163 | } 164 | 165 | return { 166 | ImportDeclaration (node) { 167 | const importPath = node.source.value; 168 | if (typeof importPath !== `string` || !isPackageImport(importPath)) 169 | return; 170 | 171 | const importInfo = getImportInfo(importPath); 172 | if (!importInfo) 173 | return; 174 | 175 | const {relativePath, absolutePath} = importInfo; 176 | 177 | const preferRelativeImport = preferRelative(importInfo.relativePath); 178 | const adjustedAbsolutePath = replaceAbsolutePathStart(absolutePath); 179 | 180 | const report = (message: string, replacement: string) => context.report({ 181 | node, 182 | message, 183 | data: { 184 | source: importPath, 185 | }, 186 | fix(fixer) { 187 | const fromRange = node.source.range![0]; 188 | const toRange = node.source.range![1]; 189 | 190 | return fixer.replaceTextRange([fromRange, toRange], `'${replacement}'`); 191 | }, 192 | }); 193 | 194 | if (isAbsolute(importPath)) { 195 | if (preferRelativeImport) { 196 | report( 197 | `Expected absolute import to be relative (rather than '{{source}}').`, 198 | relativePath 199 | ); 200 | } else if (importPath !== absolutePath) { 201 | report( 202 | `Expected absolute import to be normalized (rather than '{{source}}').`, 203 | absolutePath 204 | ); 205 | } else if (adjustedAbsolutePath !== null) { 206 | const {adjustedPath, from, to} = adjustedAbsolutePath; 207 | if (from.endsWith(`/`)) { 208 | report( 209 | `Expected absolute import to start with '${to}' prefix (rather than '${from}').`, 210 | adjustedPath 211 | ); 212 | } else { 213 | report( 214 | `Expected absolute import to be '${to}' (rather than '${from}').`, 215 | adjustedPath 216 | ); 217 | } 218 | } 219 | } else { 220 | if (!preferRelativeImport) { 221 | report( 222 | `Expected relative import to be package-absolute (rather than '{{source}}').`, 223 | adjustedAbsolutePath !== null ? adjustedAbsolutePath.adjustedPath : absolutePath 224 | ); 225 | } else if (preferRelativeImport && importPath !== relativePath) { 226 | report( 227 | `Expected relative import to be normalized (rather than '{{source}}').`, 228 | relativePath 229 | ); 230 | } 231 | } 232 | }, 233 | }; 234 | }, 235 | }; 236 | 237 | function getPackagePath(startPath: string) { 238 | let currentPath = path.resolve(startPath); 239 | let previousPath; 240 | 241 | while (currentPath !== previousPath) { 242 | if (fs.existsSync(path.join(currentPath, `package.json`))) 243 | return currentPath; 244 | 245 | previousPath = currentPath; 246 | currentPath = path.dirname(currentPath); 247 | } 248 | 249 | return null; 250 | } 251 | 252 | // eslint-disable-next-line arca/no-default-export 253 | export default rule; 254 | -------------------------------------------------------------------------------- /sources/rules/import-align.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Require from keywords to be aligned 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import type {Rule} from 'eslint'; 9 | import type * as ESTree from 'estree'; 10 | 11 | const rule: Rule.RuleModule = { 12 | meta: { 13 | fixable: `whitespace`, 14 | 15 | schema: [{ 16 | type: `object`, 17 | 18 | properties: { 19 | collapseExtraSpaces: { 20 | type: `boolean`, 21 | }, 22 | 23 | minColumnWidth: { 24 | type: `number`, 25 | }, 26 | }, 27 | 28 | additionalProperties: false, 29 | }], 30 | }, 31 | 32 | create(context) { 33 | const defaultOptions = { 34 | collapseExtraSpaces: false, 35 | minColumnWidth: 0, 36 | }; 37 | 38 | const options = Object.assign({}, defaultOptions, context.options[0]); 39 | 40 | function getFromKeyword(node: Rule.Node) { 41 | if (node.type !== `ImportDeclaration`) 42 | return null; 43 | 44 | if (node.specifiers.length < 1) 45 | return null; 46 | 47 | const sourceCode = context.getSourceCode(); 48 | 49 | let token = sourceCode.getTokenAfter(node.specifiers[node.specifiers.length - 1])!; 50 | while (token.type !== `Identifier` || token.value !== `from`) 51 | token = sourceCode.getTokenAfter(token)!; 52 | 53 | return token; 54 | } 55 | 56 | function reportUnalignedImportStatement(node: Rule.Node, diff: number) { 57 | const sourceCode = context.getSourceCode(); 58 | 59 | const fromKeyword = getFromKeyword(node)!; 60 | const previousToken = sourceCode.getTokenBefore(fromKeyword)!; 61 | 62 | context.report({ 63 | node, 64 | loc: fromKeyword.loc.start, 65 | message: `Unaligned import statement`, 66 | 67 | fix(fixer) { 68 | if (diff < 0) { 69 | const index = sourceCode.getIndexFromLoc(previousToken.loc.end); 70 | return fixer.removeRange([index, index + Math.abs(diff)]); 71 | } else { 72 | return fixer.insertTextAfter(previousToken, ` `.repeat(diff)); 73 | } 74 | }, 75 | }); 76 | } 77 | 78 | function isSingleLine(node: Rule.Node) { 79 | const sourceCode = context.getSourceCode(); 80 | 81 | const first = sourceCode.getFirstToken(node)!; 82 | const last = sourceCode.getLastToken(node)!; 83 | 84 | return first.loc.start.line === last.loc.end.line; 85 | } 86 | 87 | function isSuitableImport(node: Rule.Node): node is ESTree.ImportDeclaration { 88 | return node.type === `ImportDeclaration` && node.specifiers.length >= 1 && isSingleLine(node); 89 | } 90 | 91 | function findSurroundingImports(node: ESTree.ImportDeclaration) { 92 | const surroundingImports = [node]; 93 | 94 | const parentBody = (node.parent as ESTree.Program).body; 95 | const nodeLocation = parentBody.indexOf(node); 96 | 97 | for (let t = nodeLocation - 1; t >= 0; --t) { 98 | const neighbour = parentBody[t]; 99 | if (!isSuitableImport(neighbour)) 100 | break; 101 | 102 | surroundingImports.unshift(neighbour); 103 | } 104 | 105 | for (let t = nodeLocation + 1; t < parentBody.length; ++t) { 106 | const neighbour = parentBody[t]; 107 | if (!isSuitableImport(neighbour)) 108 | break; 109 | 110 | surroundingImports.push(neighbour); 111 | } 112 | 113 | return surroundingImports; 114 | } 115 | 116 | function getLineInfo(node: Rule.Node) { 117 | const sourceCode = context.getSourceCode(); 118 | 119 | const fromToken = getFromKeyword(node)!; 120 | const fromTokenStart = fromToken.loc.start.column; 121 | 122 | const prevToken = sourceCode.getTokenBefore(fromToken)!; 123 | const prevTokenEnd = prevToken.loc.end.column; 124 | 125 | return { 126 | prevTokenEnd, 127 | fromTokenStart, 128 | }; 129 | } 130 | 131 | function getAlignmentColumn(lines: Array<{prevTokenEnd: number, fromTokenStart: number}>) { 132 | let alignmentColumn; 133 | 134 | if (options.collapseExtraSpaces) { 135 | // use greatest endpoint of previous tokens as alignment column 136 | alignmentColumn = lines.reduce((max, line) => { 137 | return Math.max(max, line.prevTokenEnd); 138 | }, 0); 139 | 140 | // add 1 for the space 141 | alignmentColumn += 1; 142 | } else { 143 | // use greatest start of from tokens as alignment column 144 | alignmentColumn = lines.reduce((max, line) => { 145 | return Math.max(max, line.fromTokenStart); 146 | }, 0); 147 | } 148 | 149 | // check if alignment column is lower than minColumnWidth, if defined 150 | if (options.minColumnWidth) 151 | alignmentColumn = Math.max(alignmentColumn, options.minColumnWidth); 152 | 153 | return alignmentColumn; 154 | } 155 | 156 | return { 157 | ImportDeclaration (node) { 158 | if (!isSuitableImport(node)) 159 | return; 160 | 161 | const surroundingImports = findSurroundingImports(node); 162 | 163 | // get the prevTokenEnd and fromTokenStart of all lines 164 | const lines = surroundingImports.map(getLineInfo); 165 | 166 | const alignmentColumn = getAlignmentColumn(lines); 167 | 168 | // get current line info 169 | const line = getLineInfo(node); 170 | 171 | // check alignment of current line 172 | if (line.fromTokenStart !== alignmentColumn) { 173 | reportUnalignedImportStatement(node, alignmentColumn - line.fromTokenStart); 174 | } 175 | }, 176 | }; 177 | }, 178 | }; 179 | 180 | // eslint-disable-next-line arca/no-default-export 181 | export default rule; 182 | -------------------------------------------------------------------------------- /sources/rules/import-ordering.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensure that each import in the file is correctly ordered relative to the others 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import type {Rule} from 'eslint'; 9 | import type * as ESTree from 'estree'; 10 | 11 | import {DEFAULT_SECTIONS_PATTERNS} from '../constants'; 12 | 13 | const rule: Rule.RuleModule = { 14 | meta: { 15 | fixable: `code`, 16 | 17 | schema: [{ 18 | type: `object`, 19 | 20 | properties: { 21 | sections: { 22 | type: `array`, 23 | items: [{type: `string`}], 24 | }, 25 | hoistOneliners: { 26 | type: `boolean`, 27 | }, 28 | }, 29 | }], 30 | }, 31 | 32 | create(context) { 33 | const options = context.options[0] || {}; 34 | 35 | const sectionsPatterns = (options.sections || DEFAULT_SECTIONS_PATTERNS) as Array; 36 | const sectionsRegexps = sectionsPatterns.map(pattern => new RegExp(pattern)); 37 | 38 | const sourceCode = context.getSourceCode(); 39 | 40 | //-------------------------------------------------------------------------- 41 | // Helpers 42 | //-------------------------------------------------------------------------- 43 | 44 | function reportExpectedModuleToBeImportedBeforeAnotherError(node: ESTree.ImportDeclaration, firstErroneousImport: ESTree.ImportDeclaration, reason: string) { 45 | const importDeclarations = (node.parent as ESTree.Program).body.filter(node => { 46 | return node.type === `ImportDeclaration`; 47 | }) as Array; 48 | 49 | return context.report({ 50 | node, 51 | message: `Expected '{{source}}' to be imported before '{{otherSource}}' ({{reason}}).`, 52 | 53 | data: { 54 | source: `${node.source.value}`, 55 | otherSource: `${firstErroneousImport.source.value}`, 56 | reason, 57 | }, 58 | 59 | fix(fixer) { 60 | return sortAndFixAllNodes(fixer, importDeclarations); 61 | }, 62 | }); 63 | } 64 | 65 | function findNonImportStatements(from: ESTree.ImportDeclaration, to: ESTree.ImportDeclaration) { 66 | const nonImportStatements: Array = []; 67 | 68 | const parentBody = (from.parent as ESTree.Program).body; 69 | 70 | const fromIndex = parentBody.indexOf(from); 71 | const toIndex = parentBody.indexOf(to); 72 | 73 | for (let t = fromIndex; t < toIndex; ++t) 74 | if (parentBody[t].type !== `ImportDeclaration`) 75 | nonImportStatements.push(parentBody[t]); 76 | 77 | return nonImportStatements; 78 | } 79 | 80 | function getReplaceStart(node: ESTree.Node) { 81 | let replaceStart: ESTree.BaseNodeWithoutComments = node; 82 | 83 | const comments = sourceCode.getCommentsBefore(node); 84 | for (let t = comments.length - 1; t >= 0; --t) { 85 | if (comments[t].type === `Line`) { 86 | replaceStart = comments[t] as ESTree.BaseNodeWithoutComments; 87 | } else { 88 | break; 89 | } 90 | } 91 | 92 | return replaceStart; 93 | } 94 | 95 | function getReplaceEnd(node: ESTree.BaseNodeWithoutComments) { 96 | return node; 97 | } 98 | 99 | function sortAndFixAllNodes(fixer: Rule.RuleFixer, importDeclarations: Array) { 100 | const firstImportDeclaration = importDeclarations[0]; 101 | const lastImportDeclaration = importDeclarations[importDeclarations.length - 1]; 102 | 103 | const fromRange = getReplaceStart(firstImportDeclaration).range![0]; 104 | const toRange = getReplaceEnd(lastImportDeclaration).range![1]; 105 | 106 | const nonImportStatements = findNonImportStatements( 107 | firstImportDeclaration, 108 | lastImportDeclaration, 109 | ); 110 | 111 | const fixedImports = importDeclarations.slice().sort((a, b) => { 112 | const compared = compareModules(a, b); 113 | if (compared[0] === null) 114 | return 0; 115 | 116 | return compared[0]; 117 | }).reduce((sourceText, declaration, index) => { 118 | let comments = sourceCode.getCommentsBefore(declaration); 119 | 120 | if (declaration === importDeclarations[0]) { 121 | let preservedCommentCount = 0; 122 | while (preservedCommentCount < comments.length && comments[comments.length - preservedCommentCount - 1].type === `Line`) 123 | preservedCommentCount += 1; 124 | 125 | if (preservedCommentCount > 0) { 126 | comments = comments.slice(-preservedCommentCount); 127 | } else { 128 | comments = []; 129 | } 130 | } 131 | 132 | const textComments = comments.map(comment => { 133 | return `${sourceCode.getText(comment as any)}\n`; 134 | }).join(``); 135 | 136 | const textAfterSpecifier = index === importDeclarations.length - 1 137 | ? `` : sourceCode.getText().slice(importDeclarations[index].range![1], sourceCode.getTokenAfter(importDeclarations[index], {includeComments: true})!.range![0]); 138 | 139 | return sourceText + textComments + sourceCode.getText(declaration) + textAfterSpecifier; 140 | }, ``); 141 | 142 | const fixedNonImport = nonImportStatements.map(node => { 143 | return `\n${sourceCode.getText(node)}`; 144 | }).join(``); 145 | 146 | return fixer.replaceTextRange([fromRange, toRange], [ 147 | fixedImports, 148 | fixedNonImport, 149 | ].join(``)); 150 | } 151 | 152 | function getModuleLevel(path: string) { 153 | for (let t = 0, T = sectionsRegexps.length; t < T; ++ t) 154 | if (sectionsRegexps[t].test(path)) 155 | return 1 + t; 156 | 157 | return 0; 158 | } 159 | 160 | function compareModules(a: Rule.Node, b: Rule.Node): [number | null, string?] { 161 | if (a.type !== `ImportDeclaration` || b.type !== `ImportDeclaration`) 162 | return [null]; 163 | 164 | const aSource = a.source.value; 165 | const bSource = b.source.value; 166 | 167 | if (typeof aSource !== `string` || typeof bSource !== `string`) 168 | return [null]; 169 | 170 | const aHasSideEffects = !a.specifiers.length; 171 | const bHasSideEffects = !b.specifiers.length; 172 | const aIsLocal = aSource.startsWith(`.`); 173 | const bIsLocal = bSource.startsWith(`.`); 174 | 175 | if (!aHasSideEffects && bHasSideEffects) { 176 | if (bIsLocal) 177 | return [-1, `local side-effects go last`]; 178 | return [1, `side-effects go first`]; 179 | } 180 | 181 | if (aHasSideEffects && !bHasSideEffects) { 182 | if (aIsLocal) 183 | return [1, `local side-effects go last`]; 184 | return [-1, `side-effects go first`]; 185 | } 186 | 187 | if (aHasSideEffects && bHasSideEffects) { 188 | if (aIsLocal && !bIsLocal) 189 | return [1, `local side-effects go last`]; 190 | if (!aIsLocal && bIsLocal) { 191 | return [-1, `local side-effects go last`]; 192 | } 193 | } 194 | 195 | const aMultiline = a.loc!.start.line !== a.loc!.end.line; 196 | const bMultiline = b.loc!.start.line !== b.loc!.end.line; 197 | 198 | if (options.hoistOneliners) { 199 | if (aMultiline && !bMultiline) 200 | return [1, `multiline imports are last`]; 201 | 202 | if (!aMultiline && bMultiline) { 203 | return [-1, `multiline imports are last`]; 204 | } 205 | } 206 | 207 | if (aSource === bSource) 208 | return [null]; 209 | 210 | const aLevel = getModuleLevel(aSource); 211 | const bLevel = getModuleLevel(bSource); 212 | 213 | if (aLevel < bLevel) { 214 | if (aLevel === 0) { 215 | return [-1, `vendors go first`]; 216 | } else { 217 | return [-1, `'${sectionsPatterns[aLevel - 1]}' goes before '${sectionsPatterns[bLevel - 1]}'`]; 218 | } 219 | } else if (aLevel > bLevel) { 220 | if (bLevel === 0) { 221 | return [1, `vendors go first`]; 222 | } else { 223 | return [1, `'${sectionsPatterns[bLevel - 1]}' goes before '${sectionsPatterns[aLevel - 1]}'`]; 224 | } 225 | } else { 226 | if (bSource.substr(0, aSource.length + 1) === `${aSource}/`) 227 | return [1, `subdirectories go before their indexes`]; 228 | 229 | if (aSource.substr(0, bSource.length + 1) === `${bSource}/`) 230 | return [-1, `subdirectories go before their indexes`]; 231 | 232 | if (bSource.substr(0, aSource.length) === aSource) 233 | return [1, `lexicographic order`]; 234 | 235 | if (aSource.substr(0, bSource.length) === bSource) 236 | return [-1, `lexicographic order`]; 237 | 238 | if (aSource > bSource) { 239 | return [1, `lexicographic order`]; 240 | } else { 241 | return [-1, `lexicographic order`]; 242 | } 243 | } 244 | } 245 | 246 | function findFirstErroneousImport(node: ESTree.ImportDeclaration) { 247 | const parentBody = (node.parent as ESTree.Program).body; 248 | const nodeLocation = parentBody.indexOf(node); 249 | 250 | let iteratorLocation = nodeLocation; 251 | let iteratorNode = parentBody[iteratorLocation]; 252 | 253 | let firstErroneousNode = null; 254 | let firstErrorReason = null; 255 | 256 | function iterate() { 257 | do { 258 | iteratorNode = parentBody[--iteratorLocation]; 259 | } while (iteratorNode && iteratorNode.type !== `ImportDeclaration`); 260 | } 261 | 262 | iterate(); 263 | 264 | for (let errorReason; iteratorNode && (errorReason = compareModules(iteratorNode, node))[0] === 1; iterate()) { 265 | firstErroneousNode = iteratorNode; 266 | firstErrorReason = errorReason[1]!; 267 | } 268 | 269 | if (firstErroneousNode && firstErrorReason) { 270 | return {node: firstErroneousNode as ESTree.ImportDeclaration, reason: firstErrorReason}; 271 | } else { 272 | return null; 273 | } 274 | } 275 | 276 | //-------------------------------------------------------------------------- 277 | // Public 278 | //-------------------------------------------------------------------------- 279 | 280 | return { 281 | ImportDeclaration (node) { 282 | const firstErroneousImport = findFirstErroneousImport(node); 283 | if (!firstErroneousImport) 284 | return ; 285 | 286 | reportExpectedModuleToBeImportedBeforeAnotherError(node, firstErroneousImport.node, firstErroneousImport.reason); 287 | }, 288 | }; 289 | }, 290 | }; 291 | 292 | // eslint-disable-next-line arca/no-default-export 293 | export default rule; 294 | -------------------------------------------------------------------------------- /sources/rules/import-quotes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensure that each import in the file is correctly ordered relative to the others 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import type {Rule} from 'eslint'; 9 | import type * as ESTree from 'estree'; 10 | 11 | const rule: Rule.RuleModule = { 12 | meta: { 13 | fixable: `code`, 14 | }, 15 | 16 | create(context) { 17 | function reportExpectedSingleQuotesError(node: ESTree.ImportDeclaration) { 18 | return context.report({ 19 | node, 20 | message: `Expected import to use single quotes.`, 21 | 22 | fix (fixer) { 23 | const fromRange = node.source.range![0]; 24 | const toRange = node.source.range![1]; 25 | 26 | return fixer.replaceTextRange([fromRange, toRange], `'${node.source.value}'`); 27 | }, 28 | }); 29 | } 30 | 31 | return { 32 | ImportDeclaration(node) { 33 | if (!node.source.raw!.startsWith(`'`)) { 34 | reportExpectedSingleQuotesError(node); 35 | } 36 | }, 37 | }; 38 | }, 39 | }; 40 | 41 | // eslint-disable-next-line arca/no-default-export 42 | export default rule; 43 | -------------------------------------------------------------------------------- /sources/rules/jsx-import-react.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Require from keywords to be aligned 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import type {Rule} from 'eslint'; 9 | import type * as ESTree from 'estree'; 10 | 11 | import * as variableUtils from '../utils/variableUtils'; 12 | 13 | const rule: Rule.RuleModule = { 14 | meta: { 15 | fixable: `code`, 16 | }, 17 | 18 | create(context) { 19 | let isFirstElement = true; 20 | 21 | function reportMissingReactImport(node: ESTree.Node) { 22 | const sourceCode = context.getSourceCode(); 23 | 24 | return context.report({ 25 | node, 26 | message: `Missing React import.`, 27 | 28 | fix (fixer) { 29 | return fixer.insertTextAfter(sourceCode.ast, `\nimport * as React from 'react';`); 30 | }, 31 | }); 32 | } 33 | 34 | function checkIfReactIsInScope(node: ESTree.Node) { 35 | if (!isFirstElement) 36 | return; 37 | 38 | isFirstElement = false; 39 | 40 | const variables = variableUtils.variablesInScope(context); 41 | if (variableUtils.findVariable(variables, `React`)) 42 | return; 43 | 44 | reportMissingReactImport(node); 45 | } 46 | 47 | return { 48 | JSXOpeningElement: checkIfReactIsInScope, 49 | JSXOpeningFragment: checkIfReactIsInScope, 50 | }; 51 | }, 52 | }; 53 | 54 | // eslint-disable-next-line arca/no-default-export 55 | export default rule; 56 | -------------------------------------------------------------------------------- /sources/rules/jsx-longhand-props.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensure that all JSX props use the longhand style 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import type {Rule} from 'eslint'; 9 | import {JSXAttribute} from 'estree-jsx'; 10 | import type * as ESTree from 'estree'; 11 | 12 | const rule: Rule.RuleModule = { 13 | meta: { 14 | fixable: `code`, 15 | }, 16 | 17 | create(context) { 18 | function reportJsxPropsEnforceLonghand(node: ESTree.Literal) { 19 | context.report({ 20 | node: node as any, 21 | loc: node.loc!.start, 22 | message: `JSX props must use the longhand style.`, 23 | 24 | fix(fixer) { 25 | const fromRange = node.range![0]; 26 | const toRange = node.range![1]; 27 | 28 | const sourceCode = context.getSourceCode(); 29 | const text = sourceCode.getText(node); 30 | 31 | return fixer.replaceTextRange([fromRange, toRange], `{${text}}`); 32 | }, 33 | }); 34 | } 35 | 36 | return { 37 | JSXAttribute(node: ESTree.Node) { 38 | const nodeTs = node as any as JSXAttribute; 39 | 40 | if (nodeTs.value?.type === `Literal`) { 41 | reportJsxPropsEnforceLonghand(nodeTs.value); 42 | } 43 | }, 44 | }; 45 | }, 46 | }; 47 | 48 | // eslint-disable-next-line arca/no-default-export 49 | export default rule; 50 | -------------------------------------------------------------------------------- /sources/rules/jsx-no-html-attrs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensure that all JSX props use the longhand style 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import type {Rule} from 'eslint'; 9 | import {JSXAttribute, JSXIdentifier} from 'estree-jsx'; 10 | import type * as ESTree from 'estree'; 11 | 12 | import {camelize} from '../utils'; 13 | 14 | const rule: Rule.RuleModule = { 15 | meta: { 16 | fixable: `code`, 17 | }, 18 | 19 | create(context) { 20 | function reportJsxHtmlAttr(node: JSXIdentifier, replacement: string) { 21 | context.report({ 22 | node: node as any, 23 | loc: node.loc!.start, 24 | message: `This HTML attribute isn't formatted for use in React code.`, 25 | 26 | fix(fixer) { 27 | const fromRange = node.range![0]; 28 | const toRange = node.range![1]; 29 | 30 | return fixer.replaceTextRange([fromRange, toRange], replacement); 31 | }, 32 | }); 33 | } 34 | 35 | return { 36 | JSXAttribute(node: ESTree.Node) { 37 | const nodeTs = node as any as JSXAttribute; 38 | if (nodeTs.name.type !== `JSXIdentifier`) 39 | return; 40 | 41 | const name = nodeTs.name.name; 42 | 43 | if (name === `class`) { 44 | reportJsxHtmlAttr(nodeTs.name, `className`); 45 | return; 46 | } 47 | 48 | const match = name.match(/^(data-)?(.*)$/); 49 | if (!match) 50 | return; 51 | 52 | const fixedName = `${match[1] ?? ``}${camelize(match[2])}`; 53 | if (name !== fixedName) { 54 | reportJsxHtmlAttr(nodeTs.name, fixedName); 55 | return; 56 | } 57 | }, 58 | }; 59 | }, 60 | }; 61 | 62 | // eslint-disable-next-line arca/no-default-export 63 | export default rule; 64 | -------------------------------------------------------------------------------- /sources/rules/jsx-no-string-styles.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensure that all JSX props use the longhand style 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import type {Rule} from 'eslint'; 9 | import {JSXAttribute, JSXExpressionContainer} from 'estree-jsx'; 10 | import type * as ESTree from 'estree'; 11 | 12 | import {camelize} from '../utils'; 13 | 14 | const rule: Rule.RuleModule = { 15 | meta: { 16 | fixable: `code`, 17 | }, 18 | 19 | create(context) { 20 | function reportJsxStyleString(node: ESTree.Literal | JSXExpressionContainer, replacement: string | null) { 21 | context.report({ 22 | node: node as any, 23 | loc: node.loc!.start, 24 | message: `Style props must be passed as objects.`, 25 | 26 | fix: replacement !== null ? fixer => { 27 | const fromRange = node.range![0]; 28 | const toRange = node.range![1]; 29 | 30 | return fixer.replaceTextRange([fromRange, toRange], `{${replacement}}`); 31 | } : undefined, 32 | }); 33 | } 34 | 35 | function generateStyleObject(styleString: string) { 36 | const style: Record = {}; 37 | 38 | let key = ``; 39 | 40 | let buffer = ``; 41 | let parenCount = 0; 42 | 43 | for (let t = 0, T = styleString.length; t <= T; ++t) { 44 | if (t !== T) { 45 | if (styleString[t] === `(`) { 46 | buffer += `(`; 47 | parenCount += 1; 48 | continue; 49 | } 50 | 51 | if (parenCount > 0) { 52 | if (styleString[t] === `)`) 53 | parenCount -= 1; 54 | 55 | buffer += styleString[t]; 56 | continue; 57 | } 58 | 59 | if (styleString[t] === `:`) { 60 | if (!key) { 61 | key = buffer; 62 | buffer = ``; 63 | continue; 64 | } else { 65 | return null; 66 | } 67 | } 68 | } 69 | 70 | if (t === T || styleString[t] === `;`) { 71 | const styleKey = key.trim(); 72 | const styleValue = buffer.trim(); 73 | 74 | if (t === T && !styleKey && !styleValue) 75 | continue; 76 | if (!styleKey || !styleValue) 77 | return null; 78 | 79 | const numMatch = styleValue.match(/^([0-9.]+)(px)?$/); 80 | style[camelize(styleKey)] = numMatch ? parseInt(numMatch[1], 10) : styleValue; 81 | 82 | key = ``; 83 | buffer = ``; 84 | continue; 85 | } 86 | 87 | buffer += styleString[t]; 88 | } 89 | 90 | return JSON.stringify(style); 91 | } 92 | 93 | return { 94 | JSXAttribute(node: ESTree.Node) { 95 | const nodeTs = node as any as JSXAttribute; 96 | if (nodeTs.name.name !== `style`) 97 | return; 98 | 99 | if (nodeTs.value?.type === `Literal` && typeof nodeTs.value?.value === `string`) 100 | reportJsxStyleString(nodeTs.value, generateStyleObject(nodeTs.value.value)); 101 | 102 | if (nodeTs.value?.type === `JSXExpressionContainer`) { 103 | if (nodeTs.value.expression.type === `ObjectExpression`) 104 | return; 105 | 106 | if (nodeTs.value.expression.type === `Literal` && typeof nodeTs.value.expression.value === `string`) { 107 | reportJsxStyleString(nodeTs.value, generateStyleObject(nodeTs.value.expression.value)); 108 | return; 109 | } 110 | 111 | if (nodeTs.value.expression.type === `TemplateLiteral` && nodeTs.value.expression.quasis.length === 1) { 112 | reportJsxStyleString(nodeTs.value, generateStyleObject(nodeTs.value.expression.quasis[0]!.value.cooked!)); 113 | return; 114 | } 115 | 116 | reportJsxStyleString(nodeTs.value, null); 117 | } 118 | }, 119 | }; 120 | }, 121 | }; 122 | 123 | // eslint-disable-next-line arca/no-default-export 124 | export default rule; 125 | -------------------------------------------------------------------------------- /sources/rules/melted-constructs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Enforce the use of melted constructs when possible 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import type {AST, Rule} from 'eslint'; 9 | import type * as ESTree from 'estree'; 10 | 11 | const rule: Rule.RuleModule = { 12 | meta: { 13 | fixable: `code`, 14 | }, 15 | 16 | create(context) { 17 | const FOLLOWUP_TABLE = new Map([ 18 | [`IfStatement`, `if`], 19 | [`ForStatement`, `for`], 20 | [`ForInStatement`, `for-in`], 21 | [`ForOfStatement`, `for-of`], 22 | [`WhileStatement`, `while`], 23 | [`DoWhileStatement`, `do`], 24 | [`SwitchStatement`, `switch`], 25 | [`TryStatement`, `try`], 26 | [`WithStatement`, `with`], 27 | ]); 28 | 29 | function getElseKeyword(node: Rule.Node) { 30 | if (node.type !== `IfStatement`) 31 | return null; 32 | 33 | const sourceCode = context.getSourceCode(); 34 | 35 | let token = sourceCode.getTokenAfter(node.consequent)!; 36 | while (token.type !== `Keyword` || token.value !== `else`) 37 | token = sourceCode.getTokenAfter(token)!; 38 | 39 | return token; 40 | } 41 | 42 | function reportExpectedConstructToBeMeltedWithItsFollowupError(node: Rule.Node, token: AST.Token, name: string, followup: string) { 43 | context.report({ 44 | node, 45 | loc: token.loc.end, 46 | message: `Expected '{{name}}' construct to be melted with its '{{followup}}' followup.`, 47 | 48 | data: { 49 | name, 50 | followup, 51 | }, 52 | }); 53 | } 54 | 55 | function consequentLooksSimilar(node: ESTree.IfStatement) { 56 | const consequent = node.consequent; 57 | if (!consequent) 58 | return false; 59 | 60 | if (consequent.type === `BlockStatement` && consequent.body.length < 1) 61 | return false; 62 | 63 | const lastNode = consequent.type === `BlockStatement` 64 | ? consequent.body[consequent.body.length - 1] 65 | : consequent; 66 | 67 | if (FOLLOWUP_TABLE.has(lastNode.type)) 68 | return true; 69 | 70 | return false; 71 | } 72 | 73 | function checkMeltedConstruct(node: ESTree.IfStatement, name: string) { 74 | if (consequentLooksSimilar(node)) 75 | return ; 76 | 77 | if (!node.alternate) 78 | return ; 79 | 80 | const sourceCode = context.getSourceCode(); 81 | 82 | let checkNode = node.alternate; 83 | while (checkNode.type === `BlockStatement` && checkNode.body.length === 1) 84 | checkNode = checkNode.body[0]; 85 | 86 | const followup = FOLLOWUP_TABLE.get(checkNode.type); 87 | if (!followup) 88 | return; 89 | 90 | const elseToken = getElseKeyword(node)!; 91 | const elseLine = elseToken.loc.end.line; 92 | 93 | const checkToken = sourceCode.getFirstToken(checkNode)!; 94 | const checkLine = checkToken.loc.start.line; 95 | 96 | if (checkNode.type === `BlockStatement` || elseLine !== checkLine) { 97 | reportExpectedConstructToBeMeltedWithItsFollowupError(node, elseToken, name, followup); 98 | } 99 | } 100 | 101 | return { 102 | IfStatement(node) { 103 | checkMeltedConstruct(node, `else`); 104 | }, 105 | }; 106 | }, 107 | }; 108 | 109 | // eslint-disable-next-line arca/no-default-export 110 | export default rule; 111 | -------------------------------------------------------------------------------- /sources/rules/newline-after-import-section.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Require an empty newline after an import section 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import type {Rule} from 'eslint'; 9 | import type * as ESTree from 'estree'; 10 | 11 | import {DEFAULT_SECTIONS_PATTERNS} from '../constants'; 12 | 13 | const rule: Rule.RuleModule = { 14 | meta: { 15 | fixable: `whitespace`, 16 | 17 | schema: [{ 18 | type: `object`, 19 | 20 | properties: { 21 | sections: { 22 | type: `array`, 23 | items: [{type: `string`}], 24 | }, 25 | enableOnelinerSections: { 26 | type: `boolean`, 27 | }, 28 | }, 29 | }], 30 | }, 31 | 32 | create(context) { 33 | const sourceCode = context.getSourceCode(); 34 | 35 | const options = context.options[0] || {}; 36 | 37 | const sectionsPatterns = (options.sections || DEFAULT_SECTIONS_PATTERNS) as Array; 38 | const sectionsRegexps = sectionsPatterns.map(pattern => new RegExp(pattern)); 39 | 40 | //-------------------------------------------------------------------------- 41 | // Helpers 42 | //-------------------------------------------------------------------------- 43 | 44 | function reportExpectedBlankLineAfterImportSection(node: Rule.Node) { 45 | return context.report({ 46 | node, 47 | message: `Expected blank line after import section.`, 48 | 49 | fix(fixer) { 50 | return fixer.insertTextAfter(node, `\n`); 51 | }, 52 | }); 53 | } 54 | 55 | function reportExtraneousBlankLineWithinImportSection(node: Rule.Node) { 56 | return context.report({ 57 | node, 58 | message: `Expected no blank lines between imports of a same section.`, 59 | 60 | fix(fixer) { 61 | const afterNewLine = sourceCode.getIndexFromLoc({line: sourceCode.getLastToken(node)!.loc.end.line + 1, column: 0}); 62 | const beforeNewLine = afterNewLine - 1; 63 | 64 | return fixer.removeRange([beforeNewLine, afterNewLine]); 65 | }, 66 | }); 67 | } 68 | 69 | function getModuleLevel(path: string) { 70 | for (let t = 0, T = sectionsRegexps.length; t < T; ++ t) 71 | if (sectionsRegexps[t].test(path)) 72 | return 1 + t; 73 | 74 | return 0; 75 | } 76 | 77 | //-------------------------------------------------------------------------- 78 | // Public 79 | //-------------------------------------------------------------------------- 80 | 81 | return { 82 | 83 | ImportDeclaration(node) { 84 | if (typeof node.source.value !== `string`) 85 | return; 86 | 87 | const parentBody = (node.parent as ESTree.Program).body; 88 | 89 | const location = parentBody.indexOf(node); 90 | const nextNodeLocation = location + 1; 91 | if (nextNodeLocation === parentBody.length) 92 | return; 93 | 94 | const nextNode = parentBody[nextNodeLocation]; 95 | 96 | const line = node.loc!.end.line; 97 | const nextLine = sourceCode.getTokenAfter(node, {includeComments: true})!.loc!.start.line; 98 | 99 | if (nextNode.type === `ImportDeclaration` && typeof nextNode.source.value === `string`) { 100 | let level = getModuleLevel(node.source.value); 101 | let levelNext = getModuleLevel(nextNode.source.value); 102 | 103 | if (options.enableOnelinerSections) { 104 | if (node.loc!.start.line !== node.loc!.end.line) 105 | level |= 0x10000000; 106 | if (nextNode.loc!.start.line !== nextNode.loc!.end.line) { 107 | levelNext |= 0x10000000; 108 | } 109 | } 110 | 111 | if (level === levelNext) { 112 | if (nextLine - line > 1) { 113 | reportExtraneousBlankLineWithinImportSection(node); 114 | } else { 115 | return; 116 | } 117 | } 118 | } 119 | 120 | if (nextLine - line < 2) { 121 | reportExpectedBlankLineAfterImportSection(node); 122 | } 123 | }, 124 | }; 125 | }, 126 | }; 127 | 128 | // eslint-disable-next-line arca/no-default-export 129 | export default rule; 130 | -------------------------------------------------------------------------------- /sources/rules/no-default-export.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Disallow default exports 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | "use strict"; 8 | 9 | import type {Rule} from 'eslint'; 10 | 11 | const rule: Rule.RuleModule = { 12 | create(context) { 13 | return { 14 | ExportDefaultDeclaration (node) { 15 | context.report({ 16 | node, 17 | message: `Unexpected default export.`, 18 | }); 19 | }, 20 | }; 21 | }, 22 | }; 23 | 24 | // eslint-disable-next-line arca/no-default-export 25 | export default rule; 26 | -------------------------------------------------------------------------------- /sources/utils.ts: -------------------------------------------------------------------------------- 1 | import {possibleStandardNames} from './misc/possibleStandardNames'; 2 | 3 | export function camelize(str: string) { 4 | if (str === str.toUpperCase()) 5 | str = str.toLowerCase(); 6 | 7 | if (Object.prototype.hasOwnProperty.call(possibleStandardNames, str)) 8 | return possibleStandardNames[str]; 9 | 10 | return str.replace(/-+([a-z])/gi, (_, $1) => { 11 | return $1.toUpperCase(); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /sources/utils/variableUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Utility functions for React components detection 3 | * @author Yannick Croissant 4 | */ 5 | import {Rule, Scope} from 'eslint'; 6 | 7 | /** 8 | * Search a particular variable in a list 9 | * @param variables The variables list. 10 | * @param name The name of the variable to search. 11 | * @returns True if the variable was found, false if not. 12 | */ 13 | export function findVariable(variables: Array, name: string) { 14 | return variables.some(variable => variable.name === name); 15 | } 16 | 17 | /** 18 | * Find and return a particular variable in a list 19 | * @param variables The variables list. 20 | * @param name The name of the variable to search. 21 | * @returns Variable if the variable was found, null if not. 22 | */ 23 | export function getVariable(variables: Array, name: string) { 24 | return variables.find(variable => variable.name === name); 25 | } 26 | 27 | /** 28 | * List all variable in a given scope 29 | * 30 | * Contain a patch for babel-eslint to avoid https://github.com/babel/babel-eslint/issues/21 31 | * 32 | * @param context The current rule context. 33 | * @returns The variables list 34 | */ 35 | export function variablesInScope(context: Rule.RuleContext) { 36 | let scope: Scope.Scope | null = context.getScope(); 37 | let variables = scope.variables; 38 | 39 | while (scope && scope.upper) { 40 | scope = scope.upper; 41 | variables = scope.variables.concat(variables); 42 | } 43 | 44 | if (scope.childScopes.length) { 45 | variables = scope.childScopes[0].variables.concat(variables); 46 | if (scope.childScopes[0].childScopes.length) { 47 | variables = scope.childScopes[0].childScopes[0].variables.concat(variables); 48 | } 49 | } 50 | 51 | variables.reverse(); 52 | 53 | return variables; 54 | } 55 | 56 | /** 57 | * Find a variable by name in the current scope. 58 | * @param context The current rule context. 59 | * @param name Name of the variable to look for. 60 | * @returns Return null if the variable could not be found, ASTNode otherwise. 61 | */ 62 | export function findVariableByName(context: Rule.RuleContext, name: string) { 63 | const variable = getVariable(variablesInScope(context), name); 64 | 65 | if (!variable || !variable.defs[0] || !variable.defs[0].node) 66 | return null; 67 | 68 | 69 | if (variable.defs[0].node.type === `TypeAlias`) 70 | return variable.defs[0].node.right; 71 | 72 | 73 | return variable.defs[0].node.init; 74 | } 75 | 76 | /** 77 | * Returns the latest definition of the variable. 78 | * @param variable 79 | * @returns The latest variable definition or undefined. 80 | */ 81 | export function getLatestVariableDefinition(variable: Scope.Variable) { 82 | return variable.defs[variable.defs.length - 1]; 83 | } 84 | -------------------------------------------------------------------------------- /tests/rules/curly.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensure that curly braces keep the code flow easy to read 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import rule from 'eslint-plugin-arca/sources/rules/curly'; 9 | import {RuleTester} from 'eslint'; 10 | 11 | const ruleTester = new RuleTester(); 12 | 13 | ruleTester.run(`curly`, rule, { 14 | valid: [{ 15 | code: `if (test)\n test();\n`, 16 | }, { 17 | code: `for (var test in test)\n test();\n`, 18 | }, { 19 | code: `if (test) {\n test = {\n test: test\n };\n}\n`, 20 | }, { 21 | code: `if (test)\n if(test)\n test();\n`, 22 | }, { 23 | code: `function test() {\n if (test) {\n test();\n }\n};\n`, 24 | }, { 25 | code: `function test() {\n if (test) {\n test();\n } else {\n test();\n }\n};\n`, 26 | }, { 27 | code: `function test() {\n if (test)\n test();\n else\n test();\n if (test) {\n test();\n }\n};\n`, 28 | }, { 29 | code: `function test() {\n if (test) {\n test();\n } else if (test) {\n test();\n }\n};\n`, 30 | parserOptions: {ecmaVersion: 6}, 31 | }, { 32 | code: `function test() {\n if (test) {\n test();\n } else if (test) {\n test();\n } else if (test) {\n test();\n }\n};\n`, 33 | parserOptions: {ecmaVersion: 6}, 34 | }, { 35 | code: `if (test)\n test();\nelse for (;test;)\n test();\n`, 36 | }, { 37 | code: `if (test)\n test();\nelse for (var test in test)\n test();\n`, 38 | }, { 39 | code: `if (test)\n test();\nelse for (var test of test)\n test();\n`, 40 | parserOptions: {ecmaVersion: 6}, 41 | }], 42 | 43 | invalid: [{ 44 | code: `for (var test in test) {\n test();\n}\n`, 45 | errors: [{message: `Unnecessary { after 'for-in'.`}], 46 | output: `for (var test in test) \n test();\n\n`, 47 | }, { 48 | code: `if (test)\n test = {\n test: test\n };\n`, 49 | errors: [{message: `Expected { after 'if' condition.`}], 50 | }, { 51 | code: `if (test) {\n doSomething();\n}\n`, 52 | errors: [{message: `Unnecessary { after 'if' condition.`}], 53 | output: `if (test) \n doSomething();\n\n`, 54 | }, { 55 | code: `if (test)\n if (test) {\n test();\n }\n`, 56 | errors: [{message: `Expected { after 'if' condition.`}, {message: `Unnecessary { after 'if' condition.`}], 57 | output: `if (test)\n if (test) \n test();\n \n`, 58 | }, { 59 | code: `function test() {\n if (test)\n test();\n}\n`, 60 | errors: [{message: `Expected { after 'if' condition.`}], 61 | }, { 62 | code: `function test() {\n if (test)\n test();\n else\n test();\n}\n`, 63 | errors: [{message: `Expected { after 'if' condition.`}, {message: `Expected { after 'else'.`}], 64 | }, { 65 | code: `if (test) {\n test();\n} else\n for (var test in test)\n test();\n`, 66 | errors: [{message: `Unnecessary { after 'if' condition.`}], 67 | output: `if (test) \n test();\n else\n for (var test in test)\n test();\n`, 68 | }], 69 | }); 70 | -------------------------------------------------------------------------------- /tests/rules/import-absolutes.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensure that each import in the file is correctly ordered relative to the others 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import rule from 'eslint-plugin-arca/sources/rules/import-absolutes'; 9 | import {RuleTester} from 'eslint'; 10 | 11 | const parserOptions = {sourceType: `module`, ecmaVersion: 2015} as const; 12 | const ruleTester = new RuleTester(); 13 | 14 | ruleTester.run(`import-absolutes`, rule, { 15 | valid: [{ 16 | code: `import 'foo';\nimport bar1 from 'bar/bar';\nimport bar2 from 'bar';\nimport foo1 from 'foo/foo';\nimport foo2 from 'foo';\n\nimport bar3 from 'common/bar';\nimport foo3 from 'common/foo';\n\nimport bar4 from 'app/bar/bar';\nimport foo4 from 'app/bar/foo';\nimport bar5 from 'app/foo/bar';\nimport foo5 from 'app/foo/foo';\n`, 17 | parserOptions, 18 | }, { 19 | // When the path is the same, keep user's order 20 | code: `import {bar1} from 'bar';\nimport {bar2} from 'bar';\nimport baz from 'baz';\nimport {foo2} from 'foo';\nimport {foo1} from 'foo';`, 21 | parserOptions, 22 | }, { 23 | code: `import './foo';\n`, 24 | parserOptions, 25 | options: [{preferRelative: `^\\.\\/[^\\/]*$`}], 26 | }, { 27 | code: `import 'eslint-plugin-arca-actually-another-package/foo';`, 28 | parserOptions, 29 | }, { 30 | code: `import 'eslint-plugin-arca';`, 31 | parserOptions, 32 | }], 33 | invalid: [{ 34 | code: `import '.';\n`, 35 | output: `import 'eslint-plugin-arca/tests/rules';\n`, 36 | filename: __filename, 37 | parserOptions, 38 | errors: [{message: `Expected relative import to be package-absolute (rather than '.').`}], 39 | }, { 40 | code: `import '..';\n`, 41 | output: `import 'eslint-plugin-arca/tests';\n`, 42 | filename: __filename, 43 | parserOptions, 44 | errors: [{message: `Expected relative import to be package-absolute (rather than '..').`}], 45 | }, { 46 | code: `import 'eslint-plugin-arca/tests/foo';\n`, 47 | output: `import '../foo';\n`, 48 | filename: __filename, 49 | parserOptions, 50 | errors: [{message: `Expected absolute import to be relative (rather than 'eslint-plugin-arca/tests/foo').`}], 51 | options: [{preferRelative: `foo`}], 52 | }, { 53 | code: `import './';\n`, 54 | output: `import 'eslint-plugin-arca/tests/rules';\n`, 55 | filename: __filename, 56 | parserOptions, 57 | errors: [{message: `Expected relative import to be package-absolute (rather than './').`}], 58 | }, { 59 | code: `import 'eslint-plugin-arca/tests/workspace/lib';\n`, 60 | output: `import 'eslint-plugin-arca-test-workspace/lib';\n`, 61 | filename: __filename, 62 | parserOptions, 63 | errors: [{message: `Expected absolute import to start with 'eslint-plugin-arca-test-workspace/' prefix (rather than 'eslint-plugin-arca/tests/workspace/').`}] 64 | }, { 65 | code: `import 'eslint-plugin-arca/tests/workspace';\n`, 66 | output: `import 'eslint-plugin-arca-test-workspace';\n`, 67 | filename: __filename, 68 | parserOptions, 69 | errors: [{message: `Expected absolute import to be 'eslint-plugin-arca-test-workspace' (rather than 'eslint-plugin-arca/tests/workspace').`}] 70 | }, { 71 | code: `import './';\n`, 72 | output: `import './index';\n`, 73 | filename: __filename, 74 | parserOptions, 75 | errors: [{message: `Expected relative import to be normalized (rather than './').`}], 76 | options: [{preferRelative: `^\\.\\/[^\\/]*$`}], 77 | }, { 78 | code: `import '..';\n`, 79 | output: `import '../index';\n`, 80 | filename: __filename, 81 | parserOptions, 82 | errors: [{message: `Expected relative import to be normalized (rather than '..').`}], 83 | options: [{preferRelative: `..`}], 84 | }, { 85 | code: `import './foo';\n`, 86 | output: `import 'eslint-plugin-arca/tests/rules/foo';\n`, 87 | filename: __filename, 88 | parserOptions, 89 | errors: [{message: `Expected relative import to be package-absolute (rather than './foo').`}], 90 | }, { 91 | code: `import '../foo';\n`, 92 | output: `import 'eslint-plugin-arca/tests/foo';\n`, 93 | filename: __filename, 94 | parserOptions, 95 | errors: [{message: `Expected relative import to be package-absolute (rather than '../foo').`}], 96 | options: [{preferRelative: `^\\.\\/[^\\/]*$`}], 97 | }, { 98 | code: `import './/foo';\n`, 99 | output: `import './foo';\n`, 100 | filename: __filename, 101 | parserOptions, 102 | errors: [{message: `Expected relative import to be normalized (rather than './/foo').`}], 103 | options: [{preferRelative: `^\\.\\/[^\\/]*$`}], 104 | }, { 105 | code: `import '../foo';\n`, 106 | output: `import 'fff/foo';\n`, 107 | filename: __filename, 108 | parserOptions, 109 | errors: [{message: `Expected relative import to be package-absolute (rather than '../foo').`}], 110 | options: [{replaceAbsolutePathStart: [{from: `eslint-plugin-arca/tests`, to: `fff`}]}]}, 111 | { 112 | code: `import 'eslint-plugin-arca/tests/rules/bar';\n`, 113 | output: `import 'fff/rules/bar';\n`, 114 | filename: __filename, 115 | parserOptions, 116 | errors: [{message: `Expected absolute import to start with 'fff/' prefix (rather than 'eslint-plugin-arca/tests/').`}], 117 | options: [{replaceAbsolutePathStart: [{from: `eslint-plugin-arca/tests`, to: `fff`}]}], 118 | }, { 119 | code: `import 'eslint-plugin-arca/tests/rules/baz';\nimport 'eslint-plugin-arca/tests/rules/jeej/baz';\n`, 120 | output: `import './baz';\nimport 'eslint-plugin-arca/tests/rules/jeej/baz';\n`, 121 | filename: __filename, 122 | parserOptions, 123 | errors: [{message: `Expected absolute import to be relative (rather than 'eslint-plugin-arca/tests/rules/baz').`}], 124 | options: [{preferRelative: `^\\.\\/[^\\/]*$`}]}], 125 | }); 126 | -------------------------------------------------------------------------------- /tests/rules/import-align.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Require from keywords to be aligned 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import rule from 'eslint-plugin-arca/sources/rules/import-align'; 9 | import {RuleTester} from 'eslint'; 10 | 11 | const parserOptions = {sourceType: `module`, ecmaVersion: 2015} as const; 12 | const ruleTester = new RuleTester(); 13 | 14 | ruleTester.run(`import-align`, rule, { 15 | valid: [{ 16 | code: `import foo from 'foo';\nimport bar from 'bar';\n`, 17 | parserOptions, 18 | }, { 19 | code: `import foo from 'foo';\nimport supercalifragilisticexpialidocious from 'supercalifragilisticexpialidocious';\n`, 20 | parserOptions, 21 | }, { 22 | code: `import foo from 'foo';\nimport supercalifragilisticexpialidocious from 'supercalifragilisticexpialidocious';\nimport {\n A,\n B\n} from 'foo';\n`, 23 | parserOptions, 24 | }, { 25 | code: `import foo from 'foo';\nimport bar from 'bar';\n`, 26 | parserOptions, 27 | options: [{minColumnWidth: 20}], 28 | }, { 29 | code: `import supercalifragilisticexpialidocious from 'foo';\nimport bar from 'bar';\n`, 30 | parserOptions, 31 | options: [{minColumnWidth: 20}], 32 | }, { 33 | code: `import foo from 'foo';\nimport bar from 'bar';\n`, 34 | parserOptions, 35 | options: [{collapseExtraSpaces: false}], 36 | }], 37 | invalid: [{ 38 | code: `import foo from 'foo';\nimport supercalifragilisticexpialidocious from 'supercalifragilisticexpialidocious';`, 39 | parserOptions, 40 | output: `import foo from 'foo';\nimport supercalifragilisticexpialidocious from 'supercalifragilisticexpialidocious';`, 41 | errors: [{message: `Unaligned import statement`}], 42 | }, { 43 | code: `import foo from 'foo';\n\nimport bar from 'bar';\nimport supercalifragilisticexpialidocious from 'supercalifragilisticexpialidocious';`, 44 | parserOptions, 45 | output: `import foo from 'foo';\n\nimport bar from 'bar';\nimport supercalifragilisticexpialidocious from 'supercalifragilisticexpialidocious';`, 46 | errors: [{message: `Unaligned import statement`}], 47 | }, { 48 | code: `import foo from 'foo';\nimport bar from 'bar';\n`, 49 | parserOptions, 50 | output: `import foo from 'foo';\nimport bar from 'bar';\n`, 51 | options: [{collapseExtraSpaces: true}], 52 | errors: [{message: `Unaligned import statement`}, {message: `Unaligned import statement`}], 53 | }, { 54 | code: `import foo from 'foo';\nimport bar from 'bar';\n`, 55 | parserOptions, 56 | output: `import foo from 'foo';\nimport bar from 'bar';\n`, 57 | options: [{collapseExtraSpaces: true}], 58 | errors: [{message: `Unaligned import statement`}, {message: `Unaligned import statement`}], 59 | }, { 60 | code: `import foo from 'foo';\nimport bar from 'bar';\n`, 61 | parserOptions, 62 | output: `import foo from 'foo';\nimport bar from 'bar';\n`, 63 | options: [{collapseExtraSpaces: true}], 64 | errors: [{message: `Unaligned import statement`}], 65 | }, { 66 | code: `import { foo } from 'foo';\nimport bar from 'bar';\n`, 67 | parserOptions, 68 | output: `import { foo } from 'foo';\nimport bar from 'bar';\n`, 69 | options: [{collapseExtraSpaces: true}], 70 | errors: [{message: `Unaligned import statement`}, {message: `Unaligned import statement`}], 71 | }, { 72 | code: `import { foo } from 'foo';\nimport bar from 'bar';\n`, 73 | parserOptions, 74 | output: `import { foo } from 'foo';\nimport bar from 'bar';\n`, 75 | options: [{collapseExtraSpaces: true}], 76 | errors: [{message: `Unaligned import statement`}], 77 | }, { 78 | code: `import foo from 'foo';\nimport bar from 'bar';\n`, 79 | output: `import foo from 'foo';\nimport bar from 'bar';\n`, 80 | parserOptions, 81 | options: [{minColumnWidth: 20}], 82 | errors: [{message: `Unaligned import statement`}, {message: `Unaligned import statement`}], 83 | }, { 84 | code: `import foo from 'foo';\nimport bar from 'bar';\n`, 85 | output: `import foo from 'foo';\nimport bar from 'bar';\n`, 86 | parserOptions, 87 | options: [{minColumnWidth: 20, collapseExtraSpaces: true}], 88 | errors: [{message: `Unaligned import statement`}, {message: `Unaligned import statement`}], 89 | }], 90 | }); 91 | -------------------------------------------------------------------------------- /tests/rules/import-ordering.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensure that each import in the file is correctly ordered relative to the others 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import rule from 'eslint-plugin-arca/sources/rules/import-ordering'; 9 | import {RuleTester} from 'eslint'; 10 | 11 | const parserOptions = {sourceType: `module`, ecmaVersion: 2015} as const; 12 | const ruleTester = new RuleTester(); 13 | 14 | ruleTester.run(`import-ordering`, rule, { 15 | valid: [{ 16 | code: `import 'foo';\nimport bar1 from 'bar/bar';\nimport bar2 from 'bar';\nimport foo1 from 'foo/foo';\nimport foo2 from 'foo';\n\nimport bar3 from 'common/bar';\nimport foo3 from 'common/foo';\n\nimport bar4 from 'app/bar/bar';\nimport foo4 from 'app/bar/foo';\nimport bar5 from 'app/foo/bar';\nimport foo5 from 'app/foo/foo';\nimport './paz';\n`, 17 | parserOptions, 18 | }, { 19 | // When the path is the same, keep user's order 20 | code: `import {bar1} from 'bar';\nimport {bar2} from 'bar';\nimport baz from 'baz';\nimport {foo2} from 'foo';\nimport {foo1} from 'foo';`, 21 | parserOptions, 22 | }], 23 | invalid: [{ 24 | code: `import { bar } from 'bar';\nimport 'foo';\n`, 25 | output: `import 'foo';\nimport { bar } from 'bar';\n`, 26 | parserOptions, 27 | errors: [{message: `Expected 'foo' to be imported before 'bar' (side-effects go first).`}], 28 | }, 29 | { 30 | code: `import './paz';\nimport { bar } from 'bar';\n`, 31 | output: `import { bar } from 'bar';\nimport './paz';\n`, 32 | parserOptions, 33 | errors: [{message: `Expected 'bar' to be imported before './paz' (local side-effects go last).`}], 34 | }, 35 | { 36 | code: `import './paz';\nimport 'foo';\n`, 37 | output: `import 'foo';\nimport './paz';\n`, 38 | parserOptions, 39 | errors: [{message: `Expected 'foo' to be imported before './paz' (local side-effects go last).`}], 40 | }, 41 | { 42 | code: `import './paz';\nimport { bar } from 'bar';\nimport 'foo';\n`, 43 | output: `import 'foo';\nimport { bar } from 'bar';\nimport './paz';\n`, 44 | parserOptions, 45 | errors: [ 46 | {message: `Expected 'bar' to be imported before './paz' (local side-effects go last).`}, 47 | {message: `Expected 'foo' to be imported before './paz' (local side-effects go last).`}, 48 | ], 49 | }, 50 | { 51 | code: `import foo from 'common/foo';\n\nimport bar from 'bar';\n`, 52 | output: `import bar from 'bar';\n\nimport foo from 'common/foo';\n`, 53 | parserOptions, 54 | errors: [{message: `Expected 'bar' to be imported before 'common/foo' (vendors go first).`}], 55 | }, { 56 | code: `import foo from 'app/foo';\n\nimport bar from 'common/bar';\n`, 57 | output: `import bar from 'common/bar';\n\nimport foo from 'app/foo';\n`, 58 | parserOptions, 59 | errors: [{message: `Expected 'common/bar' to be imported before 'app/foo' ('^common/' goes before '^app/').`}], 60 | }, { 61 | code: `import foo1 from 'foo';\nimport foo2 from 'foo/foo';\n`, 62 | output: `import foo2 from 'foo/foo';\nimport foo1 from 'foo';\n`, 63 | parserOptions, 64 | errors: [{message: `Expected 'foo/foo' to be imported before 'foo' (subdirectories go before their indexes).`}], 65 | }, { 66 | code: `import foo1 from 'common/foo';\nimport foo2 from 'common/foo/foo';\n`, 67 | output: `import foo2 from 'common/foo/foo';\nimport foo1 from 'common/foo';\n`, 68 | parserOptions, 69 | errors: [{message: `Expected 'common/foo/foo' to be imported before 'common/foo' (subdirectories go before their indexes).`}], 70 | }, { 71 | code: `import foo1 from 'app/foo';\nimport foo2 from 'app/foo/foo';\n`, 72 | output: `import foo2 from 'app/foo/foo';\nimport foo1 from 'app/foo';\n`, 73 | parserOptions, 74 | errors: [{message: `Expected 'app/foo/foo' to be imported before 'app/foo' (subdirectories go before their indexes).`}], 75 | }, { 76 | code: `import foo from 'foo';\nimport bar from 'bar';\n`, 77 | output: `import bar from 'bar';\nimport foo from 'foo';\n`, 78 | parserOptions, 79 | errors: [{message: `Expected 'bar' to be imported before 'foo' (lexicographic order).`}], 80 | }, { 81 | code: `import foo from 'common/foo';\nimport bar from 'common/bar';\n`, 82 | output: `import bar from 'common/bar';\nimport foo from 'common/foo';\n`, 83 | parserOptions, 84 | errors: [{message: `Expected 'common/bar' to be imported before 'common/foo' (lexicographic order).`}], 85 | }, { 86 | code: `import foo from 'app/foo';\nimport bar from 'app/bar';\n`, 87 | output: `import bar from 'app/bar';\nimport foo from 'app/foo';\n`, 88 | parserOptions, 89 | errors: [{message: `Expected 'app/bar' to be imported before 'app/foo' (lexicographic order).`}], 90 | }, { 91 | code: `import bar1 from 'bar';\nimport bar2 from 'common/bar';\nimport foo1 from 'common/foo';\nimport bar3 from 'app/bar';\nimport foo2 from 'app/foo';\nimport foo3 from 'foo';\n`, 92 | output: `import bar1 from 'bar';\nimport foo3 from 'foo';\nimport bar2 from 'common/bar';\nimport foo1 from 'common/foo';\nimport bar3 from 'app/bar';\nimport foo2 from 'app/foo';\n`, 93 | parserOptions, 94 | errors: [{message: `Expected 'foo' to be imported before 'common/bar' (vendors go first).`}], 95 | }, { 96 | code: `import bar1 from 'common/bar';\nimport foo1 from 'common/foo';\nimport bar2 from 'app/bar';\nimport foo2 from 'app/foo';\nimport foo3 from 'foo';\nimport bar3 from 'bar';\n`, 97 | output: `import bar3 from 'bar';\nimport foo3 from 'foo';\nimport bar1 from 'common/bar';\nimport foo1 from 'common/foo';\nimport bar2 from 'app/bar';\nimport foo2 from 'app/foo';\n`, 98 | parserOptions, 99 | errors: [{message: `Expected 'foo' to be imported before 'common/bar' (vendors go first).`}, {message: `Expected 'bar' to be imported before 'common/bar' (vendors go first).`}], 100 | }, { 101 | code: `import foo from 'foo';\nimport {\n bar\n} from 'bar';\n`, 102 | output: `import {\n bar\n} from 'bar';\nimport foo from 'foo';\n`, 103 | parserOptions, 104 | errors: [{message: `Expected 'bar' to be imported before 'foo' (lexicographic order).`}], 105 | }, { 106 | code: `import foo from 'foo';\n// hello-world\nimport {\n bar\n} from 'bar';\n`, 107 | output: `// hello-world\nimport {\n bar\n} from 'bar';\nimport foo from 'foo';\n`, 108 | parserOptions, 109 | errors: [{message: `Expected 'bar' to be imported before 'foo' (lexicographic order).`}], 110 | }, { 111 | code: `// hello-world\nimport foo from 'foo';\nimport {\n bar\n} from 'bar';\n`, 112 | output: `import {\n bar\n} from 'bar';\n// hello-world\nimport foo from 'foo';\n`, 113 | parserOptions, 114 | errors: [{message: `Expected 'bar' to be imported before 'foo' (lexicographic order).`}], 115 | }, { 116 | code: `/* hello-world */\nimport foo from 'foo';\nimport {\n bar\n} from 'bar';\n`, 117 | output: `/* hello-world */\nimport {\n bar\n} from 'bar';\nimport foo from 'foo';\n`, 118 | parserOptions, 119 | errors: [{message: `Expected 'bar' to be imported before 'foo' (lexicographic order).`}], 120 | }, { 121 | code: `import foo from 'foo';\nimport {\n bar\n} from 'bar';\n/* hello-world */\n`, 122 | output: `import {\n bar\n} from 'bar';\nimport foo from 'foo';\n/* hello-world */\n`, 123 | parserOptions, 124 | errors: [{message: `Expected 'bar' to be imported before 'foo' (lexicographic order).`}], 125 | }, { 126 | code: `import foo from 'foo';\nimport {\n bar\n} from 'bar';\n// hello-world\n`, 127 | output: `import {\n bar\n} from 'bar';\nimport foo from 'foo';\n// hello-world\n`, 128 | parserOptions, 129 | errors: [{message: `Expected 'bar' to be imported before 'foo' (lexicographic order).`}], 130 | }, { 131 | code: `import foo from 'foo';\nvar hello = 42;\nimport bar from 'bar';\n`, 132 | output: `import bar from 'bar';\nimport foo from 'foo';\nvar hello = 42;\n`, 133 | parserOptions, 134 | errors: [{message: `Expected 'bar' to be imported before 'foo' (lexicographic order).`}], 135 | }, { 136 | code: `import {\n bar1,\n bar2,\n} from 'bar';\nimport foo from 'foo';\n`, 137 | output: `import foo from 'foo';\nimport {\n bar1,\n bar2,\n} from 'bar';\n`, 138 | parserOptions, 139 | errors: [{message: `Expected 'foo' to be imported before 'bar' (multiline imports are last).`}], 140 | options: [{hoistOneliners: true}], 141 | }], 142 | }); 143 | -------------------------------------------------------------------------------- /tests/rules/import-quotes.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensure that each import in the file is correctly ordered relative to the others 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import rule from 'eslint-plugin-arca/sources/rules/import-quotes'; 9 | import {RuleTester} from 'eslint'; 10 | 11 | const parserOptions = {sourceType: `module`, ecmaVersion: 2015} as const; 12 | const ruleTester = new RuleTester(); 13 | 14 | ruleTester.run(`import-quotes`, rule, { 15 | valid: [{ 16 | code: `import 'foo';\n`, 17 | parserOptions, 18 | }], 19 | invalid: [{ 20 | code: `import "foo";\n`, 21 | output: `import 'foo';\n`, 22 | parserOptions, 23 | errors: [{message: `Expected import to use single quotes.`}], 24 | }], 25 | }); 26 | -------------------------------------------------------------------------------- /tests/rules/jsx-import-react.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensure that each import in the file is correctly ordered relative to the others 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import rule from 'eslint-plugin-arca/sources/rules/jsx-import-react'; 9 | import {RuleTester} from 'eslint'; 10 | 11 | const parserOptions = {sourceType: `module`, ecmaVersion: 2015, ecmaFeatures: {jsx: true}} as const; 12 | const ruleTester = new RuleTester(); 13 | 14 | ruleTester.run(`jsx-import-react`, rule, { 15 | valid: [{ 16 | code: `import * as React from 'react';\nconst node =
;\n`, 17 | parserOptions, 18 | }], 19 | invalid: [{ 20 | code: `const node =
;\n`, 21 | output: `const node =
;\nimport * as React from 'react';\n`, 22 | parserOptions, 23 | errors: [{message: `Missing React import.`}], 24 | }], 25 | }); 26 | -------------------------------------------------------------------------------- /tests/rules/jsx-longhand-props.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensure that each import in the file is correctly ordered relative to the others 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import rule from 'eslint-plugin-arca/sources/rules/jsx-longhand-props'; 9 | import {RuleTester} from 'eslint'; 10 | 11 | const parserOptions = {sourceType: `module`, ecmaVersion: 2015, ecmaFeatures: {jsx: true}} as const; 12 | const ruleTester = new RuleTester(); 13 | 14 | ruleTester.run(`jsx-longhand-props`, rule, { 15 | valid: [{ 16 | code: `\n`, 17 | parserOptions, 18 | }], 19 | invalid: [{ 20 | code: `\n`, 21 | output: `\n`, 22 | parserOptions, 23 | errors: [{message: `JSX props must use the longhand style.`}], 24 | }], 25 | }); 26 | -------------------------------------------------------------------------------- /tests/rules/jsx-no-html-attrs.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensure that each import in the file is correctly ordered relative to the others 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import rule from 'eslint-plugin-arca/sources/rules/jsx-no-html-attrs'; 9 | import {RuleTester} from 'eslint'; 10 | 11 | const parserOptions = {sourceType: `module`, ecmaVersion: 2015, ecmaFeatures: {jsx: true}} as const; 12 | const ruleTester = new RuleTester(); 13 | 14 | ruleTester.run(`jsx-no-html-attrs`, rule, { 15 | valid: [{ 16 | code: `\n`, 17 | parserOptions, 18 | }, { 19 | code: `\n`, 20 | parserOptions, 21 | }, { 22 | code: `\n`, 23 | parserOptions, 24 | }, { 25 | code: `\n`, 26 | parserOptions, 27 | }], 28 | invalid: [{ 29 | code: `\n`, 30 | output: `\n`, 31 | parserOptions, 32 | errors: [{message: `This HTML attribute isn't formatted for use in React code.`}], 33 | }, { 34 | code: `\n`, 35 | output: `\n`, 36 | parserOptions, 37 | errors: [{message: `This HTML attribute isn't formatted for use in React code.`}], 38 | }, { 39 | code: `\n`, 40 | output: `\n`, 41 | parserOptions, 42 | errors: [{message: `This HTML attribute isn't formatted for use in React code.`}], 43 | }, { 44 | code: `\n`, 45 | output: `\n`, 46 | parserOptions, 47 | errors: [{message: `This HTML attribute isn't formatted for use in React code.`}], 48 | }], 49 | }); 50 | -------------------------------------------------------------------------------- /tests/rules/jsx-no-string-styles.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Ensure that each import in the file is correctly ordered relative to the others 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import rule from 'eslint-plugin-arca/sources/rules/jsx-no-string-styles'; 9 | import {RuleTester} from 'eslint'; 10 | 11 | const parserOptions = {sourceType: `module`, ecmaVersion: 2015, ecmaFeatures: {jsx: true}} as const; 12 | const ruleTester = new RuleTester(); 13 | 14 | ruleTester.run(`jsx-no-string-styles`, rule, { 15 | valid: [{ 16 | code: `\n`, 17 | parserOptions, 18 | }, { 19 | code: `\n`, 20 | parserOptions, 21 | }], 22 | invalid: [{ 23 | code: `\n`, 24 | output: `\n`, 25 | parserOptions, 26 | errors: [{message: `Style props must be passed as objects.`}], 27 | }, { 28 | code: `\n`, 29 | output: `\n`, 30 | parserOptions, 31 | errors: [{message: `Style props must be passed as objects.`}], 32 | }, { 33 | code: `\n`, 34 | output: `\n`, 35 | parserOptions, 36 | errors: [{message: `Style props must be passed as objects.`}], 37 | }, { 38 | code: `\n`, 39 | output: `\n`, 40 | parserOptions, 41 | errors: [{message: `Style props must be passed as objects.`}], 42 | }, { 43 | code: `\n`, 44 | output: `\n`, 45 | parserOptions, 46 | errors: [{message: `Style props must be passed as objects.`}], 47 | }, { 48 | code: `\n`, 49 | output: `\n`, 50 | parserOptions, 51 | errors: [{message: `Style props must be passed as objects.`}], 52 | }], 53 | }); 54 | -------------------------------------------------------------------------------- /tests/rules/melted-constructs.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Enforce the use of melted constructs when possible 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | "use strict"; 8 | 9 | import rule from 'eslint-plugin-arca/sources/rules/melted-constructs'; 10 | import {RuleTester} from 'eslint'; 11 | 12 | const parserOptions = {ecmaVersion: 2015} as const; 13 | const ruleTester = new RuleTester(); 14 | 15 | ruleTester.run(`melted-constructs`, rule, { 16 | valid: [{ 17 | code: `if (test)\n test();\nelse if (test)\n test();\n`, 18 | }, { 19 | code: `if (test)\n test();\nelse for (;test;)\n test();\n`, 20 | }, { 21 | code: `if (test)\n test();\nelse for (var test in test)\n test();\n`, 22 | }, { 23 | code: `if (test)\n test();\nelse for (var test of test)\n test();\n`, 24 | parserOptions, 25 | }, { 26 | code: `if (test)\n test();\nelse while (test)\n test();`, 27 | }, { 28 | code: `if (test)\n test();\n else do {\n test();\n} while (test);\n`, 29 | }, { 30 | code: `if (test)\n test();\n else switch (test) {\n // nothing\n}\n`, 31 | }, { 32 | code: `if (test)\n test();\n else try {\n test();\n} catch (e) {\n test();\n}\n`, 33 | }, { 34 | code: `if (test)\n test();\n else with (test) {\n test();\n}\n`, 35 | }, { 36 | code: `if (test)\n test();\nelse if (test)\n test();\nelse for (var test in test)\n test();\n`, 37 | }, { 38 | code: `if (test) {\n if (test) {\n test();\n }\n} else {\n if (test) {\n test();\n }\n}\n`, 39 | }], 40 | invalid: [{ 41 | code: `if (test)\n test();\nelse\n if (test)\n test();\n`, 42 | errors: [{message: `Expected 'else' construct to be melted with its 'if' followup.`}], 43 | }, { 44 | code: `if (test)\n test();\nelse\n for (;test;) {\n test();\n }\n`, 45 | errors: [{message: `Expected 'else' construct to be melted with its 'for' followup.`}], 46 | }, { 47 | code: `if (test)\n test();\nelse\n for (var test in test)\n test();\n`, 48 | errors: [{message: `Expected 'else' construct to be melted with its 'for-in' followup.`}], 49 | }, { 50 | code: `if (test)\n test();\nelse\n for (var test of test)\n test();\n`, 51 | parserOptions: {ecmaVersion: 6}, 52 | errors: [{message: `Expected 'else' construct to be melted with its 'for-of' followup.`}], 53 | }, { 54 | code: `if (test)\n test();\nelse\n switch (test) {\n // nothing\n }\n`, 55 | errors: [{message: `Expected 'else' construct to be melted with its 'switch' followup.`}], 56 | }, { 57 | code: `if (test)\n test();\nelse\n try {\n test();\n } catch (e) {\n test();\n }\n`, 58 | errors: [{message: `Expected 'else' construct to be melted with its 'try' followup.`}], 59 | }, { 60 | code: `if (test)\n test();\nelse\n while (test)\n test();\n`, 61 | errors: [{message: `Expected 'else' construct to be melted with its 'while' followup.`}], 62 | }, { 63 | code: `if (test)\n test();\nelse\n do {\n test();\n } while (test);\n`, 64 | errors: [{message: `Expected 'else' construct to be melted with its 'do' followup.`}], 65 | }, { 66 | code: `if (test)\n test();\nelse\n with (test) {\n test();\n }\n`, 67 | errors: [{message: `Expected 'else' construct to be melted with its 'with' followup.`}], 68 | }, { 69 | code: `if (test) {\n test();\n} else {\n if (test)\n test();\n}\n`, 70 | errors: [{message: `Expected 'else' construct to be melted with its 'if' followup.`}], 71 | }, { 72 | code: `if (test) {\n test();\n} else {\n for (;test;) {\n test();\n }\n}\n`, 73 | errors: [{message: `Expected 'else' construct to be melted with its 'for' followup.`}], 74 | }, { 75 | code: `if (test) {\n test();\n} else {\n for (var test in test)\n test();\n}\n`, 76 | errors: [{message: `Expected 'else' construct to be melted with its 'for-in' followup.`}], 77 | }, { 78 | code: `if (test) {\n test();\n} else {\n for (var test of test)\n test();\n}\n`, 79 | parserOptions, 80 | errors: [{message: `Expected 'else' construct to be melted with its 'for-of' followup.`}], 81 | }, { 82 | code: `if (test) {\n test();\n} else {\n switch (test) {\n // nothing\n }\n}\n`, 83 | errors: [{message: `Expected 'else' construct to be melted with its 'switch' followup.`}], 84 | }, { 85 | code: `if (test) {\n test();\n} else {\n try {\n test();\n } catch (e) {\n test();\n }\n}\n`, 86 | errors: [{message: `Expected 'else' construct to be melted with its 'try' followup.`}], 87 | }, { 88 | code: `if (test) {\n test();\n} else {\n while (test)\n test();\n}\n`, 89 | errors: [{message: `Expected 'else' construct to be melted with its 'while' followup.`}], 90 | }, { 91 | code: `if (test) {\n test();\n} else {\n do {\n test();\n } while (test);\n}\n`, 92 | errors: [{message: `Expected 'else' construct to be melted with its 'do' followup.`}], 93 | }, { 94 | code: `if (test) {\n test();\n} else {\n with (test) {\n test();\n }\n}\n`, 95 | errors: [{message: `Expected 'else' construct to be melted with its 'with' followup.`}], 96 | }], 97 | }); 98 | -------------------------------------------------------------------------------- /tests/rules/newline-after-import-section.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Require an empty newline after an import section 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import rule from 'eslint-plugin-arca/sources/rules/newline-after-import-section'; 9 | import {RuleTester} from 'eslint'; 10 | 11 | const parserOptions = {sourceType: `module`, ecmaVersion: 2015} as const; 12 | const ruleTester = new RuleTester(); 13 | 14 | ruleTester.run(`newline-after-import-section`, rule, { 15 | valid: [{ 16 | code: `import bar1 from 'bar';\nimport foo1 from 'foo';\n\nimport bar2 from 'common/bar';\nimport foo2 from 'common/foo';\n\nimport bar3 from 'app/bar';\nimport foo3 from 'app/foo';\n\nexport var foo;\n`, 17 | parserOptions, 18 | }, { 19 | code: `import bar from 'app/bar';\n// hello-world\nimport foo from 'app/foo';\n`, 20 | parserOptions, 21 | }], 22 | invalid: [{ 23 | code: `import foo1 from 'foo';\nimport foo2 from 'common/foo';\n`, 24 | output: `import foo1 from 'foo';\n\nimport foo2 from 'common/foo';\n`, 25 | parserOptions, 26 | errors: [{message: `Expected blank line after import section.`}], 27 | }, { 28 | code: `import foo1 from 'common/foo';\nimport foo2 from 'app/foo';\n`, 29 | output: `import foo1 from 'common/foo';\n\nimport foo2 from 'app/foo';\n`, 30 | parserOptions, 31 | errors: [{message: `Expected blank line after import section.`}], 32 | }, { 33 | code: `import foo1 from 'app/foo';\nexport var foo;\n`, 34 | output: `import foo1 from 'app/foo';\n\nexport var foo;\n`, 35 | parserOptions, 36 | errors: [{message: `Expected blank line after import section.`}], 37 | }, { 38 | code: `import foo from 'common/foo';\n\nimport bar from 'common/bar';\n`, 39 | output: `import foo from 'common/foo';\nimport bar from 'common/bar';\n`, 40 | parserOptions, 41 | errors: [{message: `Expected no blank lines between imports of a same section.`}], 42 | }, { 43 | code: `import foo from 'foo';\nimport {\n bar1,\n bar2,\n} from 'bar';\n`, 44 | output: `import foo from 'foo';\n\nimport {\n bar1,\n bar2,\n} from 'bar';\n`, 45 | parserOptions, 46 | errors: [{message: `Expected blank line after import section.`}], 47 | options: [{enableOnelinerSections: true}], 48 | }], 49 | }); 50 | -------------------------------------------------------------------------------- /tests/rules/no-default-export.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Disallow default exports 3 | * @author Maël Nison 4 | * @copyright 2016 Maël Nison. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | import rule from 'eslint-plugin-arca/sources/rules/no-default-export'; 9 | import {RuleTester} from 'eslint'; 10 | 11 | const parserOptions = {sourceType: `module`, ecmaVersion: 2015} as const; 12 | const ruleTester = new RuleTester(); 13 | 14 | ruleTester.run(`no-default-export`, rule, { 15 | valid: [{ 16 | code: `export function foo() {\n}\n`, 17 | parserOptions, 18 | }], 19 | invalid: [{ 20 | code: `export default function () {\n}\n`, 21 | parserOptions, 22 | errors: [{message: `Unexpected default export.`}], 23 | }], 24 | }); 25 | -------------------------------------------------------------------------------- /tests/workspace/lib.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcanis/eslint-plugin-arca/53cc858e924c163c9e676871ca602991ac57b95b/tests/workspace/lib.js -------------------------------------------------------------------------------- /tests/workspace/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-arca-test-workspace" 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "rootDir": "sources", 6 | "outDir": "lib", 7 | "types": [ 8 | "node" 9 | ] 10 | }, 11 | "include": [ 12 | "sources/**/*" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": ".", 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "jsx": "react", 9 | "lib": ["esnext", "esnext.asynciterable"], 10 | "module": "commonjs", 11 | "noImplicitReturns": true, 12 | "strict": true, 13 | "target": "ES2018", 14 | "declaration": true, 15 | "isolatedModules": true, 16 | "skipLibCheck": true 17 | } 18 | } 19 | --------------------------------------------------------------------------------