Skip to content

Commit

Permalink
Merge pull request #43 from ChoTotOSS/feat/share-ruleset
Browse files Browse the repository at this point in the history
feat: share ruleset
  • Loading branch information
tunguyen-ct committed Sep 13, 2023
2 parents 5a0f698 + 8cecd4d commit 8f545f0
Show file tree
Hide file tree
Showing 16 changed files with 2,294 additions and 2,850 deletions.
5 changes: 5 additions & 0 deletions .changeset/purple-kiwis-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chotot/eslint-ruleset': major
---

init ruleset for sharing between config
8 changes: 8 additions & 0 deletions .changeset/violet-starfishes-deny.md
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
1,115 changes: 11 additions & 1,104 deletions packages/eslint-config-bare/index.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/eslint-config-bare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@typescript-eslint/parser": "5.62.0",
"eslint-import-resolver-node": "0.3.7",
"eslint-import-resolver-typescript": "3.5.5",
"eslint-plugin-import": "2.27.5"
"eslint-plugin-import": "2.27.5",
"@chotot/eslint-ruleset": "workspace:*"
}
}
1,751 changes: 19 additions & 1,732 deletions packages/eslint-config-next/index.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/eslint-config-next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"dependencies": {
"@chotot/eslint-plugin-chotot": "workspace:*",
"@typescript-eslint/eslint-plugin": "5.62.0",
"eslint-config-next": "13.4.8"
"eslint-config-next": "13.4.8",
"@chotot/eslint-ruleset": "workspace:*"
},
"peerDependencies": {
"eslint": "^7.23.0 || ^8.0.0",
Expand Down
6 changes: 4 additions & 2 deletions packages/eslint-plugin-chotot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
"dist"
],
"dependencies": {
"safe-regex": "2.1.1",
"@eslint-community/eslint-utils": "4.4.0"
"@eslint-community/eslint-utils": "4.4.0",
"@types/node": "^20.6.0",
"lodash": "4.17.21",
"safe-regex": "2.1.1"
},
"devDependencies": {
"@swc/cli": "0.1.62",
Expand Down
6 changes: 6 additions & 0 deletions packages/eslint-plugin-chotot/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ module.exports = {
'no-abusive-eslint-disable': require('./rules/no-abusive-eslint-disable'),
'no-unsafe-regex': require('./rules/no-unsafe-regex'),
'no-instanceof-array': require('./rules/no-instanceof-array'),
'filename-case': require('./rules/filename-case'),
'no-nested-ternary': require('./rules/no-nested-ternary'),
'no-this-assignment': require('./rules/no-this-assignment'),
},
configs: {
recommended: {
Expand All @@ -15,6 +18,9 @@ module.exports = {
'@chotot/chotot/no-abusive-eslint-disable': 'error',
'@chotot/chotot/no-unsafe-regex': 'error',
'@chotot/chotot/no-instanceof-array': 'error',
'@chotot/chotot/filename-case': 'error',
'@chotot/chotot/no-nested-ternary': 'error',
'@chotot/chotot/no-this-assignment': 'error',
},
},
},
Expand Down
275 changes: 275 additions & 0 deletions packages/eslint-plugin-chotot/src/rules/filename-case.ts
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}\``)),
},
});
},
};
},
});
2 changes: 1 addition & 1 deletion packages/eslint-plugin-chotot/src/rules/no-empty-catch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { defineRule } from '../utils/define-rule';
export = defineRule({
meta: {
messages: {
emptyCatch: 'Empty catch block is not allowed. Please do something with the error.',
emptyCatch: 'Empty catch block is not allowed.. Please do something with the error.',
},
},
create(context) {
Expand Down
Loading

0 comments on commit 8f545f0

Please sign in to comment.