Skip to content

Commit

Permalink
feat: add jsx-filename-naming-convention
Browse files Browse the repository at this point in the history
  • Loading branch information
rEl1cx committed Aug 23, 2023
1 parent 7e16961 commit e97ced0
Show file tree
Hide file tree
Showing 17 changed files with 492 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,6 @@ This project uses code from following third-party projects:
- eslint-plugin-react (MIT)
- eslint-plugin-solid (MIT)
- @tanstack/eslint-plugin-query (MIT)
- eslint-plugin-filenames-simple (MIT)

Licenses are list in [THIRD-PARTY-LICENSE](THIRD-PARTY-LICENSE)
26 changes: 26 additions & 0 deletions THIRD-PARTY-LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,29 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

--------------------------------------------------------------------------------

eslint-plugin-filenames-simple

MIT License

Copyright (c) 2020 Ryo Maeda

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Empty file.
49 changes: 49 additions & 0 deletions src/lib/case-validator/case-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* eslint-disable security/detect-non-literal-regexp */
// Copied from https://github.com/epaew/eslint-plugin-filenames-simple/blob/master/src/utils/case-validator.ts
import { getRule } from "./preset-rules";

type RecommendationBuilder = (name: string) => string;

export class CaseValidator {
readonly #expression: RegExp;
readonly #ignorePatterns: RegExp[];
readonly #recommendationBuilder: RecommendationBuilder;

constructor(
expression: RegExp,
ignorePatterns: RegExp[],
recommendationBuilder: RecommendationBuilder = () => {
throw new Error("Not implemented");
},
) {
this.#expression = expression;
this.#ignorePatterns = ignorePatterns;
this.#recommendationBuilder = recommendationBuilder;
}

getRecommendedName(name: string): string {
const recommendedName = this.#recommendationBuilder(name);
if (this.#expression.test(recommendedName)) {
return recommendedName;
}

throw new Error("Failed to build recommendation.");
}

validate(name: string): boolean {
if (this.#ignorePatterns.some((re) => re.test(name))) {
return true;
}

return this.#expression.test(name);
}
}

export const getCaseValidator = (ruleName: string, ignorePattern: string[] = []): CaseValidator => {
const { expression, recommendationBuilder } = getRule(ruleName);
return new CaseValidator(
expression,
ignorePattern.map((pattern) => new RegExp(`^${pattern}$`, "u")),
recommendationBuilder,
);
};
File renamed without changes.
62 changes: 62 additions & 0 deletions src/lib/case-validator/preset-rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/* eslint-disable security/detect-object-injection */
/* eslint-disable security/detect-non-literal-regexp */
// Copied from https://github.com/epaew/eslint-plugin-filenames-simple/blob/master/src/utils/preset-rules.ts
import { splitName } from "./split-name";

type Rule = {
expression: RegExp;
recommendationBuilder?: (name: string) => string;
};
type PresetRules = {
[key: string]: Required<Rule> | undefined;
PascalCase: Required<Rule>;
camelCase: Required<Rule>;
"kebab-case": Required<Rule>;
snake_case: Required<Rule>;
};

export const presetRules: PresetRules = {
PascalCase: {
expression: /^[A-Z][\dA-Za-z]*$/u,
recommendationBuilder: (name: string): string => {
return splitName(name)
.map((word) => {
const [first, ...rest] = word;
return `${first?.toUpperCase() ?? ""}${rest.join("")}`;
})
.join("");
},
},
camelCase: {
expression: /^[a-z][\dA-Za-z]*$/u,
recommendationBuilder: (name: string): string => {
return splitName(name)
.map((word, i) => {
if (i === 0) {
return word;
}

const [first, ...rest] = word;
return `${first?.toUpperCase() ?? ""}${rest.join("")}`;
})
.join("");
},
},
"kebab-case": {
expression: /^[a-z][\d\-a-z]*$/u,
recommendationBuilder: (name: string): string => {
return splitName(name).join("-");
},
},
snake_case: {
expression: /^[a-z][\d_a-z]*$/u,
recommendationBuilder: (name: string): string => {
return splitName(name).join("_");
},
},
};

export const getRule = (expression: string): Rule => {
const rule = presetRules[expression];
return rule ?? { expression: new RegExp(`^${expression}$`, "u") };
};
11 changes: 11 additions & 0 deletions src/lib/case-validator/split-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copied from https://github.com/epaew/eslint-plugin-filenames-simple/blob/master/src/utils/split-name.ts
* Split the file/variable name written in camelCase, kebab-case, PascalCase, and snake_case.
*/
export const splitName = (name: string): string[] => {
return name
.replace(/_/gu, "-")
.replace(/([\da-z])([A-Z])|([A-Z])([A-Z])(?=[a-z])/gu, "$1$3-$2$4")
.toLowerCase()
.split("-");
};
2 changes: 1 addition & 1 deletion src/rules/jsx-boolean-value.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type Options = [("never" | "always")?, { always?: string[]; never?: string[] }?]

### Default Option

