Skip to content

Commit

Permalink
feat: eslint-plugin-case-police (#153)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <[email protected]>
  • Loading branch information
jaw52 and antfu authored May 3, 2023
1 parent 4bd4691 commit 322912b
Show file tree
Hide file tree
Showing 41 changed files with 2,441 additions and 1,601 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fixture
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
ignore-workspace-root-check=true
shamefully-hoist=true
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,23 @@ It will scan all your source files and fix the cases of [known names](./dict).

Only the word including both uppercase and lowercase will be fixed. (e.g. `Github` -> `GitHub`; `github` and `GITHUB` will be left untouched).

### Use in ESLint

We also provide an ESLint plugin that can be used to lint your codebase.

```bash
npm i -D eslint-plugin-case-police
```

```jsonc
// .eslintrc
{
"extends": [
"plugin:case-police/recommended"
]
}
```

### Use in CI

Simply add `case-police` (without `--fix`) to your workflow and it will exit with a non-zero code for your CI to catch it.
Expand Down
46 changes: 8 additions & 38 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,60 +1,30 @@
{
"name": "case-police",
"type": "module",
"version": "0.5.14",
"private": true,
"packageManager": "[email protected]",
"description": "Make the case correct, PLEASE!",
"author": "Anthony Fu <[email protected]>",
"license": "MIT",
"funding": "https://github.com/sponsors/antfu",
"homepage": "https://github.com/antfu/case-police#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/antfu/case-police.git"
},
"bugs": {
"url": "https://github.com/antfu/case-police/issues"
},
"keywords": [],
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs"
},
"./*": "./*"
},
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"bin": {
"case-police": "./bin/case-police.mjs"
},
"files": [
"dist",
"bin",
"dict"
],
"scripts": {
"build": "rimraf dist && unbuild",
"dev": "unbuild --stub",
"build": "pnpm -r run build",
"dev": "pnpm -r run dev",
"lint": "eslint .",
"prepublishOnly": "nr build",
"release": "bumpp && pnpm publish",
"start": "esno src/index.ts",
"release": "bumpp -r && pnpm -r publish",
"start": "cd packages/case-police && esno src/index.ts",
"test": "vitest",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@antfu/eslint-config": "^0.36.0",
"@antfu/ni": "^0.20.0",
"@babel/types": "^7.21.2",
"@types/fs-extra": "^11.0.1",
"@types/minimist": "^1.2.2",
"@types/node": "^18.14.6",
"bumpp": "^9.0.0",
"eslint": "^8.35.0",
"eslint-plugin-case-police": "workspace:*",
"esno": "^0.16.3",
"fast-glob": "^3.2.12",
"fs-extra": "^11.1.0",
"is-binary-path": "^2.1.0",
"is-text-path": "^2.0.0",
"minimist": "^1.2.8",
Expand Down
2 changes: 1 addition & 1 deletion bin/case-police.mjs → packages/case-police/bin/case-police.mjs
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env node
'use strict'
import('../dist/index.mjs')
import('../dist/cli.mjs')
3 changes: 3 additions & 0 deletions build.config.ts → packages/case-police/build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: [
'src/index',
'src/dirs',
'src/cli',
],
declaration: true,
clean: true,
rollup: {
emitCJS: true,
inlineDependencies: true,
},
})
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
50 changes: 50 additions & 0 deletions packages/case-police/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "case-police",
"type": "module",
"version": "0.5.14",
"packageManager": "[email protected]",
"description": "Make the case correct, PLEASE!",
"author": "Anthony Fu <[email protected]>",
"license": "MIT",
"funding": "https://github.com/sponsors/antfu",
"homepage": "https://github.com/antfu/case-police#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/antfu/case-police.git"
},
"bugs": {
"url": "https://github.com/antfu/case-police/issues"
},
"keywords": [],
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.cjs",
"import": "./dist/index.mjs"
},
"./cli": {
"types": "./dist/cli.d.ts",
"require": "./dist/cli.cjs",
"import": "./dist/cli.mjs"
},
"./*": "./*"
},
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"bin": {
"case-police": "./bin/case-police.mjs"
},
"files": [
"dist",
"bin",
"dict"
],
"scripts": {
"build": "rimraf dist && unbuild",
"dev": "unbuild --stub",
"prepublishOnly": "nr build",
"start": "esno src/index.ts"
}
}
26 changes: 5 additions & 21 deletions src/index.ts → packages/case-police/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable no-console */
import { existsSync, promises as fs } from 'node:fs'
import fg from 'fast-glob'
import c from 'picocolors'
Expand All @@ -7,7 +6,7 @@ import isText from 'is-text-path'
import pLimit from 'p-limit'
import minimist from 'minimist'
import { version } from '../package.json'
import { buildRegex, loadAllPresets, replace, resolvePreset } from './utils'
import { buildRegex, loadDictPresets, replace } from './utils'

