-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #43 from ChoTotOSS/feat/share-ruleset
feat: share ruleset
- Loading branch information
Showing
16 changed files
with
2,294 additions
and
2,850 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@chotot/eslint-ruleset': major | ||
--- | ||
|
||
init ruleset for sharing between config |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
--- | ||
'@chotot/eslint-plugin-chotot': minor | ||
'@chotot/eslint-config-bare': minor | ||
'@chotot/eslint-config-next': minor | ||
'@chotot/eslint-ruleset': minor | ||
--- | ||
|
||
share ruleset |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
275 changes: 275 additions & 0 deletions
275
packages/eslint-plugin-chotot/src/rules/filename-case.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
// This is a testing plugin to check if eslint plugin loaded correctly | ||
|
||
import { defineRule } from '../utils/define-rule'; | ||
import * as path from 'path'; | ||
import { camelCase, kebabCase, snakeCase, upperFirst } from 'lodash'; | ||
import cartesianProductSamples from './utils/cartesian-product-samples.js'; | ||
|
||
const MESSAGE_ID = 'filename-case'; | ||
const MESSAGE_ID_EXTENSION = 'filename-extension'; | ||
const messages = { | ||
[MESSAGE_ID]: 'Filename is not in {{chosenCases}}. Rename it to {{renamedFilenames}}.', | ||
[MESSAGE_ID_EXTENSION]: | ||
'File extension `{{extension}}` is not in lowercase. Rename it to `{{filename}}`.', | ||
}; | ||
|
||
const pascalCase = (string) => upperFirst(camelCase(string)); | ||
const numberRegex = /\d+/; | ||
const PLACEHOLDER = '\uFFFF\uFFFF\uFFFF'; | ||
const PLACEHOLDER_REGEX = new RegExp(PLACEHOLDER, 'i'); | ||
const isIgnoredChar = (char) => !/^[a-z\d-_$]$/i.test(char); | ||
const ignoredByDefault = new Set([ | ||
'index.js', | ||
'index.mjs', | ||
'index.cjs', | ||
'index.ts', | ||
'index.tsx', | ||
'index.vue', | ||
]); | ||
const isLowerCase = (string) => string === string.toLowerCase(); | ||
|
||
function ignoreNumbers(caseFunction) { | ||
return (string) => { | ||
const stack = []; | ||
let execResult = numberRegex.exec(string); | ||
|
||
while (execResult) { | ||
stack.push(execResult[0]); | ||
string = string.replace(execResult[0], PLACEHOLDER); | ||
execResult = numberRegex.exec(string); | ||
} | ||
|
||
let withCase = caseFunction(string); | ||
|
||
while (stack.length > 0) { | ||
withCase = withCase.replace(PLACEHOLDER_REGEX, stack.shift()); | ||
} | ||
|
||
return withCase; | ||
}; | ||
} | ||
|
||
const cases = { | ||
camelCase: { | ||
fn: camelCase, | ||
name: 'camel case', | ||
}, | ||
kebabCase: { | ||
fn: kebabCase, | ||
name: 'kebab case', | ||
}, | ||
snakeCase: { | ||
fn: snakeCase, | ||
name: 'snake case', | ||
}, | ||
pascalCase: { | ||
fn: pascalCase, | ||
name: 'pascal case', | ||
}, | ||
}; | ||
|
||
/** | ||
Get the cases specified by the option. | ||
@param {object} options | ||
@returns {string[]} The chosen cases. | ||
*/ | ||
function getChosenCases(options) { | ||
if (options.case) { | ||
return [options.case]; | ||
} | ||
|
||
if (options.cases) { | ||
const cases = Object.keys(options.cases).filter((cases) => options.cases[cases]); | ||
|
||
return cases.length > 0 ? cases : ['kebabCase']; | ||
} | ||
|
||
return ['kebabCase']; | ||
} | ||
|
||
function validateFilename(words, caseFunctions) { | ||
return words | ||
.filter(({ ignored }) => !ignored) | ||
.every(({ word }) => caseFunctions.some((caseFunction) => caseFunction(word) === word)); | ||
} | ||
|
||
function fixFilename(words, caseFunctions, { leading, extension }) { | ||
const replacements = words.map(({ word, ignored }) => | ||
ignored ? [word] : caseFunctions.map((caseFunction) => caseFunction(word)) | ||
); | ||
|
||
const { samples: combinations } = cartesianProductSamples(replacements); | ||
|
||
return [ | ||
...new Set( | ||
combinations.map((parts) => `${leading}${parts.join('')}${extension.toLowerCase()}`) | ||
), | ||
]; | ||
} | ||
|
||
const leadingUnderscoresRegex = /^(?<leading>_+)(?<tailing>.*)$/; | ||
function splitFilename(filename) { | ||
const result = leadingUnderscoresRegex.exec(filename) || { groups: {} }; | ||
// @ts-ignore | ||
const { leading = '', tailing = filename } = result.groups; | ||
|
||
const words = []; | ||
|
||
let lastWord; | ||
for (const char of tailing) { | ||
const isIgnored = isIgnoredChar(char); | ||
|
||
if (lastWord && lastWord.ignored === isIgnored) { | ||
lastWord.word += char; | ||
} else { | ||
lastWord = { | ||
word: char, | ||
ignored: isIgnored, | ||
}; | ||
words.push(lastWord); | ||
} | ||
} | ||
|
||
return { | ||
leading, | ||
words, | ||
}; | ||
} | ||
|
||
/** | ||
Turns `[a, b, c]` into `a, b, or c`. | ||
@param {string[]} words | ||
@returns {string} | ||
*/ | ||
function englishishJoinWords(words) { | ||
if (words.length === 1) { | ||
return words[0]; | ||
} | ||
|
||
if (words.length === 2) { | ||
return `${words[0]} or ${words[1]}`; | ||
} | ||
|
||
return `${words.slice(0, -1).join(', ')}, or ${words[words.length - 1]}`; | ||
} | ||
|
||
const schema = [ | ||
{ | ||
oneOf: [ | ||
{ | ||
properties: { | ||
case: { | ||
enum: ['camelCase', 'snakeCase', 'kebabCase', 'pascalCase'], | ||
}, | ||
ignore: { | ||
type: 'array', | ||
uniqueItems: true, | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
{ | ||
properties: { | ||
cases: { | ||
properties: { | ||
camelCase: { | ||
type: 'boolean', | ||
}, | ||
snakeCase: { | ||
type: 'boolean', | ||
}, | ||
kebabCase: { | ||
type: 'boolean', | ||
}, | ||
pascalCase: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
ignore: { | ||
type: 'array', | ||
uniqueItems: true, | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
}, | ||
]; | ||
|
||
//------------------------------------------------------------------------------ | ||
// Rule Definition | ||
//------------------------------------------------------------------------------ | ||
export = defineRule({ | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Enforce a case style for filenames.', | ||
}, | ||
schema, | ||
messages, | ||
}, | ||
create(context) { | ||
const options = context.options[0] || {}; | ||
const chosenCases = getChosenCases(options); | ||
const ignore = (options.ignore || []).map((item) => { | ||
if (item instanceof RegExp) { | ||
return item; | ||
} | ||
|
||
return new RegExp(item, 'u'); | ||
}); | ||
const chosenCasesFunctions = chosenCases.map((case_) => ignoreNumbers(cases[case_].fn)); | ||
const filenameWithExtension = context.getPhysicalFilename(); | ||
|
||
if (filenameWithExtension === '<input>' || filenameWithExtension === '<text>') { | ||
return {}; | ||
} | ||
|
||
return { | ||
Program() { | ||
const extension = path.extname(filenameWithExtension); | ||
const filename = path.basename(filenameWithExtension, extension); | ||
const base = filename + extension; | ||
|
||
if (ignoredByDefault.has(base) || ignore.some((regexp) => regexp.test(base))) { | ||
return; | ||
} | ||
|
||
const { leading, words } = splitFilename(filename); | ||
const isValid = validateFilename(words, chosenCasesFunctions); | ||
|
||
if (isValid) { | ||
if (!isLowerCase(extension)) { | ||
return context.report({ | ||
loc: { column: 0, line: 1 }, | ||
messageId: MESSAGE_ID_EXTENSION, | ||
data: { filename: filename + extension.toLowerCase(), extension }, | ||
}); | ||
} | ||
|
||
return; | ||
} | ||
|
||
const renamedFilenames = fixFilename(words, chosenCasesFunctions, { | ||
leading, | ||
extension, | ||
}); | ||
|
||
return context.report({ | ||
// Report on first character like `unicode-bom` rule | ||
// https://github.com/eslint/eslint/blob/8a77b661bc921c3408bae01b3aa41579edfc6e58/lib/rules/unicode-bom.js#L46 | ||
loc: { column: 0, line: 1 }, | ||
messageId: MESSAGE_ID, | ||
data: { | ||
chosenCases: englishishJoinWords(chosenCases.map((x) => cases[x].name)), | ||
renamedFilenames: englishishJoinWords(renamedFilenames.map((x) => `\`${x}\``)), | ||
}, | ||
}); | ||
}, | ||
}; | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.