```js
```json
"react-ts/jsx-boolean-value": ["error", "never"]
```

Expand Down
65 changes: 65 additions & 0 deletions src/rules/jsx-filename-naming-convention.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# jsx-filename-naming-convention

## Rule Details

Examples of **correct** case for this rule:

```bash
npx eslint --rule 'react/jsx-filename-naming-convention: ["error", { "rule": "PascalCase" }]' .

src/components/ExampleComponent.tsx

✨ Done in 0.61s.
```

```bash
npx eslint --rule 'react/jsx-filename-naming-convention: ["error", { "rule": "kebab-case" }]' .

src/components/example-component.tsx

✨ Done in 0.61s.
```

Examples of **incorrect** case for this rule:

```bash
npx eslint --rule 'react/jsx-filename-naming-convention: ["error", { "rule": "PascalCase" }]' .

src/components/exampleComponent.tsx
1:1 error "File name `exampleComponent.tsx` does not match `PascalCase`. Should rename to `ExampleComponent.tsx` react/jsx-filename-naming-convention
✖ 1 problems (1 errors, 0 warnings)
```
```bash
npx eslint --rule 'react/jsx-filename-naming-convention: ["error", { "rule": "kebab-case" }]' .
src/components/example_component.tsx
1:1 error "File name `example_component.tsx` does not match `kebab-case`. Should rename to `example-component.tsx` react/jsx-filename-naming-convention

✖ 1 problems (1 errors, 0 warnings)
```

## Rule Options

### Type Signature

```ts
type Options = {
rule: "PascalCase" | "kebab-case" | "camelCase" | "snake_case";
};
```

### Default Option

```json
"react/jsx-filename-naming-convention": ["error", {
"rule": "PascalCase"
}]
```

- `rule`: The naming convention to enforce. Defaults to `PascalCase`

## When Not To Use It

If you are not using JSX, or if you don't want to enforce specific naming conventions for filenames.
156 changes: 156 additions & 0 deletions src/rules/jsx-filename-naming-convention.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import RuleTester, { getFixturesRootDir } from "../../test/rule-tester";
import rule from "./jsx-filename-naming-convention";

const rootDir = getFixturesRootDir();

const ruleTester = new RuleTester({
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2021,
sourceType: "module",
project: "./tsconfig.json",
tsconfigRootDir: rootDir,
},
});

const RULE_NAME = "jsx-filename-naming-convention";

const code = "export {}";

ruleTester.run(RULE_NAME, rule, {
valid: [
{
filename: "PascalCase.tsx",
code,
},
{
filename: "PascalCase.tsx",
code,
options: [{ rule: "PascalCase" }],
},
{
filename: "camelCase.tsx",
code,
options: [{ rule: "camelCase" }],
},
{
filename: "kebab-case.tsx",
code,
options: [{ rule: "kebab-case" }],
},
{
filename: "snake_case.tsx",
code,
options: [{ rule: "snake_case" }],
},
],
invalid: [
{
filename: "pascalCase.tsx",
code,
errors: [
{
messageId: "filenameCaseMismatchWithSuggestion",
data: {
name: "pascalCase.tsx",
rule: "PascalCase",
suggestion: "PascalCase.tsx",
},
},
],
},
{
filename: "camelCase.tsx",
code,
options: [{ rule: "PascalCase" }],
errors: [
{
messageId: "filenameCaseMismatchWithSuggestion",
data: {
name: "camelCase.tsx",
rule: "PascalCase",
suggestion: "CamelCase.tsx",
},
},
],
},
{
filename: "kebab-case.tsx",
code,
options: [{ rule: "PascalCase" }],
errors: [
{
messageId: "filenameCaseMismatchWithSuggestion",
data: {
name: "kebab-case.tsx",
rule: "PascalCase",
suggestion: "KebabCase.tsx",
},
},
],
},
{
filename: "snake_case.tsx",
code,
options: [{ rule: "PascalCase" }],
errors: [
{
messageId: "filenameCaseMismatchWithSuggestion",
data: {
name: "snake_case.tsx",
rule: "PascalCase",
suggestion: "SnakeCase.tsx",
},
},
],
},
{
filename: "PascalCase.tsx",
code,
options: [{ rule: "camelCase" }],
errors: [
{
messageId: "filenameCaseMismatchWithSuggestion",
data: {
name: "PascalCase.tsx",
rule: "camelCase",
suggestion: "pascalCase.tsx",
},
},
],
},
{
filename: "kebab-case.tsx",
code,
options: [{ rule: "camelCase" }],
errors: [
{
messageId: "filenameCaseMismatchWithSuggestion",
data: {
name: "kebab-case.tsx",
rule: "camelCase",
suggestion: "kebabCase.tsx",
},
},
],
},
{
filename: "snake_case.tsx",
code,
options: [{ rule: "camelCase" }],
errors: [
{
messageId: "filenameCaseMismatchWithSuggestion",
data: {
name: "snake_case.tsx",
rule: "camelCase",
suggestion: "snakeCase.tsx",
},
},
],
},
],
});
Loading

0 comments on commit e97ced0

Please sign in to comment.