async function run() {
const argv = minimist(process.argv.slice(2), {
Expand Down Expand Up @@ -37,28 +36,13 @@ async function run() {
ignore.push(...argv.ignore.split(',').map((i: string) => i.trim()))
ignore = ignore.filter(Boolean)

let dictionary = {}
let dict = {}

// presets
const presets: string[] = (argv.presets || '')
.split(',')
.map((i: string) => i.trim())
.filter(Boolean)
if (argv['no-default']) {
// nothing
}
else if (presets.length) {
Object.assign(
dictionary,
...await Promise.all(presets.map(resolvePreset)),
)
}
else {
dictionary = await loadAllPresets()
}
if (!argv['no-default'])
dict = await loadDictPresets(argv.preset)

// dict
let dict = argv['no-default'] ? {} : dictionary
if (argv.dict) {
const str = await fs.readFile(argv.dict, 'utf8')
const userDict = JSON.parse(str)
Expand All @@ -84,7 +68,7 @@ async function run() {
console.log()
console.log(c.inverse(c.red(' Case ')) + c.inverse(c.blue(' Police ')) + c.dim(` v${version}`))
console.log()
console.log(c.blue(files.length) + c.dim(' files found for checking, ') + c.cyan(Object.keys(dictionary).length) + c.dim(' words loaded\n'))
console.log(c.blue(files.length) + c.dim(' files found for checking, ') + c.cyan(Object.keys(dict).length) + c.dim(' words loaded\n'))
const wrote: string[] = []
await Promise.all(files.map(file => limit(async () => {
const code = await fs.readFile(file, 'utf-8')
Expand Down
5 changes: 5 additions & 0 deletions packages/case-police/src/dirs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { fileURLToPath } from 'node:url'
import { resolve } from 'node:path'

export const distDir = fileURLToPath(new URL('../dist', import.meta.url))
export const dictDir = resolve(distDir, '../dict')
1 change: 1 addition & 0 deletions packages/case-police/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './utils'
File renamed without changes.
93 changes: 68 additions & 25 deletions src/utils.ts → packages/case-police/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/* eslint-disable no-console */
import { existsSync, promises as fs } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import c from 'picocolors'
import { dictDir } from './dirs'

const DICT_FOLDER = path.resolve(fileURLToPath(import.meta.url), '../../dict')
export type Presets = 'softwares' | 'products' | 'general' | 'brands' | 'abbreviates'

export const DICT_FOLDER = dictDir

export const IGNORE_KEY = '@case-police-ignore'
export const DISABLE_KEY = '@case-police-disable'
Expand All @@ -19,49 +21,69 @@ export function buildRegex(dictionary: Record<string, string>): RegExp {
return regex
}

export async function replace(
export const replaceCore = (
code: string,
id: string,
_dict?: Record<string, string>,
dict: Record<string, string>,
ignore: string[] = [],
output?: (code: string, index: number, from: string, to: string) => void,
regex?: RegExp,
_ignore: string[] = [],
): Promise<string | undefined> {
if (code.includes(DISABLE_KEY))
return

const dict = _dict || await loadAllPresets()
const ignore = _ignore.slice()

) => {
regex = regex || buildRegex(dict)
Array.from(code.matchAll(IGNORE_REGEX)).forEach((match) => {
const [, key] = match
ignore.push(...key.split(',').map(k => k.trim().toLowerCase()).filter(Boolean))
})

regex = regex || buildRegex(dict)
let changed = false
code = code.replace(regex, (_, key: string, index: number) => {
if (containsUTF8(code, key, index))
code = code.replace(regex, (_, from: string, index: number) => {
if (containsUTF8(code, from, index))
return _

if (!key.match(/[A-Z]/) || !key.match(/[a-z]/))
if (!from.match(/[A-Z]/) || !from.match(/[a-z]/))
return _
const lower = key.toLowerCase()
const lower = from.toLowerCase()
if (ignore.includes(lower))
return _
const value = dict[lower]
if (!value || value === key)
const to = dict[lower]
if (!to || to === from)
return _
changed = true
const lines = code.slice(0, index).split('\n')
const line = lines.length
const col = (lines[line - 1].length || 0) + 1
console.log(`${c.yellow(key)} ${c.dim('→')} ${c.green(value)} \t ${c.dim(`./${id}:${line}:${col}`)}`)
return value
output?.(code, index, from, to)
return to
})
if (changed)
return code
}

export async function replace(
code: string,
id: string,
_dict?: Record<string, string>,
regex?: RegExp,
_ignore: string[] = [],
): Promise<string | undefined> {
if (code.includes(DISABLE_KEY))
return

const dict = _dict || await loadAllPresets()
const ignore = _ignore.slice()

const output = (code: string, offset: number, original: string, replaced: string) => {
const lines = code.slice(0, offset).split('\n')
const line = lines.length
const col = (lines[line - 1].length || 0) + 1
console.log(`${c.yellow(original)} ${c.dim('→')} ${c.green(replaced)} \t ${c.dim(`./${id}:${line}:${col}`)}`)
}

return replaceCore(
code,
dict,
ignore,
output,
regex,
)
}

export async function resolvePreset(preset: string) {
let result = {}
const file = `${preset}.json`
Expand Down Expand Up @@ -95,3 +117,24 @@ function containsUTF8(code: string, key: string, index: number) {
const tail = code.charAt(index + key.length)
return utf8Regex.test(head) || utf8Regex.test(tail)
}

export async function loadDictPresets(preset: string) {
const presets: string[] = (preset || '')
.split(',')
.map((i: string) => i.trim())
.filter(Boolean)

let dictionary = {}

if (presets.length) {
Object.assign(
dictionary,
...await Promise.all(presets.map(resolvePreset)),
)
}
else {
dictionary = await loadAllPresets()
}

return dictionary
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
15 changes: 15 additions & 0 deletions packages/eslint-plugin-case-police/build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
entries: [
'src/index',
'src/dirs',
'src/worker-load',
],
declaration: true,
clean: true,
rollup: {
emitCJS: true,
inlineDependencies: true,
},
})
1 change: 1 addition & 0 deletions packages/eslint-plugin-case-police/fixture/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
6 changes: 6 additions & 0 deletions packages/eslint-plugin-case-police/fixture/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": [
"@antfu",
"plugin:case-police/recommended"
]
}
Loading

0 comments on commit 322912b

Please sign in to comment.