├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── babel.config.js ├── eslint-local-rules.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── __tests__ │ └── scope.ts ├── index.ts ├── rules │ ├── block.ts │ └── scope.ts └── utils │ ├── export.ts │ ├── scope.ts │ └── variables.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint", 17 | "local-rules" 18 | ], 19 | "rules": { 20 | "no-undef": ["off"], 21 | "local-rules/scope": ["error"] 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npm 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 14 15 | - run: npm ci 16 | - run: npm run build 17 | - run: npm test 18 | - uses: JS-DevTools/npm-publish@v1 19 | with: 20 | token: ${{ secrets.NPM_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 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 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/cache@v2 15 | with: 16 | path: ~/.npm 17 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 18 | restore-keys: | 19 | ${{ runner.os }}-node- 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: 14 24 | - run: npm ci 25 | - run: npm run build 26 | - run: npm run lint 27 | - run: npm test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.organizeImports": true 4 | } 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 uhyo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-var-length 2 | 3 | ESLint plugin to restrict short variable name. Currently has one rule: 4 | 5 | - **var-length/scope**: force variables that live in larger scope to have longer name. 6 | 7 | ## Installation 8 | 9 | ```sh 10 | npm i -D eslint-plugin-var-length 11 | ``` 12 | 13 | ```js 14 | // .eslintrc.js 15 | "plugins": [ 16 | // ... 17 | "var-length" 18 | ], 19 | ``` 20 | 21 | ## `var-length/scope` 22 | 23 | ```js 24 | // .eslintrc.js 25 | "rules": { 26 | // ... 27 | "var-length/scope": ["error"] 28 | } 29 | ``` 30 | 31 | This rule forces variables that live in larger scope to have longer name. 32 | 33 | Examples of **incorrect** code for this rule: 34 | 35 | ```js 36 | // Argument 'x' must have at least 2 characters 37 | function double(x) { 38 | const result = x * x; 39 | return result; 40 | } 41 | ``` 42 | 43 | Examples of **correct** code for this rule: 44 | 45 | ```js 46 | // Argument 'x' can be one character long because this function is short 47 | function double(x) { 48 | return x * x; 49 | } 50 | ``` 51 | 52 | ### Options 53 | 54 | This rule accepts an object with following properties (all optional): 55 | 56 | - `lengthCount`: `"length"`, `"codePoint"` or a function `(str: string) => number` (default: `"length"`). Specifies how to calculate the length of a variable name. 57 | - `"length"`: equivalent to `(str: string) => str.length`. 58 | - `"codePoint"`: equivalent to `(str: string) => [...str].length`. 59 | - `checkFunctionName`: boolean value (default: `false`). If set to `true`, function names in function declarations are also checked. 60 | - `checkExportedName`: boolean value (default: `false`). If set to `true`, exported variable names are also checked. 61 | - `limit`: Object or function to specify how to calculate the minimum variable length for given scope (see below; defaults to `{ factor: 0.75 }`). 62 | 63 | #### The `limit` option 64 | 65 | By default, the minumum variable length is quadratic to the number of lines in the surrounding scope: 66 | 67 | ```js 68 | minimumLength = Math.ceil(factor * Math.sqrt(linesCount)); 69 | ``` 70 | 71 | The `factor` defaults to `0.75`. By passing an object of the form `{ factor: num }` as the `limit` option, you can override the factor. A smaller factor is more permissive. 72 | 73 | If you want more control on how to calculate the minimum length, pass a function as `limit`. 74 | 75 | ## Contributing 76 | 77 | Welcome 78 | 79 | ## License 80 | 81 | MIT 82 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | ], 6 | }; -------------------------------------------------------------------------------- /eslint-local-rules.js: -------------------------------------------------------------------------------- 1 | // for eslint-plugin-local-rules 2 | try { 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | module.exports = require('./').rules 5 | // eslint-disable-next-line no-empty 6 | } catch (e) { 7 | console.warn(e) 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/gn/tglh_1gn0w501pf81ml6skw40000gp/T/jest_dy", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | // clearMocks: false, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | // coverageDirectory: undefined, 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | // coverageProvider: "babel", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "json", 77 | // "jsx", 78 | // "ts", 79 | // "tsx", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | // preset: undefined, 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state between every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state between every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | // rootDir: undefined, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | // setupFiles: [], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | // setupFilesAfterEnv: [], 132 | 133 | // The number of seconds after which a test is considered as slow and reported as such in the results. 134 | // slowTestThreshold: 5, 135 | 136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 137 | // snapshotSerializers: [], 138 | 139 | // The test environment that will be used for testing 140 | testEnvironment: "node", 141 | 142 | // Options that will be passed to the testEnvironment 143 | // testEnvironmentOptions: {}, 144 | 145 | // Adds a location field to test results 146 | // testLocationInResults: false, 147 | 148 | // The glob patterns Jest uses to detect test files 149 | // testMatch: [ 150 | // "**/__tests__/**/*.[jt]s?(x)", 151 | // "**/?(*.)+(spec|test).[tj]s?(x)" 152 | // ], 153 | 154 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 155 | // testPathIgnorePatterns: [ 156 | // "/node_modules/" 157 | // ], 158 | 159 | // The regexp pattern or array of patterns that Jest uses to detect test files 160 | // testRegex: [], 161 | 162 | // This option allows the use of a custom results processor 163 | // testResultsProcessor: undefined, 164 | 165 | // This option allows use of a custom test runner 166 | // testRunner: "jasmine2", 167 | 168 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 169 | // testURL: "http://localhost", 170 | 171 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 172 | // timers: "real", 173 | 174 | // A map from regular expressions to paths to transformers 175 | // transform: undefined, 176 | 177 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 178 | // transformIgnorePatterns: [ 179 | // "/node_modules/", 180 | // "\\.pnp\\.[^\\/]+$" 181 | // ], 182 | 183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 184 | // unmockedModulePathPatterns: undefined, 185 | 186 | // Indicates whether each individual test should be reported during the run 187 | // verbose: undefined, 188 | 189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 190 | // watchPathIgnorePatterns: [], 191 | 192 | // Whether to use watchman for file crawling 193 | // watchman: true, 194 | }; 195 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-var-length", 3 | "version": "1.0.0", 4 | "description": "ESLint rules for linting variable length", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "scripts": { 10 | "build": "tsc", 11 | "lint": "eslint src/**/*.ts", 12 | "test": "jest" 13 | }, 14 | "keywords": [ 15 | "eslint", 16 | "eslintplugin" 17 | ], 18 | "author": "uhyo ", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/uhyo/eslint-plugin-var-length" 22 | }, 23 | "license": "MIT", 24 | "dependencies": { 25 | "@typescript-eslint/experimental-utils": "^4.11.1", 26 | "@typescript-eslint/typescript-estree": "^4.11.1", 27 | "memoize-one": "^5.1.1" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.12.10", 31 | "@babel/preset-env": "^7.12.11", 32 | "@babel/preset-typescript": "^7.12.7", 33 | "@types/jest": "^26.0.19", 34 | "@typescript-eslint/eslint-plugin": "^4.11.1", 35 | "@typescript-eslint/parser": "^4.11.1", 36 | "babel-jest": "^26.6.3", 37 | "eslint": "^7.17.0", 38 | "eslint-plugin-local-rules": "^1.0.1", 39 | "espree": "^7.3.1", 40 | "jest": "^26.6.3", 41 | "prettier": "^2.2.1", 42 | "typescript": "^4.1.3" 43 | } 44 | } -------------------------------------------------------------------------------- /src/__tests__/scope.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable local-rules/scope */ 2 | import { TSESLint } from "@typescript-eslint/experimental-utils"; 3 | import varLengthScopeRule from "../rules/scope"; 4 | 5 | const ruleName = "scope"; 6 | const tester = new TSESLint.RuleTester({ 7 | parser: require.resolve("espree"), 8 | parserOptions: { ecmaVersion: 2020, sourceType: "module" }, 9 | }); 10 | 11 | describe("FunctionDeclaration", () => { 12 | tester.run(ruleName, varLengthScopeRule, { 13 | valid: [ 14 | { 15 | code: `function func(a) { 16 | return a; 17 | }`, 18 | }, 19 | { 20 | code: `function func(aa) { 21 | const bbb = aa + aa; 22 | return bbb; 23 | }`, 24 | }, 25 | { 26 | options: [ 27 | { 28 | checkFunctionName: true, 29 | }, 30 | ], 31 | code: `function fu() { 32 | }`, 33 | }, 34 | ], 35 | invalid: [ 36 | { 37 | code: `function func(a) { 38 | const a2 = a + a; 39 | return a2; 40 | }`, 41 | errors: [ 42 | { 43 | messageId: "min", 44 | data: { 45 | length: 2, 46 | }, 47 | }, 48 | ], 49 | }, 50 | { 51 | options: [ 52 | { 53 | checkFunctionName: true, 54 | }, 55 | ], 56 | code: `function f() { 57 | } 58 | export function g() { 59 | }`, 60 | errors: [ 61 | { 62 | messageId: "min", 63 | data: { 64 | length: 2, 65 | }, 66 | }, 67 | ], 68 | }, 69 | { 70 | options: [ 71 | { 72 | checkFunctionName: true, 73 | checkExportedName: true, 74 | }, 75 | ], 76 | code: `function f() { 77 | } 78 | export function g() { 79 | }`, 80 | errors: [ 81 | { 82 | messageId: "min", 83 | data: { 84 | length: 2, 85 | }, 86 | }, 87 | { 88 | messageId: "min", 89 | data: { 90 | length: 2, 91 | }, 92 | }, 93 | ], 94 | }, 95 | ], 96 | }); 97 | }); 98 | 99 | describe("FunctionExpression", () => { 100 | tester.run(ruleName, varLengthScopeRule, { 101 | valid: [ 102 | { 103 | code: `(function(a) { 104 | return a; 105 | })(123);`, 106 | }, 107 | { 108 | code: `+function func(aa) { 109 | const bbb = aa + aa; 110 | return bbb; 111 | }(123);`, 112 | }, 113 | ], 114 | invalid: [ 115 | { 116 | code: `!function func(a) { 117 | const a22 = a + a; 118 | 119 | 120 | 121 | return a22; 122 | }()`, 123 | errors: [ 124 | { 125 | messageId: "min", 126 | data: { 127 | length: 3, 128 | }, 129 | }, 130 | ], 131 | }, 132 | ], 133 | }); 134 | }); 135 | 136 | describe("ArrowFunctionExpression", () => { 137 | tester.run(ruleName, varLengthScopeRule, { 138 | valid: [ 139 | { 140 | code: `((a)=> { 141 | return a; 142 | })(123);`, 143 | }, 144 | { 145 | code: `(async (aa)=>{ 146 | const bbb = aa + aa; 147 | return bbb; 148 | })(123);`, 149 | }, 150 | ], 151 | invalid: [ 152 | { 153 | code: `((a)=> { 154 | const a2 = a + a; 155 | return a2; 156 | })()`, 157 | errors: [ 158 | { 159 | messageId: "min", 160 | data: { 161 | length: 2, 162 | }, 163 | }, 164 | ], 165 | }, 166 | ], 167 | }); 168 | }); 169 | 170 | describe("VariableDeclaration", () => { 171 | tester.run(ruleName, varLengthScopeRule, { 172 | valid: [ 173 | { 174 | code: `function ffffffff() { 175 | var a; 176 | }`, 177 | }, 178 | { 179 | code: `function ffffffff(obj) { 180 | const { aa, bb, cc } = obj; 181 | return aa + bb + cc; 182 | }`, 183 | }, 184 | { 185 | code: `function ffffffff(obj) { 186 | const { a: aa, b: bb, c: cc } = obj; 187 | return aa + bb + cc; 188 | }`, 189 | }, 190 | { 191 | code: `let a = 123;`, 192 | }, 193 | { 194 | code: `export let a = 0; 195 | a += 12345;`, 196 | }, 197 | ], 198 | invalid: [ 199 | { 200 | code: `function ffffffff() { 201 | var a = 1; 202 | return a * 10; 203 | }`, 204 | errors: [ 205 | { 206 | messageId: "min", 207 | data: { 208 | length: 2, 209 | }, 210 | }, 211 | ], 212 | }, 213 | { 214 | code: `function ffffffff(obj) { 215 | const { x, y, z } = obj; 216 | return x + y + z; 217 | }`, 218 | errors: [ 219 | { 220 | messageId: "min", 221 | data: { 222 | length: 2, 223 | }, 224 | }, 225 | { 226 | messageId: "min", 227 | data: { 228 | length: 2, 229 | }, 230 | }, 231 | { 232 | messageId: "min", 233 | data: { 234 | length: 2, 235 | }, 236 | }, 237 | ], 238 | }, 239 | { 240 | code: `let a = 123, 241 | // 242 | // 243 | // 244 | abc = 4;`, 245 | errors: [ 246 | { 247 | messageId: "min", 248 | data: { 249 | length: 3, 250 | }, 251 | }, 252 | ], 253 | }, 254 | { 255 | code: `for (const i of arr) { 256 | console.log(i); 257 | i; 258 | }`, 259 | errors: [ 260 | { 261 | messageId: "min", 262 | data: { 263 | length: 2, 264 | }, 265 | }, 266 | ], 267 | }, 268 | { 269 | code: `export let a = 0; 270 | a += 12345;`, 271 | options: [ 272 | { 273 | checkExportedName: true, 274 | }, 275 | ], 276 | errors: [ 277 | { 278 | messageId: "min", 279 | data: { 280 | length: 2, 281 | }, 282 | }, 283 | ], 284 | }, 285 | ], 286 | }); 287 | }); 288 | 289 | describe("CatchClause", () => { 290 | tester.run(ruleName, varLengthScopeRule, { 291 | valid: [ 292 | { 293 | code: `try {} 294 | catch ({ e }) { 295 | console.error(e); 296 | }`, 297 | }, 298 | ], 299 | invalid: [ 300 | { 301 | code: `try {} 302 | catch ({ e, ee }) { 303 | console.error(e); 304 | console.error(ee); 305 | }`, 306 | errors: [ 307 | { 308 | messageId: "min", 309 | data: { 310 | length: 2, 311 | }, 312 | }, 313 | ], 314 | }, 315 | ], 316 | }); 317 | }); 318 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import scope from "./rules/scope"; 2 | 3 | export = { 4 | rules: { 5 | scope: scope, 6 | }, 7 | configs: { 8 | all: { 9 | plugins: ["var-length"], 10 | rules: { 11 | "var-length/scope": "error", 12 | }, 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/rules/block.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AST_NODE_TYPES, 3 | TSESTree, 4 | } from "@typescript-eslint/experimental-utils"; 5 | 6 | export interface ScopeLocation { 7 | firstElement: TSESTree.Node; 8 | lastElement: TSESTree.Node; 9 | startLine: number; 10 | endLine: number; 11 | lineCount: number; 12 | } 13 | 14 | type Block = 15 | | TSESTree.BlockStatement 16 | | TSESTree.FunctionDeclaration 17 | | TSESTree.ArrowFunctionExpression 18 | | TSESTree.CatchClause 19 | | TSESTree.ClassDeclaration 20 | | TSESTree.ClassExpression 21 | | TSESTree.ForInStatement 22 | | TSESTree.ForOfStatement 23 | | TSESTree.ForStatement 24 | | TSESTree.FunctionExpression 25 | | TSESTree.Program 26 | | TSESTree.SwitchStatement 27 | | TSESTree.WithStatement 28 | | TSESTree.TSConditionalType 29 | | TSESTree.TSCallSignatureDeclaration 30 | | TSESTree.TSConstructorType 31 | | TSESTree.TSConstructSignatureDeclaration 32 | | TSESTree.TSDeclareFunction 33 | | TSESTree.TSEmptyBodyFunctionExpression 34 | | TSESTree.TSEnumDeclaration 35 | | TSESTree.TSFunctionType 36 | | TSESTree.TSInterfaceDeclaration 37 | | TSESTree.TSMappedType 38 | | TSESTree.TSMethodSignatureComputedName 39 | | TSESTree.TSMethodSignatureNonComputedName 40 | | TSESTree.TSModuleDeclaration 41 | | TSESTree.TSTypeAliasDeclaration; 42 | 43 | export function getScopeLocation(block: Block): ScopeLocation { 44 | const [firstElement, lastElement] = getEdgeElements(block); 45 | const startLine = firstElement.loc.start.line; 46 | const endLine = lastElement.loc.end.line; 47 | const lineCount = endLine - startLine + 1; 48 | return { 49 | firstElement, 50 | lastElement, 51 | startLine, 52 | endLine, 53 | lineCount, 54 | }; 55 | } 56 | 57 | function getEdgeElements(block: Block): [TSESTree.Node, TSESTree.Node] { 58 | switch (block.type) { 59 | case AST_NODE_TYPES.BlockStatement: { 60 | return edges(block.body, block); 61 | } 62 | case AST_NODE_TYPES.FunctionDeclaration: 63 | case AST_NODE_TYPES.FunctionExpression: { 64 | return edges(block.body.body, block.body); 65 | } 66 | case AST_NODE_TYPES.ArrowFunctionExpression: { 67 | return getEdgeOfStatement(block.body); 68 | } 69 | case AST_NODE_TYPES.CatchClause: { 70 | return getEdgeElements(block.body); 71 | } 72 | case AST_NODE_TYPES.ClassDeclaration: 73 | case AST_NODE_TYPES.ClassExpression: { 74 | return edges(block.body.body, block.body); 75 | } 76 | case AST_NODE_TYPES.ForInStatement: 77 | case AST_NODE_TYPES.ForOfStatement: 78 | case AST_NODE_TYPES.ForStatement: { 79 | return getEdgeOfStatement(block.body); 80 | } 81 | case AST_NODE_TYPES.Program: { 82 | return edges(block.body, block); 83 | } 84 | case AST_NODE_TYPES.SwitchStatement: { 85 | return edges(block.cases, block); 86 | } 87 | case AST_NODE_TYPES.WithStatement: { 88 | return getEdgeOfStatement(block.body); 89 | } 90 | default: { 91 | return [block, block]; 92 | } 93 | } 94 | } 95 | 96 | function getEdgeOfStatement( 97 | statement: TSESTree.Node 98 | ): [TSESTree.Node, TSESTree.Node] { 99 | if (statement.type === AST_NODE_TYPES.BlockStatement) { 100 | return [ 101 | first(statement.body) || statement, 102 | last(statement.body) || statement, 103 | ]; 104 | } 105 | return [statement, statement]; 106 | } 107 | 108 | function edges(arr: readonly T[], defaultValue: U): [T | U, T | U] { 109 | return [first(arr) || defaultValue, last(arr) || defaultValue]; 110 | } 111 | 112 | function first(arr: readonly T[]): T | undefined { 113 | return arr[0]; 114 | } 115 | 116 | function last(arr: readonly T[]): T | undefined { 117 | return arr[arr.length - 1]; 118 | } 119 | -------------------------------------------------------------------------------- /src/rules/scope.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AST_NODE_TYPES, 3 | TSESLint, 4 | TSESTree, 5 | } from "@typescript-eslint/experimental-utils"; 6 | import memoizeOne from "memoize-one"; 7 | import { isExportDeclartation } from "../utils/export"; 8 | import { getVarScope } from "../utils/scope"; 9 | import { 10 | getFunctionParameterVariables, 11 | getPatternVariables, 12 | getVariableDeclarationVariables, 13 | } from "../utils/variables"; 14 | import { getScopeLocation, ScopeLocation } from "./block"; 15 | 16 | type MessageId = "min" | "max"; 17 | 18 | type RuleOptions = { 19 | lengthCount: "length" | "codePoint" | ((id: string) => number); 20 | checkFunctionName: boolean; 21 | checkExportedName: boolean; 22 | limit: LimitOption; 23 | }; 24 | 25 | type LimitOption = 26 | | { 27 | mode?: "quadratic"; 28 | factor?: number; 29 | } 30 | | ((scopeLocation: ScopeLocation) => number | { min?: number; max?: number }); 31 | 32 | type LimitFunction = ( 33 | scopeLocation: ScopeLocation 34 | ) => { min: number; max: number }; 35 | 36 | const varLengthScopeRule: Omit< 37 | TSESLint.RuleModule?]>, 38 | "docs" 39 | > = { 40 | meta: { 41 | type: "problem", 42 | docs: { 43 | category: "Possible Errors", 44 | description: "Force variables that live longer to have longer name.", 45 | recommended: "error", 46 | url: "TODO", 47 | }, 48 | messages: { 49 | min: "This Variable must have at least {{ length }} characters.", 50 | max: "This Variable must have at most {{ length }} characters.", 51 | }, 52 | schema: [ 53 | { 54 | type: "object", 55 | properties: { 56 | checkFunctionName: { 57 | type: "boolean", 58 | }, 59 | checkExportedName: { 60 | type: "boolean", 61 | }, 62 | }, 63 | propertyNames: { 64 | enum: [ 65 | "lengthCount", 66 | "checkFunctionName", 67 | "checkExportedName", 68 | "limit", 69 | ], 70 | }, 71 | }, 72 | ], 73 | }, 74 | create: function (context) { 75 | const options = context.options[0] || {}; 76 | const lengthCountFunction = 77 | options.lengthCount === "codePoint" 78 | ? (str: string) => [...str].length 79 | : typeof options.lengthCount === "function" 80 | ? options.lengthCount 81 | : (str: string) => str.length; 82 | const limitFunction = getLimitFunction(options.limit); 83 | 84 | const checkFunctionName = options.checkFunctionName ?? false; 85 | const checkExportedName = options.checkExportedName ?? false; 86 | 87 | return { 88 | FunctionDeclaration: checkFunctionLike, 89 | FunctionExpression: checkFunctionLike, 90 | ArrowFunctionExpression: checkFunctionLike, 91 | VariableDeclaration: (node) => { 92 | if (!checkExportedName && isExportDeclartation(node.parent)) { 93 | return; 94 | } 95 | const vars = getVariableDeclarationVariables(node); 96 | const scope = context.getScope(); 97 | const effectiveScope = node.kind === "var" ? getVarScope(scope) : scope; 98 | for (const checkedVar of vars) { 99 | checkForScope( 100 | context, 101 | lengthCountFunction, 102 | limitFunction, 103 | checkedVar, 104 | effectiveScope 105 | ); 106 | } 107 | }, 108 | CatchClause: (node) => { 109 | const vars = getPatternVariables(node.param); 110 | const scope = context.getScope(); 111 | for (const v of vars) { 112 | checkForScope(context, lengthCountFunction, limitFunction, v, scope); 113 | } 114 | }, 115 | }; 116 | function checkFunctionLike( 117 | node: 118 | | TSESTree.FunctionDeclaration 119 | | TSESTree.FunctionExpression 120 | | TSESTree.ArrowFunctionExpression 121 | ) { 122 | const vars = getFunctionParameterVariables(node); 123 | const scope = context.getScope(); 124 | // check function name 125 | if (node.type === AST_NODE_TYPES.FunctionDeclaration) { 126 | if ( 127 | checkFunctionName && 128 | (checkExportedName || !isExportDeclartation(node.parent)) 129 | ) { 130 | const funcNameScope = scope.upper; 131 | if (funcNameScope && node.id) { 132 | checkForScope( 133 | context, 134 | lengthCountFunction, 135 | limitFunction, 136 | node.id, 137 | funcNameScope 138 | ); 139 | } 140 | } 141 | } 142 | for (const v of vars) { 143 | checkForScope(context, lengthCountFunction, limitFunction, v, scope); 144 | } 145 | } 146 | }, 147 | }; 148 | 149 | export default varLengthScopeRule; 150 | 151 | function getLimitFunction(limit: LimitOption = {}): LimitFunction { 152 | if (typeof limit === "function") { 153 | return (scopeLocation: ScopeLocation) => { 154 | const result = limit(scopeLocation); 155 | return typeof result === "number" 156 | ? { 157 | min: result, 158 | max: Infinity, 159 | } 160 | : { 161 | min: result.min ?? 0, 162 | max: result.max ?? Infinity, 163 | }; 164 | }; 165 | } else if (!limit.mode || limit.mode === "quadratic") { 166 | // quadratic 167 | const factor = limit.factor || 0.75; 168 | return (scopeLocation) => { 169 | return { 170 | min: Math.ceil(factor * Math.ceil(Math.sqrt(scopeLocation.lineCount))), 171 | max: Infinity, 172 | }; 173 | }; 174 | } else { 175 | throw new Error("'limit' option has unknown mode"); 176 | } 177 | } 178 | 179 | const getScopeLocationMemo = memoizeOne(getScopeLocation); 180 | 181 | function checkForScope( 182 | context: Readonly>, 183 | lengthCountFunction: (str: string) => number, 184 | limitFunction: LimitFunction, 185 | identifier: TSESTree.Identifier, 186 | scope: TSESLint.Scope.Scope 187 | ) { 188 | const scopeLoc = getScopeLocationMemo(scope.block); 189 | const idLength = lengthCountFunction(identifier.name); 190 | const limit = limitFunction(scopeLoc); 191 | // exclude type annotation from error location 192 | const reportLoc: 193 | | TSESTree.SourceLocation 194 | | undefined = identifier.typeAnnotation 195 | ? { 196 | start: identifier.loc.start, 197 | end: { 198 | line: identifier.loc.start.line, 199 | column: identifier.loc.start.column + identifier.name.length, 200 | }, 201 | } 202 | : undefined; 203 | if (idLength < limit.min) { 204 | context.report({ 205 | node: identifier, 206 | loc: reportLoc, 207 | messageId: "min", 208 | data: { 209 | length: limit.min, 210 | }, 211 | }); 212 | } else if (idLength > limit.max) { 213 | context.report({ 214 | node: identifier, 215 | loc: reportLoc, 216 | messageId: "max", 217 | data: { 218 | length: limit.max, 219 | }, 220 | }); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/utils/export.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AST_NODE_TYPES, 3 | TSESTree, 4 | } from "@typescript-eslint/experimental-utils"; 5 | 6 | export function isExportDeclartation( 7 | node: TSESTree.Node | undefined 8 | ): node is TSESTree.ExportNamedDeclaration | TSESTree.ExportDefaultDeclaration { 9 | return ( 10 | node?.type === AST_NODE_TYPES.ExportDefaultDeclaration || 11 | node?.type === AST_NODE_TYPES.ExportNamedDeclaration 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/scope.ts: -------------------------------------------------------------------------------- 1 | import { TSESLint } from "@typescript-eslint/experimental-utils"; 2 | 3 | /** 4 | * Traverse up to non-block scope. 5 | */ 6 | export function getVarScope(scope: TSESLint.Scope.Scope): TSESLint.Scope.Scope { 7 | while ( 8 | scope.type === TSESLint.Scope.ScopeType.block || 9 | scope.type === TSESLint.Scope.ScopeType.switch || 10 | scope.type === TSESLint.Scope.ScopeType.for 11 | ) { 12 | scope = scope.upper; 13 | } 14 | return scope; 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/variables.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AST_NODE_TYPES, 3 | TSESTree, 4 | } from "@typescript-eslint/experimental-utils"; 5 | 6 | export function getFunctionParameterVariables( 7 | decl: 8 | | TSESTree.FunctionDeclaration 9 | | TSESTree.FunctionExpression 10 | | TSESTree.ArrowFunctionExpression 11 | ): TSESTree.Identifier[] { 12 | return decl.params.flatMap(getPatternVariables); 13 | } 14 | 15 | export function getVariableDeclarationVariables( 16 | decl: TSESTree.VariableDeclaration 17 | ): TSESTree.Identifier[] { 18 | return decl.declarations.flatMap((declarator) => { 19 | return getPatternVariables(declarator.id); 20 | }); 21 | } 22 | 23 | export function getPatternVariables( 24 | pattern: TSESTree.Node | null 25 | ): TSESTree.Identifier[] { 26 | if (pattern === null) { 27 | return []; 28 | } 29 | switch (pattern.type) { 30 | case AST_NODE_TYPES.ArrayPattern: { 31 | return pattern.elements.flatMap(getPatternVariables); 32 | } 33 | case AST_NODE_TYPES.ObjectPattern: { 34 | return pattern.properties.flatMap(getPatternVariables); 35 | } 36 | case AST_NODE_TYPES.Identifier: { 37 | return [pattern]; 38 | } 39 | case AST_NODE_TYPES.AssignmentPattern: { 40 | return getPatternVariables(pattern.left); 41 | } 42 | case AST_NODE_TYPES.Property: { 43 | return getPatternVariables(pattern.value); 44 | } 45 | case AST_NODE_TYPES.RestElement: { 46 | return getPatternVariables(pattern.argument); 47 | } 48 | case AST_NODE_TYPES.TSParameterProperty: { 49 | return getPatternVariables(pattern.parameter); 50 | } 51 | default: { 52 | return []; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | 44 | /* Module Resolution Options */ 45 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 | }, 70 | "include": ["./src/**/*.ts"] 71 | } 72 | --------------------------------------------------------------------------------