├── .eslintrc.json ├── .github └── workflows │ └── ci-cd.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .releaserc.yml ├── README.md ├── example ├── index.ts └── legacy │ └── legacy-file.js ├── jest.config.js ├── package.json ├── src ├── cli.ts ├── display │ ├── analyseSpinner.ts │ └── print.ts ├── eslintDisabledRules │ ├── eslintParser.spec.ts │ ├── eslintParser.ts │ └── types.ts ├── getDataFromFiles.ts ├── getFiles.ts ├── index.ts ├── integration.spec.ts ├── statistics │ ├── computeStats.ts │ ├── printStats.ts │ └── types.ts ├── tokens.ts ├── types.ts └── unicode.ts ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:prettier/recommended", 5 | "prettier/@typescript-eslint" 6 | ], 7 | "rules": { 8 | "curly": ["error", "all"], 9 | "eqeqeq": ["error", "smart"], 10 | "complexity": ["error", 8], 11 | "import/no-extraneous-dependencies": [ 12 | "error", 13 | { 14 | "devDependencies": true, 15 | "optionalDependencies": false, 16 | "peerDependencies": false 17 | } 18 | ], 19 | "no-shadow": [ 20 | "error", 21 | { 22 | "hoist": "all" 23 | } 24 | ], 25 | "prefer-const": "error", 26 | "import/order": [ 27 | "error", 28 | { 29 | "groups": [ 30 | ["external", "builtin"], 31 | "internal", 32 | ["parent", "sibling", "index"] 33 | ] 34 | } 35 | ], 36 | "sort-imports": [ 37 | "error", 38 | { 39 | "ignoreCase": true, 40 | "ignoreDeclarationSort": true, 41 | "ignoreMemberSort": false, 42 | "memberSyntaxSortOrder": ["none", "all", "multiple", "single"] 43 | } 44 | ] 45 | }, 46 | "root": true, 47 | "plugins": ["import"], 48 | "env": { 49 | "browser": true, 50 | "es6": true, 51 | "node": true 52 | }, 53 | "overrides": [ 54 | { 55 | "files": ["**/*.ts?(x)"], 56 | "extends": [ 57 | "plugin:@typescript-eslint/recommended", 58 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 59 | ], 60 | "parser": "@typescript-eslint/parser", 61 | "parserOptions": { 62 | "project": "tsconfig.json" 63 | }, 64 | "rules": { 65 | "@typescript-eslint/prefer-optional-chain": "error", 66 | "@typescript-eslint/prefer-nullish-coalescing": "error", 67 | "@typescript-eslint/strict-boolean-expressions": "error", 68 | "no-shadow": "off", 69 | "@typescript-eslint/no-shadow": "error" 70 | } 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /.github/workflows/ci-cd.yml: -------------------------------------------------------------------------------- 1 | name: CI-CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | 8 | jobs: 9 | build: 10 | if: "!contains(github.event.head_commit.message, 'skip ci')" 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [12] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Get yarn cache directory path 20 | id: yarn-cache-dir-path 21 | run: echo "::set-output name=dir::$(yarn cache dir)" 22 | 23 | - name: Bind yarn cach with github cache 24 | uses: actions/cache@v1 25 | with: 26 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 27 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 28 | restore-keys: | 29 | ${{ runner.os }}-yarn- 30 | 31 | - run: yarn install --frozen-lockfile 32 | 33 | - run: yarn prettier 34 | 35 | - run: yarn lint 36 | 37 | - run: yarn test 38 | 39 | create-release: 40 | if: "!contains(github.event.head_commit.message, 'skip ci') && github.ref == 'refs/heads/master'" 41 | runs-on: ubuntu-latest 42 | strategy: 43 | matrix: 44 | node-version: [12] 45 | needs: [build] 46 | 47 | steps: 48 | - uses: actions/checkout@v1 49 | 50 | - name: Use Node.js ${{ matrix.node-version }} 51 | uses: actions/setup-node@v1 52 | with: 53 | node-version: ${{ matrix.node-version }} 54 | 55 | - name: Get yarn cache directory path 56 | id: yarn-cache-dir-path 57 | run: echo "::set-output name=dir::$(yarn cache dir)" 58 | 59 | - name: Bind yarn cach with github cache 60 | uses: actions/cache@v1 61 | with: 62 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 63 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 64 | restore-keys: | 65 | ${{ runner.os }}-yarn- 66 | 67 | - name: Install Dependencies 68 | run: yarn install --frozen-lockfile 69 | 70 | - name: Semantic Release 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 74 | run: yarn semantic-release 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | src 3 | yarn.lock 4 | tsconfig.json 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.releaserc.yml: -------------------------------------------------------------------------------- 1 | { 2 | release: { branches: ['master'] }, 3 | plugins: 4 | [ 5 | '@semantic-release/commit-analyzer', 6 | '@semantic-release/release-notes-generator', 7 | '@semantic-release/github', 8 | ['@semantic-release/npm', { 'npmPublish': true }], 9 | ['@semantic-release/git', { 'assets': ['package.json'] }], 10 | ], 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-disabled-stats 2 | 3 | Compute statistics about the eslint rules disabled 4 | 5 | [![npm version](https://badge.fury.io/js/eslint-disabled-stats.svg)](https://badge.fury.io/js/eslint-disabled-stats) 6 | [![CI-CD](https://github.com/CorentinDoue/eslint-disabled-stats/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/CorentinDoue/eslint-disabled-stats/actions/workflows/ci-cd.yml) 7 | 8 | It could be useful to track the correction of legacy eslint errors commented with a tool such as https://github.com/CorentinDoue/eslint-disable-inserter on your codebase. 9 | 10 | The number of analyzed files and number of analyzed lines could be useful to track the evolution of the eslint errors 11 | compared to the evolution of the codebase. 12 | 13 | ## Usage 14 | 15 | ``` 16 | $ npx eslint-disabled-stats -g -p "example/**/*.(js|ts)" 17 | 18 | ℹ Analysing 2 files... 19 | ✔ Statistics computed 20 | 21 | Rules disabled by rule: 22 | • prefer-const: 1 23 | • eqeqeq: 1 24 | • curly: 1 25 | • ALL_RULES: 1 26 | 27 | Rules disabled by file: 28 | • example/index.ts: 3 29 | • example/legacy/legacy-file.js: 1 30 | 31 | Total rules disabled: 4 32 | 33 | Analysed files: 2 34 | Analysed lines: 19 35 | 36 | ✔ Done 37 | ``` 38 | 39 | ### Options 40 | 41 | - The `--pattern` / `-p` flag allows specifying 42 | the glob pattern of files on which the statistics are computed. 43 | The default pattern is `**/*.(js|ts|jsx|tsx)` 44 | 45 | - The `--quiet` / `-q` flag makes the console output lighter. 46 | The details of the errors by rules and by files will be omitted. 47 | 48 | ## License 49 | 50 | MIT 51 | -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { legacyFunction } from './legacy/legacy-file'; 3 | 4 | const main: Function = () => { 5 | // FIXME 6 | // eslint-disable-next-line prefer-const 7 | let legacyOutput = legacyFunction(); 8 | // eslint-disable-next-line eqeqeq, curly 9 | if (legacyOutput == 0) console.log('the legacy function is operational'); 10 | }; 11 | -------------------------------------------------------------------------------- /example/legacy/legacy-file.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export function legacyFunction() { 3 | let legacyFunction = function (arg) { 4 | return 0; 5 | }; 6 | return legacyFunction(); 7 | } 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['src'], 3 | transform: { 4 | '.ts$': 'ts-jest', 5 | }, 6 | testMatch: ['**/__tests__/**/*.ts', '**/*.spec.ts'], 7 | moduleFileExtensions: ['js', 'json', 'ts'], 8 | testURL: 'http://localhost', 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-disabled-stats", 3 | "version": "1.2.0", 4 | "main": "dist/index.js", 5 | "license": "MIT", 6 | "homepage": "https://github.com/CorentinDoue/eslint-disabled-stats", 7 | "author": "Corentin Doué", 8 | "scripts": { 9 | "start": "tsc --watch", 10 | "build": "rimraf dist && tsc", 11 | "prepare": "yarn build", 12 | "test": "jest", 13 | "test:integration": "jest --config jest.integration.config.js --runInBand", 14 | "prettier": "prettier . -c", 15 | "prettier:fix": "prettier . --write", 16 | "lint": "eslint --quiet 'src/**/*.ts'", 17 | "lint:fix": "eslint --quiet --fix 'src/**/*.ts'" 18 | }, 19 | "bin": { 20 | "eslint-disabled-stats": "dist/cli.js" 21 | }, 22 | "dependencies": { 23 | "chalk": "^4.1.0", 24 | "esprima": "^4.0.1", 25 | "fast-glob": "^3.2.4", 26 | "figlet": "^1.5.0", 27 | "inquirer": "^7.3.3", 28 | "lodash": "^4.17.20", 29 | "meow": "^7.1.1", 30 | "ora": "^5.1.0" 31 | }, 32 | "devDependencies": { 33 | "@semantic-release/git": "^9.0.0", 34 | "@types/chalk": "^2.2.0", 35 | "@types/esprima": "^4.0.2", 36 | "@types/figlet": "^1.2.0", 37 | "@types/jest": "^26.0.14", 38 | "@types/lodash": "^4.14.164", 39 | "@types/meow": "^5.0.0", 40 | "@types/node": "^14.11.1", 41 | "@typescript-eslint/eslint-plugin": "^4.6.0", 42 | "@typescript-eslint/parser": "^4.6.0", 43 | "eslint": "^7.12.1", 44 | "eslint-config-prettier": "^6.15.0", 45 | "eslint-plugin-import": "^2.22.1", 46 | "eslint-plugin-prettier": "^3.1.4", 47 | "jest": "^26.4.2", 48 | "prettier": "^2.1.2", 49 | "rimraf": "^3.0.2", 50 | "semantic-release": "^17.2.1", 51 | "ts-jest": "^26.3.0", 52 | "typescript": "^4.0.3" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import meow from 'meow'; 3 | import chalk from 'chalk'; 4 | import { computeEslintDisabledStats } from './index'; 5 | import { printError } from './display/print'; 6 | import { Options } from './types'; 7 | 8 | const defaultPattern = '**/*.(js|ts|jsx|tsx)'; 9 | const cli = meow( 10 | ` 11 | Usage 12 | $ eslint-disabled-stats 13 | 14 | Options 15 | --pattern, -p ${chalk.gray( 16 | `Glob pattern of matching files ( default: "${defaultPattern}" )`, 17 | )} 18 | --quiet, -q ${chalk.gray( 19 | `Do not display stats by rule and by file ( default: "false" )`, 20 | )} 21 | `, 22 | { 23 | flags: { 24 | pattern: { 25 | type: 'string', 26 | alias: 'p', 27 | }, 28 | quiet: { 29 | type: 'boolean', 30 | alias: 'q', 31 | }, 32 | }, 33 | }, 34 | ); 35 | 36 | const options: Options = { 37 | pattern: cli.flags.pattern ?? defaultPattern, 38 | quiet: Boolean(cli.flags.quiet), 39 | }; 40 | 41 | computeEslintDisabledStats(options).catch((error) => printError(error)); 42 | -------------------------------------------------------------------------------- /src/display/analyseSpinner.ts: -------------------------------------------------------------------------------- 1 | import ora, { Ora } from 'ora'; 2 | import { printInfo } from './print'; 3 | 4 | export class AnalyseSpinner { 5 | private spinner: Ora; 6 | private numberOfFilesDone = 0; 7 | constructor(private numberOfFiles: number) { 8 | printInfo(`Analysing ${numberOfFiles} files...`); 9 | this.spinner = ora(this.getText()).start(); 10 | } 11 | public tick(): void { 12 | this.numberOfFilesDone += 1; 13 | this.spinner.text = this.getText(); 14 | } 15 | 16 | public stop(): void { 17 | this.spinner.stop(); 18 | } 19 | 20 | private getText(): string { 21 | return `${this.numberOfFilesDone}/${this.numberOfFiles}`; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/display/print.ts: -------------------------------------------------------------------------------- 1 | import { blue, dim, green, red, yellow } from 'chalk'; 2 | import { textSync } from 'figlet'; 3 | import { Unicode } from '../unicode'; 4 | 5 | export const printTitle = (title: string): void => 6 | console.log(blue(textSync(title))); 7 | 8 | export const printError = (error: Error): void => { 9 | console.error(red(`${Unicode.RedCross} ${error.message}`)); 10 | process.exitCode = 1; 11 | }; 12 | 13 | export const printWarning = (message: string): void => { 14 | console.warn(yellow(`${Unicode.Warning} ${message}`)); 15 | }; 16 | 17 | export const printSuccess = (message: string): void => { 18 | console.info(green(`${Unicode.CheckMark} ${message}`)); 19 | }; 20 | 21 | export const printInfo = (message: string): void => { 22 | console.info(dim(`${Unicode.Information} ${message}`)); 23 | }; 24 | -------------------------------------------------------------------------------- /src/eslintDisabledRules/eslintParser.spec.ts: -------------------------------------------------------------------------------- 1 | import { Token, TokenType } from '../types'; 2 | import { ALL_RULES, parseEslintRules } from './eslintParser'; 3 | 4 | const ruleLine = 123; 5 | const tokenFactory = (value: string): Token => ({ 6 | type: TokenType.LineComment, 7 | value, 8 | loc: { start: { line: ruleLine, column: 1 }, end: { line: 2, column: 3 } }, 9 | }); 10 | const filePath = 'filePath'; 11 | describe('EslintParser', () => { 12 | it('returns the eslint rule formatted', () => { 13 | const [eslintRule] = parseEslintRules( 14 | [tokenFactory('eslint-disable-next-line no-shadow')], 15 | filePath, 16 | ); 17 | 18 | expect(eslintRule).toEqual({ 19 | rule: 'no-shadow', 20 | file: filePath, 21 | line: ruleLine, 22 | }); 23 | }); 24 | 25 | it('returns the correct rule even with extra spaces', () => { 26 | const [eslintRule] = parseEslintRules( 27 | [tokenFactory(' eslint-disable-next-line no-shadow ')], 28 | filePath, 29 | ); 30 | 31 | expect(eslintRule.rule).toEqual('no-shadow'); 32 | }); 33 | 34 | it('returns the rules', () => { 35 | const eslintRules = parseEslintRules( 36 | [tokenFactory('eslint-disable-next-line no-shadow,prefer-const')], 37 | filePath, 38 | ); 39 | 40 | expect(eslintRules[0].rule).toEqual('no-shadow'); 41 | expect(eslintRules[1].rule).toEqual('prefer-const'); 42 | }); 43 | 44 | it('returns the correct rules even with extra spaces', () => { 45 | const eslintRules = parseEslintRules( 46 | [ 47 | tokenFactory( 48 | ' eslint-disable-next-line no-shadow , prefer-const ', 49 | ), 50 | ], 51 | filePath, 52 | ); 53 | 54 | expect(eslintRules[0].rule).toEqual('no-shadow'); 55 | expect(eslintRules[1].rule).toEqual('prefer-const'); 56 | }); 57 | 58 | it('returns ALL_RULES as rule if no rule is specified', () => { 59 | const [eslintRule] = parseEslintRules( 60 | [tokenFactory('eslint-disable')], 61 | filePath, 62 | ); 63 | 64 | expect(eslintRule.rule).toEqual(ALL_RULES); 65 | }); 66 | 67 | it('returns ALL_RULES as rule if no rule is specified even with extra spaces', () => { 68 | const [eslintRule] = parseEslintRules( 69 | [tokenFactory(' eslint-disable ')], 70 | filePath, 71 | ); 72 | 73 | expect(eslintRule.rule).toEqual(ALL_RULES); 74 | }); 75 | 76 | it('returns flatten rules of tokens', () => { 77 | const eslintRules = parseEslintRules( 78 | [ 79 | tokenFactory('eslint-disable'), 80 | tokenFactory('eslint-disable-next-line no-shadow, prefer-const'), 81 | tokenFactory('eslint-disable-next-line no-shadow'), 82 | ], 83 | filePath, 84 | ); 85 | 86 | expect(eslintRules[0].rule).toEqual(ALL_RULES); 87 | expect(eslintRules[1].rule).toEqual('no-shadow'); 88 | expect(eslintRules[2].rule).toEqual('prefer-const'); 89 | expect(eslintRules[3].rule).toEqual('no-shadow'); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /src/eslintDisabledRules/eslintParser.ts: -------------------------------------------------------------------------------- 1 | import { flatten } from 'lodash'; 2 | import { EslintDisabledRule } from './types'; 3 | import { Token } from '../types'; 4 | 5 | const eslintDisablePattern = 'eslint-disable'; 6 | export const ALL_RULES = 'ALL_RULES'; 7 | 8 | export const valueContainsEslintDisable = (value: string): boolean => 9 | value.includes(eslintDisablePattern); 10 | 11 | const valueIsAEslintRule = (value: string): boolean => 12 | !valueContainsEslintDisable(value); 13 | 14 | const splitValue = (value: string): string[] => 15 | flatten( 16 | value 17 | .trim() 18 | .split(' ') 19 | .map((splitOnce) => 20 | splitOnce 21 | .trim() 22 | .split(',') 23 | .filter((splitTwice) => splitTwice !== ''), 24 | ), 25 | ); 26 | 27 | export const parseEslintRules = ( 28 | tokens: Token[], 29 | filePath: string, 30 | ): EslintDisabledRule[] => 31 | flatten( 32 | tokens.map(({ value, loc }) => { 33 | const formatEslintRule = (rule: string) => ({ 34 | rule, 35 | file: filePath, 36 | line: loc.start.line, 37 | }); 38 | const eslintRules = splitValue(value).filter(valueIsAEslintRule); 39 | if (eslintRules.length === 0) { 40 | return [formatEslintRule(ALL_RULES)]; 41 | } 42 | return eslintRules.map(formatEslintRule); 43 | }), 44 | ); 45 | -------------------------------------------------------------------------------- /src/eslintDisabledRules/types.ts: -------------------------------------------------------------------------------- 1 | export type EslintDisabledRule = { 2 | rule: string; 3 | file: string; 4 | line: number; 5 | }; 6 | -------------------------------------------------------------------------------- /src/getDataFromFiles.ts: -------------------------------------------------------------------------------- 1 | import { getFileContent } from './getFiles'; 2 | import { getEslintDisabledTokens, getTokens } from './tokens'; 3 | import { parseEslintRules } from './eslintDisabledRules/eslintParser'; 4 | import { AnalyseSpinner } from './display/analyseSpinner'; 5 | import { EslintDisabledRule } from './eslintDisabledRules/types'; 6 | 7 | export type ParsedData = { 8 | filePath: string; 9 | eslintDisabledRules: EslintDisabledRule[]; 10 | totalLines: number; 11 | }; 12 | 13 | const getTotalLines = (fileContent: string): number => 14 | fileContent.split('\n').length; 15 | 16 | export const getDataFromFiles = async ({ 17 | filePaths, 18 | spinner, 19 | }: { 20 | filePaths: string[]; 21 | spinner: AnalyseSpinner; 22 | }): Promise => 23 | Promise.all( 24 | filePaths.map(async (filePath) => { 25 | const fileContent = await getFileContent(filePath); 26 | const totalLines = getTotalLines(fileContent); 27 | const tokens = getTokens(fileContent); 28 | const eslintDisabledTokens = getEslintDisabledTokens(tokens); 29 | const eslintDisabledRules = parseEslintRules( 30 | eslintDisabledTokens, 31 | filePath, 32 | ); 33 | spinner.tick(); 34 | return { filePath, eslintDisabledRules, totalLines }; 35 | }), 36 | ); 37 | -------------------------------------------------------------------------------- /src/getFiles.ts: -------------------------------------------------------------------------------- 1 | import glob, { Options } from 'fast-glob'; 2 | import { promises } from 'fs'; 3 | const { readFile } = promises; 4 | 5 | const globOptions: Options = { 6 | unique: true, 7 | onlyFiles: true, 8 | }; 9 | 10 | export const getFilesPaths = async (pattern: string): Promise => 11 | glob(pattern, globOptions); 12 | 13 | export const getFileContent = async (path: string): Promise => 14 | (await readFile(path)).toString(); 15 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { flatten, sumBy } from 'lodash'; 2 | import { printSuccess, printTitle } from './display/print'; 3 | import { getFilesPaths } from './getFiles'; 4 | import { Options } from './types'; 5 | import { computeStatistics } from './statistics/computeStats'; 6 | import { printStats } from './statistics/printStats'; 7 | import { AnalyseSpinner } from './display/analyseSpinner'; 8 | import { getDataFromFiles } from './getDataFromFiles'; 9 | 10 | export const computeEslintDisabledStats = async ( 11 | options: Options, 12 | ): Promise => { 13 | const { pattern } = options; 14 | printTitle('Eslint Disabled Stats'); 15 | const filePaths = await getFilesPaths(pattern); 16 | const totalFiles = filePaths.length; 17 | const spinner = new AnalyseSpinner(totalFiles); 18 | const parsedData = await getDataFromFiles({ 19 | filePaths, 20 | spinner, 21 | }); 22 | const eslintDisabledRules = flatten( 23 | parsedData.map((data) => data.eslintDisabledRules), 24 | ); 25 | const totalLines = sumBy(parsedData, 'totalLines'); 26 | spinner.stop(); 27 | const statistics = computeStatistics(eslintDisabledRules); 28 | printSuccess('Statistics computed'); 29 | printStats({ statistics, totalLines, totalFiles, options }); 30 | printSuccess('Done'); 31 | }; 32 | -------------------------------------------------------------------------------- /src/integration.spec.ts: -------------------------------------------------------------------------------- 1 | import { mocked } from 'ts-jest'; 2 | import { computeEslintDisabledStats } from './index'; 3 | import { getFileContent } from './getFiles'; 4 | import * as getFiles from './getFiles'; 5 | import { Options } from './types'; 6 | import { printStats } from './statistics/printStats'; 7 | import * as printStatsLib from './statistics/printStats'; 8 | import { Statistics } from './statistics/types'; 9 | 10 | const testPattern = 'example/**/*.(js|ts|jsx|tsx)'; 11 | const testOptions: Options = { 12 | pattern: testPattern, 13 | quiet: false, 14 | }; 15 | 16 | describe('computeEslintDisabledStats', () => { 17 | it('finds the two example files', async () => { 18 | jest.spyOn(getFiles, 'getFileContent'); 19 | 20 | await computeEslintDisabledStats(testOptions); 21 | 22 | expect(getFileContent).toHaveBeenCalledTimes(2); 23 | expect(getFileContent).toHaveBeenCalledWith('example/index.ts'); 24 | expect(getFileContent).toHaveBeenCalledWith( 25 | 'example/legacy/legacy-file.js', 26 | ); 27 | }); 28 | describe('Stats', () => { 29 | jest.spyOn(printStatsLib, 'printStats'); 30 | let statistics: Statistics; 31 | let totalFiles: number; 32 | let totalLines: number; 33 | beforeAll(async () => { 34 | await computeEslintDisabledStats(testOptions); 35 | ({ statistics, totalFiles, totalLines } = mocked( 36 | printStats, 37 | ).mock.calls[0][0]); 38 | }); 39 | 40 | it('counts 4 disabled rules', () => { 41 | expect(statistics.totalRulesDisabled).toEqual(4); 42 | }); 43 | 44 | it('correctly computes stats by files', () => { 45 | expect(statistics.byFiles).toEqual({ 46 | 'example/index.ts': [ 47 | { rule: 'prefer-const', file: 'example/index.ts', line: 6 }, 48 | { rule: 'eqeqeq', file: 'example/index.ts', line: 8 }, 49 | { rule: 'curly', file: 'example/index.ts', line: 8 }, 50 | ], 51 | 'example/legacy/legacy-file.js': [ 52 | { rule: 'ALL_RULES', file: 'example/legacy/legacy-file.js', line: 1 }, 53 | ], 54 | }); 55 | }); 56 | 57 | it('correctly computes stats by rules', () => { 58 | expect(statistics.byRules).toEqual({ 59 | 'prefer-const': [ 60 | { rule: 'prefer-const', file: 'example/index.ts', line: 6 }, 61 | ], 62 | eqeqeq: [{ rule: 'eqeqeq', file: 'example/index.ts', line: 8 }], 63 | curly: [{ rule: 'curly', file: 'example/index.ts', line: 8 }], 64 | ALL_RULES: [ 65 | { rule: 'ALL_RULES', file: 'example/legacy/legacy-file.js', line: 1 }, 66 | ], 67 | }); 68 | }); 69 | 70 | it('computes the number of files', () => { 71 | expect(totalFiles).toBe(2); 72 | }); 73 | 74 | it('computes the number of lines', () => { 75 | expect(totalLines).toBe(19); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/statistics/computeStats.ts: -------------------------------------------------------------------------------- 1 | import { groupBy } from 'lodash'; 2 | import { Statistics, StatsByFiles, StatsByRules } from './types'; 3 | import { EslintDisabledRule } from '../eslintDisabledRules/types'; 4 | 5 | const groupByRules = (disabledRules: EslintDisabledRule[]): StatsByRules => 6 | groupBy(disabledRules, 'rule'); 7 | 8 | const groupByFiles = (disabledRules: EslintDisabledRule[]): StatsByFiles => 9 | groupBy(disabledRules, 'file'); 10 | 11 | export const computeStatistics = ( 12 | disabledRules: EslintDisabledRule[], 13 | ): Statistics => { 14 | const totalRulesDisabled = disabledRules.length; 15 | const byRules = groupByRules(disabledRules); 16 | const byFiles = groupByFiles(disabledRules); 17 | return { totalRulesDisabled, byFiles, byRules }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/statistics/printStats.ts: -------------------------------------------------------------------------------- 1 | import { bold, cyan, yellow } from 'chalk'; 2 | import { Statistics } from './types'; 3 | import { Unicode } from '../unicode'; 4 | import { Options } from '../types'; 5 | 6 | const printList = (dictionary: { [key: string]: T[] }): void => 7 | Object.entries(dictionary).forEach(([key, disabledRules]) => 8 | console.log( 9 | `${Unicode.ListDot} ${bold(key)}: ${cyan(disabledRules.length)}`, 10 | ), 11 | ); 12 | 13 | export const printStats = ({ 14 | statistics, 15 | totalLines, 16 | totalFiles, 17 | options, 18 | }: { 19 | statistics: Statistics; 20 | totalLines: number; 21 | totalFiles: number; 22 | options: Options; 23 | }): void => { 24 | const { quiet } = options; 25 | const { totalRulesDisabled, byRules, byFiles } = statistics; 26 | if (!quiet) { 27 | console.log(`\nRules disabled by rule:`); 28 | printList(byRules); 29 | console.log(`\nRules disabled by file:`); 30 | printList(byFiles); 31 | } 32 | console.log(`\nTotal rules disabled: ${yellow.bold(totalRulesDisabled)}\n`); 33 | console.log(`Analysed files: ${yellow.bold(totalFiles)}`); 34 | console.log(`Analysed lines: ${yellow.bold(totalLines)}\n`); 35 | }; 36 | -------------------------------------------------------------------------------- /src/statistics/types.ts: -------------------------------------------------------------------------------- 1 | export type StatsByRules = { 2 | [ruleName: string]: { file: string; line: number }[]; 3 | }; 4 | export type StatsByFiles = { 5 | [filePath: string]: { rule: string; line: number }[]; 6 | }; 7 | export type Statistics = { 8 | byRules: StatsByRules; 9 | byFiles: StatsByFiles; 10 | totalRulesDisabled: number; 11 | }; 12 | -------------------------------------------------------------------------------- /src/tokens.ts: -------------------------------------------------------------------------------- 1 | import { tokenize, TokenizeOptions } from 'esprima'; 2 | import { valueContainsEslintDisable } from './eslintDisabledRules/eslintParser'; 3 | import { Token, TokenType } from './types'; 4 | 5 | const tokenizeOptions: TokenizeOptions = { 6 | comment: true, 7 | tolerant: true, // to not throw on invalid js files, this is not the purpose of this tool 8 | loc: true, 9 | }; 10 | 11 | export const getTokens = (fileContent: string): Token[] => 12 | tokenize(fileContent, tokenizeOptions) as Token[]; 13 | 14 | const isComment = ({ type }: Token): boolean => 15 | type === TokenType.LineComment || type === TokenType.BlockComment; 16 | 17 | const containsEslintDisable = ({ value }: Token): boolean => 18 | valueContainsEslintDisable(value); 19 | 20 | export const getEslintDisabledTokens = (tokens: Token[]): Token[] => 21 | tokens.filter((token) => isComment(token) && containsEslintDisable(token)); 22 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Token as EsprimaToken } from 'esprima'; 2 | export type Options = { 3 | pattern: string; 4 | quiet: boolean; 5 | }; 6 | 7 | type TokenLocation = { 8 | line: number; 9 | column: number; 10 | }; 11 | 12 | export enum TokenType { 13 | LineComment = 'LineComment', 14 | BlockComment = 'BlockComment', 15 | } 16 | 17 | export type Token = EsprimaToken & { 18 | type: TokenType; 19 | loc: { start: TokenLocation; end: TokenLocation }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/unicode.ts: -------------------------------------------------------------------------------- 1 | export enum Unicode { 2 | ListDot = '\u2022', 3 | RedCross = '\u274C', 4 | Warning = '\u26A0', 5 | CheckMark = '\u2714', 6 | Information = '\u2139', 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "baseUrl": ".", 11 | "strict": true, 12 | "lib": ["es2017"] 13 | }, 14 | "include": ["src/**/*.ts"], 15 | "exclude": ["node_modules"] 16 | } 17 | --------------------------------------------------------------------------------