From dcf184803dba5c58db03b7b60103677e01bdc6d8 Mon Sep 17 00:00:00 2001 From: Igor Kowalski Date: Wed, 2 Oct 2024 12:16:47 +0200 Subject: [PATCH] 3.0.0 --- README.md | 14 +- fileComposition.schema.json | 103 +++--- package.json | 4 +- .../fileComposition/fileComposition.consts.ts | 1 + src/rules/fileComposition/fileComposition.ts | 1 + .../fileComposition/fileComposition.types.ts | 26 +- .../getFileCompositionConfig.consts.ts | 99 ++--- .../helpers => }/getProgramFromNode.test.ts | 2 +- .../helpers => }/getProgramFromNode.ts | 0 .../isExportedName/helpers/isExportDefault.ts | 2 +- .../isExportedName/helpers/isNamedExport.ts | 2 +- .../validateRules/helpers/getRules.test.ts | 15 - .../helpers/validateRules/helpers/getRules.ts | 7 - .../helpers/isCorrectSelector.test.ts | 4 +- .../helpers/getCustomError.test.ts | 60 ++- .../helpers/getCustomError.ts | 24 +- .../isSelectorAllowed.test.ts | 54 +++ .../isSelectorAllowed/isSelectorAllowed.ts | 61 ++-- .../prepareFormat/prepareFormat.test.ts | 4 +- .../helpers/prepareFormat/prepareFormat.ts | 4 +- .../helpers/removeFilenameParts.test.ts | 4 +- .../helpers/removeFilenameParts.ts | 4 +- .../helpers/getConvertedPositionIndex.test.ts | 35 ++ .../helpers/getConvertedPositionIndex.ts | 14 + .../helpers/getNodePosition.test.ts | 342 ++++++++++++++++++ .../helpers/getNodePosition.ts | 22 ++ .../validatePositionIndex.test.ts | 45 +++ .../validatePositionIndex.ts | 57 +++ .../validateRules/validateRules.test.ts | 38 +- .../helpers/validateRules/validateRules.ts | 115 +++--- .../helpers/validateFile/validateFile.test.ts | 85 +++-- .../helpers/validateFile/validateFile.ts | 42 ++- 32 files changed, 975 insertions(+), 315 deletions(-) rename src/rules/fileComposition/helpers/validateFile/helpers/{isExportedName/helpers => }/getProgramFromNode.test.ts (94%) rename src/rules/fileComposition/helpers/validateFile/helpers/{isExportedName/helpers => }/getProgramFromNode.ts (100%) delete mode 100644 src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/getRules.test.ts delete mode 100644 src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/getRules.ts create mode 100644 src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/isSelectorAllowed.test.ts create mode 100644 src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getConvertedPositionIndex.test.ts create mode 100644 src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getConvertedPositionIndex.ts create mode 100644 src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getNodePosition.test.ts create mode 100644 src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getNodePosition.ts create mode 100644 src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/validatePositionIndex.test.ts create mode 100644 src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/validatePositionIndex.ts diff --git a/README.md b/README.md index d741a39..9d715d6 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@

FolderOwl
eslint‑plugin-project‑structure

-

ESLint plugin with rules to help you achieve a scalable, consistent, and well-structured project.

-

Create your own framework! Define your folder structure, advanced naming conventions, file composition, and create independent modules.

+

Powerful ESLint plugin with rules to help you achieve a scalable, consistent, and well-structured project.

+

Create your own framework! Define your folder structure, file composition, advanced naming conventions, and create independent modules.

Take your project to the next level and save time by automating the review of key principles of a healthy project!

@@ -43,7 +43,6 @@ Leave a ⭐ and share the link with your friends.
## 📚 Documentation -- [Migration guide to 2.7.0](https://github.com/Igorkowalski94/eslint-plugin-project-structure/wiki/Migration-guide-to-2.7.0) - [project-structure/folder-structure](https://github.com/Igorkowalski94/eslint-plugin-project-structure/wiki/project%E2%80%91structure-%E2%80%8Bfolder%E2%80%91structure#root) - [project-structure/independent-modules](https://github.com/Igorkowalski94/eslint-plugin-project-structure/wiki/project%E2%80%91structure-%E2%80%8Bindependent%E2%80%91modules#root) - [project-structure/file-composition](https://github.com/Igorkowalski94/eslint-plugin-project-structure/wiki/project%E2%80%91structure-%E2%80%8Bfile%E2%80%91composition#root) @@ -84,18 +83,19 @@ where removing or editing one feature triggers a chain reaction that impacts the - An option to create a separate configuration file with TypeScript support.

project‑structure/​file‑composition

-

Enforce advanced naming rules and prohibit the use of given selectors in a given file.

-

Have full control over what your file can contain and the naming conventions it must follow.

+

Compose your ideal files!

+

Have full control over the order and quantity of selectors. Define advanced naming conventions and prohibit the use of specific selectors in given files.

Rocket Features:

- File composition validation. - Supported selectors: `class`, `function`, `arrowFunction`, `type`, `interface`, `enum`, `variable`, `variableExpression`. +- Inheriting the filename as the selector name. Option to add your own prefixes/suffixes, change the case, or remove parts of the filename. +- Prohibit the use of given selectors in a given file. For example, `**/*.consts.ts` files can only contain variables, `**/*.types.ts` files can only contain interfaces and types. +- Define the order in which your selectors should appear in a given file. Support for `--fix` to automatically correct the order. - Rules for exported selectors, selectors in the root of the file and nested/all selectors in the file. They can be used together in combination. -- Prohibit the use of given selectors in a given file. For example, `**/*.consts.ts` files can only contain variables, `**/*.types.ts` files can only contain enums, interfaces and types. - Enforcing a maximum of one main function/class per file. - The ability to set a specific limit on the occurrence of certain selectors in the root of a given file. -- Inheriting the filename as the selector name. Option to add your own prefixes/suffixes, change the case, or remove parts of the filename. - Selector name regex validation. - Build in case validation. - Different rules for different files. diff --git a/fileComposition.schema.json b/fileComposition.schema.json index e04f881..296fd05 100644 --- a/fileComposition.schema.json +++ b/fileComposition.schema.json @@ -90,6 +90,12 @@ } ] }, + "scope": { + "type": "string", + "default": "file", + "enum": ["file", "fileExport", "fileRoot"] + }, + "positionIndex": { "type": "number", "default": 0 }, "filenamePartsToRemove": { "oneOf": [ { "type": "string", "default": "" }, @@ -109,34 +115,20 @@ }, "required": ["selector"] }, - "FileRuleObject": { + "CustomErrors": { "type": "object", "default": {}, "additionalProperties": false, "properties": { - "allowOnlySpecifiedSelectors": { "type": "boolean", "default": true }, - "errors": { - "type": "object", - "default": {}, - "additionalProperties": false, - "properties": { - "class": { "type": "string", "default": "" }, - "variable": { "type": "string", "default": "" }, - "variableExpression": { "type": "string", "default": "" }, - "function": { "type": "string", "default": "" }, - "arrowFunction": { "type": "string", "default": "" }, - "type": { "type": "string", "default": "" }, - "interface": { "type": "string", "default": "" }, - "enum": { "type": "string", "default": "" } - } - }, - "rules": { - "type": "array", - "default": [], - "items": { "$ref": "#/definitions/FileRule" } - } - }, - "required": ["rules"] + "class": { "type": "string", "default": "" }, + "variable": { "type": "string", "default": "" }, + "variableExpression": { "type": "string", "default": "" }, + "function": { "type": "string", "default": "" }, + "arrowFunction": { "type": "string", "default": "" }, + "type": { "type": "string", "default": "" }, + "interface": { "type": "string", "default": "" }, + "enum": { "type": "string", "default": "" } + } }, "FileRules": { "type": "object", @@ -162,36 +154,47 @@ } ] }, - "rootSelectorsLimits": { "$ref": "#/definitions/RootSelectorsLimits" }, - "fileRootRules": { + "allowOnlySpecifiedSelectors": { "oneOf": [ + { "type": "boolean", "default": true }, { - "type": "array", - "default": [], - "items": { "$ref": "#/definitions/FileRule" } - }, - { "$ref": "#/definitions/FileRuleObject" } - ] - }, - "fileExportRules": { - "oneOf": [ - { - "type": "array", - "default": [], - "items": { "$ref": "#/definitions/FileRule" } - }, - { "$ref": "#/definitions/FileRuleObject" } + "type": "object", + "additionalProperties": false, + "default": { + "errors": {}, + "fileRoot": true, + "fileExport": true, + "file": true + }, + "properties": { + "error": { "$ref": "#/definitions/CustomErrors" }, + "fileRoot": { + "oneOf": [ + { "type": "boolean", "default": true }, + { "$ref": "#/definitions/CustomErrors" } + ] + }, + "fileExport": { + "oneOf": [ + { "type": "boolean", "default": true }, + { "$ref": "#/definitions/CustomErrors" } + ] + }, + "file": { + "oneOf": [ + { "type": "boolean", "default": true }, + { "$ref": "#/definitions/CustomErrors" } + ] + } + } + } ] }, - "fileRules": { - "oneOf": [ - { - "type": "array", - "default": [], - "items": { "$ref": "#/definitions/FileRule" } - }, - { "$ref": "#/definitions/FileRuleObject" } - ] + "rootSelectorsLimits": { "$ref": "#/definitions/RootSelectorsLimits" }, + "rules": { + "type": "array", + "default": [], + "items": { "$ref": "#/definitions/FileRule" } } }, "required": ["filePattern"] diff --git a/package.json b/package.json index 17a3154..0d08b95 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "author": "Igor Kowalski (Igorkowalski94)", "name": "eslint-plugin-project-structure", - "version": "2.7.4", + "version": "3.0.0", "license": "MIT", - "description": "ESLint plugin with rules to help you achieve a scalable, consistent, and well-structured project. Define your folder structure, advanced naming conventions, file composition, and create independent modules. react folder structure react file structure react project structure react conventions architecture react next.js angular node solid vue svelte", + "description": "Powerful ESLint plugin with rules to help you achieve a scalable, consistent, and well-structured project. Create your own framework! Define your folder structure, file composition, advanced naming conventions, and create independent modules. Take your project to the next level and save time by automating the review of key principles of a healthy project! react folder structure react file structure react project structure react conventions architecture react next.js angular node solid vue svelte", "keywords": [ "react", "native", diff --git a/src/rules/fileComposition/fileComposition.consts.ts b/src/rules/fileComposition/fileComposition.consts.ts index b469b02..319d342 100644 --- a/src/rules/fileComposition/fileComposition.consts.ts +++ b/src/rules/fileComposition/fileComposition.consts.ts @@ -1,5 +1,6 @@ export const ESLINT_ERRORS = { invalidName: `🔥 Invalid {{selectorType}} name, allowed formats = {{formatWithoutReferences}} 🔥`, + invalidPosition: `🔥 Invalid {{selectorType}} position. The current 'positionIndex' is {{currentPosition}}, but {{positionIndex}} is required. 🔥`, prohibitedSelector: `🔥 The use of '{{selectorType}}' is prohibited in this file. 🔥{{error}}`, prohibitedSelectorRoot: `🔥 The use of '{{selectorType}}' is prohibited in the root of the file. 🔥{{error}}`, prohibitedSelectorExport: `🔥 Exporting '{{selectorType}}' is prohibited in this file. 🔥{{error}}`, diff --git a/src/rules/fileComposition/fileComposition.ts b/src/rules/fileComposition/fileComposition.ts index 58a3444..3de3ddb 100644 --- a/src/rules/fileComposition/fileComposition.ts +++ b/src/rules/fileComposition/fileComposition.ts @@ -25,6 +25,7 @@ export const fileComposition = ESLintUtils.RuleCreator( type: "problem", schema: [{ type: "object", additionalProperties: true }], messages: ESLINT_ERRORS, + fixable: "code", }, defaultOptions: [], create(context) { diff --git a/src/rules/fileComposition/fileComposition.types.ts b/src/rules/fileComposition/fileComposition.types.ts index d4edc3b..8b4090c 100644 --- a/src/rules/fileComposition/fileComposition.types.ts +++ b/src/rules/fileComposition/fileComposition.types.ts @@ -44,31 +44,35 @@ interface VariableExpression { export type Selector = SelectorType | VariableExpression; -export interface FileRule { +export type Scope = "fileExport" | "fileRoot" | "file"; + +export interface Rule { selector: Selector | Selector[]; + scope?: Scope; + positionIndex?: number; filenamePartsToRemove?: string | string[]; format?: string[] | string; } -export type CustomErrors = Partial>; - -export interface FileRuleObject { - allowOnlySpecifiedSelectors?: boolean; - errors?: CustomErrors; - rules: FileRule[]; -} +type CustomErrors = Partial>; export interface RootSelectorLimit { selector: SelectorType | SelectorType[]; limit: number; } +export interface AllowOnlySpecifiedSelectors { + error?: CustomErrors; + fileRoot?: boolean | CustomErrors; + fileExport?: boolean | CustomErrors; + file?: boolean | CustomErrors; +} + export interface FileRules { filePattern: Pattern; + allowOnlySpecifiedSelectors?: AllowOnlySpecifiedSelectors | boolean; rootSelectorsLimits?: RootSelectorLimit[]; - fileRootRules?: FileRule[] | FileRuleObject; - fileExportRules?: FileRule[] | FileRuleObject; - fileRules?: FileRule[] | FileRuleObject; + rules?: Rule[]; } export interface FileCompositionConfig { diff --git a/src/rules/fileComposition/helpers/getFileCompositionConfig/getFileCompositionConfig.consts.ts b/src/rules/fileComposition/helpers/getFileCompositionConfig/getFileCompositionConfig.consts.ts index 7981e19..bb1de91 100644 --- a/src/rules/fileComposition/helpers/getFileCompositionConfig/getFileCompositionConfig.consts.ts +++ b/src/rules/fileComposition/helpers/getFileCompositionConfig/getFileCompositionConfig.consts.ts @@ -92,6 +92,12 @@ export const FILE_COMPOSITION_SCHEMA: JSONSchema4 = { }, ], }, + scope: { + type: "string", + default: "file", + enum: ["file", "fileExport", "fileRoot"], + }, + positionIndex: { type: "number", default: 0 }, filenamePartsToRemove: { oneOf: [ { type: "string", default: "" }, @@ -111,34 +117,20 @@ export const FILE_COMPOSITION_SCHEMA: JSONSchema4 = { }, required: ["selector"], }, - FileRuleObject: { + CustomErrors: { type: "object", default: {}, additionalProperties: false, properties: { - allowOnlySpecifiedSelectors: { type: "boolean", default: true }, - errors: { - type: "object", - default: {}, - additionalProperties: false, - properties: { - class: { type: "string", default: "" }, - variable: { type: "string", default: "" }, - variableExpression: { type: "string", default: "" }, - function: { type: "string", default: "" }, - arrowFunction: { type: "string", default: "" }, - type: { type: "string", default: "" }, - interface: { type: "string", default: "" }, - enum: { type: "string", default: "" }, - }, - }, - rules: { - type: "array", - default: [], - items: { $ref: "#/definitions/FileRule" }, - }, + class: { type: "string", default: "" }, + variable: { type: "string", default: "" }, + variableExpression: { type: "string", default: "" }, + function: { type: "string", default: "" }, + arrowFunction: { type: "string", default: "" }, + type: { type: "string", default: "" }, + interface: { type: "string", default: "" }, + enum: { type: "string", default: "" }, }, - required: ["rules"], }, FileRules: { type: "object", @@ -164,36 +156,47 @@ export const FILE_COMPOSITION_SCHEMA: JSONSchema4 = { }, ], }, - rootSelectorsLimits: { $ref: "#/definitions/RootSelectorsLimits" }, - fileRootRules: { - oneOf: [ - { - type: "array", - default: [], - items: { $ref: "#/definitions/FileRule" }, - }, - { $ref: "#/definitions/FileRuleObject" }, - ], - }, - fileExportRules: { + allowOnlySpecifiedSelectors: { oneOf: [ + { type: "boolean", default: true }, { - type: "array", - default: [], - items: { $ref: "#/definitions/FileRule" }, + type: "object", + additionalProperties: false, + default: { + errors: {}, + fileRoot: true, + fileExport: true, + file: true, + }, + properties: { + error: { $ref: "#/definitions/CustomErrors" }, + fileRoot: { + oneOf: [ + { type: "boolean", default: true }, + { $ref: "#/definitions/CustomErrors" }, + ], + }, + fileExport: { + oneOf: [ + { type: "boolean", default: true }, + { $ref: "#/definitions/CustomErrors" }, + ], + }, + file: { + oneOf: [ + { type: "boolean", default: true }, + { $ref: "#/definitions/CustomErrors" }, + ], + }, + }, }, - { $ref: "#/definitions/FileRuleObject" }, ], }, - fileRules: { - oneOf: [ - { - type: "array", - default: [], - items: { $ref: "#/definitions/FileRule" }, - }, - { $ref: "#/definitions/FileRuleObject" }, - ], + rootSelectorsLimits: { $ref: "#/definitions/RootSelectorsLimits" }, + rules: { + type: "array", + default: [], + items: { $ref: "#/definitions/FileRule" }, }, }, required: ["filePattern"], diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/isExportedName/helpers/getProgramFromNode.test.ts b/src/rules/fileComposition/helpers/validateFile/helpers/getProgramFromNode.test.ts similarity index 94% rename from src/rules/fileComposition/helpers/validateFile/helpers/isExportedName/helpers/getProgramFromNode.test.ts rename to src/rules/fileComposition/helpers/validateFile/helpers/getProgramFromNode.test.ts index f3f929a..73394f1 100644 --- a/src/rules/fileComposition/helpers/validateFile/helpers/isExportedName/helpers/getProgramFromNode.test.ts +++ b/src/rules/fileComposition/helpers/validateFile/helpers/getProgramFromNode.test.ts @@ -1,6 +1,6 @@ import { TSESTree } from "@typescript-eslint/utils"; -import { getProgramFromNode } from "rules/fileComposition/helpers/validateFile/helpers/isExportedName/helpers/getProgramFromNode"; +import { getProgramFromNode } from "rules/fileComposition/helpers/validateFile/helpers/getProgramFromNode"; import { ValidateFileProps } from "rules/fileComposition/helpers/validateFile/validateFile"; describe("IsExportName", () => { diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/isExportedName/helpers/getProgramFromNode.ts b/src/rules/fileComposition/helpers/validateFile/helpers/getProgramFromNode.ts similarity index 100% rename from src/rules/fileComposition/helpers/validateFile/helpers/isExportedName/helpers/getProgramFromNode.ts rename to src/rules/fileComposition/helpers/validateFile/helpers/getProgramFromNode.ts diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/isExportedName/helpers/isExportDefault.ts b/src/rules/fileComposition/helpers/validateFile/helpers/isExportedName/helpers/isExportDefault.ts index a38ffbe..15eaa1d 100644 --- a/src/rules/fileComposition/helpers/validateFile/helpers/isExportedName/helpers/isExportDefault.ts +++ b/src/rules/fileComposition/helpers/validateFile/helpers/isExportedName/helpers/isExportDefault.ts @@ -1,6 +1,6 @@ import { TSESTree } from "@typescript-eslint/utils"; -import { getProgramFromNode } from "rules/fileComposition/helpers/validateFile/helpers/isExportedName/helpers/getProgramFromNode"; +import { getProgramFromNode } from "rules/fileComposition/helpers/validateFile/helpers/getProgramFromNode"; import { ValidateFileProps } from "rules/fileComposition/helpers/validateFile/validateFile"; export interface IsExportDefaultProps { diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/isExportedName/helpers/isNamedExport.ts b/src/rules/fileComposition/helpers/validateFile/helpers/isExportedName/helpers/isNamedExport.ts index 04e127c..64c2a9d 100644 --- a/src/rules/fileComposition/helpers/validateFile/helpers/isExportedName/helpers/isNamedExport.ts +++ b/src/rules/fileComposition/helpers/validateFile/helpers/isExportedName/helpers/isNamedExport.ts @@ -1,6 +1,6 @@ import { TSESTree } from "@typescript-eslint/utils"; -import { getProgramFromNode } from "rules/fileComposition/helpers/validateFile/helpers/isExportedName/helpers/getProgramFromNode"; +import { getProgramFromNode } from "rules/fileComposition/helpers/validateFile/helpers/getProgramFromNode"; import { ValidateFileProps } from "rules/fileComposition/helpers/validateFile/validateFile"; export interface IsNamedExportProps { diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/getRules.test.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/getRules.test.ts deleted file mode 100644 index 2d152fd..0000000 --- a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/getRules.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { getRules } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/getRules"; - -describe("getRules", () => { - test("Should return correct value for object", () => { - expect(getRules({ rules: [{ selector: "arrowFunction" }] })).toEqual([ - { selector: "arrowFunction" }, - ]); - }); - - test("Should return correct value for array", () => { - expect(getRules([{ selector: "arrowFunction" }])).toEqual([ - { selector: "arrowFunction" }, - ]); - }); -}); diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/getRules.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/getRules.ts deleted file mode 100644 index 7459ae3..0000000 --- a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/getRules.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { - FileRule, - FileRuleObject, -} from "rules/fileComposition/fileComposition.types"; - -export const getRules = (fileRule: FileRule[] | FileRuleObject): FileRule[] => - Array.isArray(fileRule) ? fileRule : fileRule.rules; diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isCorrectSelector.test.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isCorrectSelector.test.ts index 36ff207..daaa5de 100644 --- a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isCorrectSelector.test.ts +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isCorrectSelector.test.ts @@ -2,13 +2,13 @@ import { getInvalidRegexError } from "errors/getInvalidRegexError"; import { SelectorType, - FileRule, + Rule, } from "rules/fileComposition/fileComposition.types"; import { isCorrectSelector } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isCorrectSelector"; describe("isCorrectNameType", () => { test.each<{ - selector: FileRule["selector"]; + selector: Rule["selector"]; selectorType: SelectorType; expected: boolean; expressionName?: string; diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/helpers/getCustomError.test.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/helpers/getCustomError.test.ts index 05bf4ed..085a62c 100644 --- a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/helpers/getCustomError.test.ts +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/helpers/getCustomError.test.ts @@ -1,16 +1,48 @@ -import { getCustomError } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/helpers/getCustomError"; +import { + getCustomError, + GetCustomErrorProps, +} from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/helpers/getCustomError"; -describe("getRules", () => { - test("Should return error message when !!errors", () => { - expect( - getCustomError({ - selectorType: "arrowFunction", - errors: { arrowFunction: "arrowFunction error" }, - }), - ).toEqual("\n\narrowFunction error\n\n"); - }); - - test("Should return undefined when !errors", () => { - expect(getCustomError({ selectorType: "arrowFunction" })).toEqual(""); - }); +describe("getCustomError", () => { + test.each<{ + allowOnlySpecifiedSelectors: GetCustomErrorProps["allowOnlySpecifiedSelectors"]; + expected: string; + }>([ + { + allowOnlySpecifiedSelectors: true, + expected: "", + }, + { + allowOnlySpecifiedSelectors: { error: { arrowFunction: "errorGlobal" } }, + expected: "\n\nerrorGlobal\n\n", + }, + { + allowOnlySpecifiedSelectors: { + error: { arrowFunction: "errorGlobal" }, + file: { arrowFunction: "errorFile" }, + }, + expected: "\n\nerrorFile\n\n", + }, + { + allowOnlySpecifiedSelectors: { + file: { arrowFunction: "errorFile" }, + }, + expected: "\n\nerrorFile\n\n", + }, + { + allowOnlySpecifiedSelectors: {}, + expected: "", + }, + ])( + "Should return correct values for %o", + ({ allowOnlySpecifiedSelectors, expected }) => { + expect( + getCustomError({ + allowOnlySpecifiedSelectors, + scope: "file", + selectorType: "arrowFunction", + }), + ).toEqual(expected); + }, + ); }); diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/helpers/getCustomError.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/helpers/getCustomError.ts index 278fc47..09fa167 100644 --- a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/helpers/getCustomError.ts +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/helpers/getCustomError.ts @@ -1,18 +1,30 @@ import { - CustomErrors, + AllowOnlySpecifiedSelectors, + Scope, SelectorType, } from "rules/fileComposition/fileComposition.types"; -interface GetCustomErrorProps { +export interface GetCustomErrorProps { selectorType: SelectorType; - errors?: CustomErrors; + allowOnlySpecifiedSelectors: AllowOnlySpecifiedSelectors | true; + scope: Scope; } export const getCustomError = ({ selectorType, - errors, + allowOnlySpecifiedSelectors, + scope, }: GetCustomErrorProps): string => { - if (!errors?.[selectorType]) return ""; + if (allowOnlySpecifiedSelectors === true) return ""; - return `\n\n${errors[selectorType]}\n\n`; + if ( + typeof allowOnlySpecifiedSelectors[scope] === "object" && + allowOnlySpecifiedSelectors[scope][selectorType] + ) + return `\n\n${allowOnlySpecifiedSelectors[scope][selectorType]}\n\n`; + + if (allowOnlySpecifiedSelectors.error?.[selectorType]) + return `\n\n${allowOnlySpecifiedSelectors.error[selectorType]}\n\n`; + + return ""; }; diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/isSelectorAllowed.test.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/isSelectorAllowed.test.ts new file mode 100644 index 0000000..2dd969b --- /dev/null +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/isSelectorAllowed.test.ts @@ -0,0 +1,54 @@ +import { + FileRules, + Node, + SelectorType, +} from "rules/fileComposition/fileComposition.types"; +import { isSelectorAllowed } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/isSelectorAllowed"; + +describe("isSelectorAllowed", () => { + test.each<{ + allowOnlySpecifiedSelectors: FileRules["allowOnlySpecifiedSelectors"]; + expected: boolean; + selectorType: SelectorType; + }>([ + { + allowOnlySpecifiedSelectors: true, + selectorType: "arrowFunction", + expected: true, + }, + { + allowOnlySpecifiedSelectors: true, + selectorType: "variable", + expected: false, + }, + { + allowOnlySpecifiedSelectors: { + file: false, + }, + selectorType: "variable", + expected: true, + }, + { + allowOnlySpecifiedSelectors: { + file: false, + }, + selectorType: "variable", + expected: true, + }, + ])( + "Should return correct values for %o", + ({ allowOnlySpecifiedSelectors, selectorType, expected }) => { + expect( + isSelectorAllowed({ + errorMessageId: "prohibitedSelector", + node: {} as Node, + rules: [{ selector: "arrowFunction" }], + scope: "file", + allowOnlySpecifiedSelectors, + report: jest.fn(), + selectorType, + }), + ).toEqual(expected); + }, + ); +}); diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/isSelectorAllowed.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/isSelectorAllowed.ts index 12a4ab3..e0ccb3c 100644 --- a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/isSelectorAllowed.ts +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/isSelectorAllowed.ts @@ -1,16 +1,19 @@ import { ESLINT_ERRORS } from "rules/fileComposition/fileComposition.consts"; import { Context, - FileRule, - FileRuleObject, + Rule, Node, SelectorType, + Scope, + FileRules, } from "rules/fileComposition/fileComposition.types"; import { isCorrectSelector } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isCorrectSelector"; import { getCustomError } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/helpers/getCustomError"; interface IsSelectorAllowedProps { - fileRule: FileRule[] | FileRuleObject; + rules: Rule[]; + allowOnlySpecifiedSelectors?: FileRules["allowOnlySpecifiedSelectors"]; + scope: Scope; report: Context["report"]; node: Node; errorMessageId: keyof typeof ESLINT_ERRORS; @@ -19,36 +22,42 @@ interface IsSelectorAllowedProps { } export const isSelectorAllowed = ({ - fileRule, + rules, report, node, errorMessageId, selectorType, expressionName, + scope, + allowOnlySpecifiedSelectors, }: IsSelectorAllowedProps): boolean => { + const isAllowed = rules + .map(({ selector }) => selector) + .flat() + .some((selector) => + isCorrectSelector({ selector, selectorType, expressionName }), + ); + if ( - !Array.isArray(fileRule) && - fileRule.allowOnlySpecifiedSelectors && - !fileRule.rules - .map(({ selector }) => selector) - .flat() - .some((selector) => - isCorrectSelector({ selector, selectorType, expressionName }), - ) - ) { - report({ - messageId: errorMessageId, - data: { + isAllowed || + !allowOnlySpecifiedSelectors || + (typeof allowOnlySpecifiedSelectors === "object" && + allowOnlySpecifiedSelectors[scope] === false) + ) + return true; + + report({ + messageId: errorMessageId, + data: { + selectorType, + error: getCustomError({ selectorType, - error: getCustomError({ - selectorType, - errors: fileRule.errors, - }), - }, - node, - }); - return false; - } + scope, + allowOnlySpecifiedSelectors, + }), + }, + node, + }); - return true; + return false; }; diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/prepareFormat/prepareFormat.test.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/prepareFormat/prepareFormat.test.ts index b04d340..1eb4086 100644 --- a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/prepareFormat/prepareFormat.test.ts +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/prepareFormat/prepareFormat.test.ts @@ -6,7 +6,7 @@ import { STRICT_PASCAL_CASE, } from "consts"; -import { FileRule } from "rules/fileComposition/fileComposition.types"; +import { Rule } from "rules/fileComposition/fileComposition.types"; import { prepareFormat, PrepareFormatReturn, @@ -15,7 +15,7 @@ import { describe("prepareFormat", () => { test.each<{ filenameWithoutParts: string; - format?: FileRule["format"]; + format?: Rule["format"]; expected: PrepareFormatReturn; }>([ { diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/prepareFormat/prepareFormat.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/prepareFormat/prepareFormat.ts index a9660f6..102fa35 100644 --- a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/prepareFormat/prepareFormat.ts +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/prepareFormat/prepareFormat.ts @@ -3,13 +3,13 @@ import { RegexParameters } from "types"; import { getRegexWithoutReferences } from "helpers/getRegexWithoutReferences/getRegexWithoutReferences"; -import { FileRule } from "rules/fileComposition/fileComposition.types"; +import { Rule } from "rules/fileComposition/fileComposition.types"; import { getDefaultRegexParameters } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/getDefaultRegexParameters"; import { DEFAULT_FORMAT } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/prepareFormat/prepareFormat.consts"; interface PrepareFormatProps { filenameWithoutParts: string; - format: FileRule["format"]; + format: Rule["format"]; regexParameters?: RegexParameters; } diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/removeFilenameParts.test.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/removeFilenameParts.test.ts index 1e9a607..d76fb5d 100644 --- a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/removeFilenameParts.test.ts +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/removeFilenameParts.test.ts @@ -1,9 +1,9 @@ -import { FileRule } from "rules/fileComposition/fileComposition.types"; +import { Rule } from "rules/fileComposition/fileComposition.types"; import { removeFilenameParts } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/removeFilenameParts"; describe("removeFilenameParts", () => { test.each<{ - filenamePartsToRemove?: FileRule["filenamePartsToRemove"]; + filenamePartsToRemove?: Rule["filenamePartsToRemove"]; filenameWithoutExtension: string; expected: string; }>([ diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/removeFilenameParts.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/removeFilenameParts.ts index 67005f8..6022959 100644 --- a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/removeFilenameParts.ts +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/removeFilenameParts.ts @@ -1,8 +1,8 @@ -import { FileRule } from "rules/fileComposition/fileComposition.types"; +import { Rule } from "rules/fileComposition/fileComposition.types"; interface RemoveFilenamePartsProps { filenameWithoutExtension: string; - filenamePartsToRemove: FileRule["filenamePartsToRemove"]; + filenamePartsToRemove: Rule["filenamePartsToRemove"]; } export const removeFilenameParts = ({ diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getConvertedPositionIndex.test.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getConvertedPositionIndex.test.ts new file mode 100644 index 0000000..17fd715 --- /dev/null +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getConvertedPositionIndex.test.ts @@ -0,0 +1,35 @@ +import { getConvertedPositionIndex } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getConvertedPositionIndex"; + +describe("getConvertedPositionIndex", () => { + test.each<{ + positionIndex: number; + bodyWithoutImportsLength: number; + expected: number; + }>([ + { + positionIndex: -1, + bodyWithoutImportsLength: 1, + expected: 0, + }, + { + positionIndex: 2, + bodyWithoutImportsLength: 1, + expected: 0, + }, + { + positionIndex: 1, + bodyWithoutImportsLength: 2, + expected: 1, + }, + ])( + "Should return correct values for %o", + ({ bodyWithoutImportsLength, positionIndex, expected }) => { + expect( + getConvertedPositionIndex({ + bodyWithoutImportsLength, + positionIndex, + }), + ).toEqual(expected); + }, + ); +}); diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getConvertedPositionIndex.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getConvertedPositionIndex.ts new file mode 100644 index 0000000..4e242ad --- /dev/null +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getConvertedPositionIndex.ts @@ -0,0 +1,14 @@ +interface GetConvertedPositionIndexProps { + positionIndex: number; + bodyWithoutImportsLength: number; +} + +export const getConvertedPositionIndex = ({ + positionIndex, + bodyWithoutImportsLength, +}: GetConvertedPositionIndexProps): number => { + if (positionIndex < 0) return 0; + if (positionIndex > bodyWithoutImportsLength - 1) + return bodyWithoutImportsLength - 1; + return positionIndex; +}; diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getNodePosition.test.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getNodePosition.test.ts new file mode 100644 index 0000000..31d5c5a --- /dev/null +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getNodePosition.test.ts @@ -0,0 +1,342 @@ +import { TSESTree } from "@typescript-eslint/utils"; + +import { Node } from "rules/fileComposition/fileComposition.types"; +import { getNodePosition } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getNodePosition"; + +describe("getConvertedPositionIndex", () => { + test.each<{ + node: Node; + expected: number; + }>([ + { + node: { + type: "VariableDeclarator", + parent: { + type: "VariableDeclaration", + parent: { type: "Program", range: [0, 76] }, + declarations: [ + { + type: "VariableDeclarator", + id: { + type: "Identifier", + name: "variable", + range: [6, 14], + }, + init: { + type: "Literal", + value: 1, + raw: "1", + range: [17, 18], + }, + range: [6, 18], + }, + ], + kind: "const", + range: [0, 18], + }, + id: { + type: "Identifier", + name: "variable", + range: [6, 14], + }, + init: { + type: "Literal", + value: 1, + raw: "1", + range: [17, 18], + }, + range: [6, 18], + } as Node, + expected: 0, + }, + { + node: { + type: "VariableDeclarator", + parent: { + type: "VariableDeclaration", + parent: { type: "Program", range: [0, 76] }, + declarations: [ + { + type: "VariableDeclarator", + id: { + type: "Identifier", + name: "arrowFunction", + range: [25, 38], + }, + init: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [], + body: { + type: "BlockStatement", + body: [], + range: [47, 49], + }, + async: false, + expression: false, + range: [41, 49], + }, + range: [25, 49], + }, + ], + kind: "const", + range: [19, 49], + }, + id: { + type: "Identifier", + name: "arrowFunction", + range: [25, 38], + }, + init: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [], + body: { + type: "BlockStatement", + body: [], + range: [47, 49], + }, + async: false, + expression: false, + range: [41, 49], + }, + range: [25, 49], + } as unknown as Node, + expected: 1, + }, + { + node: { + type: "FunctionDeclaration", + parent: { type: "Program", range: [0, 76] }, + id: { + type: "Identifier", + name: "functionName", + range: [59, 71], + }, + generator: false, + expression: false, + async: false, + params: [], + body: { + type: "BlockStatement", + body: [], + range: [73, 75], + }, + range: [50, 75], + } as unknown as Node, + expected: 2, + }, + { + node: { + type: "VariableDeclarator", + parent: { + type: "VariableDeclaration", + parent: { + type: "ExportNamedDeclaration", + declaration: { + type: "VariableDeclaration", + declarations: [ + { + type: "VariableDeclarator", + id: { + type: "Identifier", + name: "arrowFunction", + range: [89, 102], + }, + init: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [], + body: { + type: "BlockStatement", + body: [], + range: [111, 113], + }, + async: false, + expression: false, + range: [105, 113], + }, + range: [89, 113], + }, + ], + kind: "const", + range: [83, 113], + }, + specifiers: [], + source: null, + exportKind: "value", + range: [76, 113], + assertions: [], + }, + declarations: [ + { + type: "VariableDeclarator", + id: { + type: "Identifier", + name: "arrowFunction", + range: [89, 102], + }, + init: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [], + body: { + type: "BlockStatement", + body: [], + range: [111, 113], + }, + async: false, + expression: false, + range: [105, 113], + }, + range: [89, 113], + }, + ], + kind: "const", + range: [83, 113], + }, + id: { + type: "Identifier", + name: "arrowFunction", + range: [89, 102], + }, + init: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [], + body: { + type: "BlockStatement", + body: [], + range: [111, 113], + }, + async: false, + expression: false, + range: [105, 113], + }, + range: [89, 113], + } as unknown as Node, + expected: 3, + }, + ])("Should return correct values for %o", ({ node, expected }) => { + expect( + getNodePosition({ + bodyWithoutImports: [ + { + type: "VariableDeclaration", + declarations: [ + { + type: "VariableDeclarator", + id: { + type: "Identifier", + name: "variable", + range: [6, 14], + }, + init: { + type: "Literal", + value: 1, + raw: "1", + range: [17, 18], + }, + range: [6, 18], + }, + ], + kind: "const", + range: [0, 18], + }, + { + type: "VariableDeclaration", + declarations: [ + { + type: "VariableDeclarator", + id: { + type: "Identifier", + name: "arrowFunction", + range: [25, 38], + }, + init: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [], + body: { + type: "BlockStatement", + body: [], + range: [47, 49], + }, + async: false, + expression: false, + range: [41, 49], + }, + range: [25, 49], + }, + ], + kind: "const", + range: [19, 49], + }, + { + type: "FunctionDeclaration", + id: { + type: "Identifier", + name: "functionName", + range: [59, 71], + }, + generator: false, + expression: false, + async: false, + params: [], + body: { + type: "BlockStatement", + body: [], + range: [73, 75], + }, + range: [50, 75], + }, + { + type: "ExportNamedDeclaration", + declaration: { + type: "VariableDeclaration", + declarations: [ + { + type: "VariableDeclarator", + id: { + type: "Identifier", + name: "arrowFunction", + range: [89, 102], + }, + init: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [], + body: { + type: "BlockStatement", + body: [], + range: [111, 113], + }, + async: false, + expression: false, + range: [105, 113], + }, + range: [89, 113], + }, + ], + kind: "const", + range: [83, 113], + }, + specifiers: [], + source: null, + exportKind: "value", + range: [76, 113], + assertions: [], + }, + ] as TSESTree.ProgramStatement[], + node, + }), + ).toEqual(expected); + }); +}); diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getNodePosition.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getNodePosition.ts new file mode 100644 index 0000000..f78eb11 --- /dev/null +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getNodePosition.ts @@ -0,0 +1,22 @@ +import { TSESTree } from "@typescript-eslint/utils"; + +import { Node } from "rules/fileComposition/fileComposition.types"; + +interface GetNodePositionProps { + bodyWithoutImports: TSESTree.ProgramStatement[]; + node: Node; +} + +export const getNodePosition = ({ + bodyWithoutImports, + node, +}: GetNodePositionProps): number => + bodyWithoutImports.findIndex( + (bodyNode) => + (bodyNode.range[0] === node.range[0] && + bodyNode.range[1] === node.range[1]) || + (bodyNode.range[0] === node.parent.range[0] && + bodyNode.range[1] === node.parent.range[1]) || + (bodyNode.range[0] === node.parent.parent?.range[0] && + bodyNode.range[1] === node.parent.parent.range[1]), + ); diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/validatePositionIndex.test.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/validatePositionIndex.test.ts new file mode 100644 index 0000000..364ab07 --- /dev/null +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/validatePositionIndex.test.ts @@ -0,0 +1,45 @@ +import { Context, Node } from "rules/fileComposition/fileComposition.types"; +import { getConvertedPositionIndex } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getConvertedPositionIndex"; +import { getNodePosition } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getNodePosition"; +import { validatePositionIndex } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/validatePositionIndex"; + +jest.mock( + "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getNodePosition", + () => ({ + getNodePosition: jest.fn(), + }), +); + +jest.mock( + "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getConvertedPositionIndex", + () => ({ + getConvertedPositionIndex: jest.fn(), + }), +); + +describe("validatePositionIndex", () => { + test("Should return undefined if positionIndex === undefined", () => { + expect( + validatePositionIndex({ + context: {} as Context, + node: {} as Node, + selectorType: "arrowFunction", + positionIndex: undefined, + }), + ).toEqual(undefined); + }); + + test("Should return undefined if nodePosition === convertedPositionIndex", () => { + (getNodePosition as jest.Mock).mockReturnValue(1); + (getConvertedPositionIndex as jest.Mock).mockReturnValue(1); + + expect( + validatePositionIndex({ + context: {} as Context, + node: { type: "Program", body: [] } as unknown as Node, + selectorType: "arrowFunction", + positionIndex: 1, + }), + ).toEqual(undefined); + }); +}); diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/validatePositionIndex.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/validatePositionIndex.ts new file mode 100644 index 0000000..08b0ba8 --- /dev/null +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/validatePositionIndex.ts @@ -0,0 +1,57 @@ +import { TSESTree } from "@typescript-eslint/utils"; + +import { + Context, + Node, + SelectorType, +} from "rules/fileComposition/fileComposition.types"; +import { getProgramFromNode } from "rules/fileComposition/helpers/validateFile/helpers/getProgramFromNode"; +import { getConvertedPositionIndex } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getConvertedPositionIndex"; +import { getNodePosition } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/helpers/getNodePosition"; + +interface ValidatePositionIndexProps { + node: Node; + positionIndex?: number; + selectorType: SelectorType; + context: Context; +} + +export const validatePositionIndex = ({ + node, + positionIndex, + selectorType, + context: { report, sourceCode }, +}: ValidatePositionIndexProps): void => { + if (positionIndex === undefined) return; + + const program = getProgramFromNode(node); + const bodyWithoutImports = program.body.filter( + ({ type }) => type !== TSESTree.AST_NODE_TYPES.ImportDeclaration, + ); + + const nodePosition = getNodePosition({ bodyWithoutImports, node }); + + const convertedPositionIndex = getConvertedPositionIndex({ + positionIndex, + bodyWithoutImportsLength: bodyWithoutImports.length, + }); + + if (nodePosition === convertedPositionIndex) return; + + const nodeToReplace = bodyWithoutImports[convertedPositionIndex]; + const currentNodePosition = bodyWithoutImports[nodePosition]; + + report({ + messageId: "invalidPosition", + node, + data: { + selectorType, + currentPosition: nodePosition, + positionIndex: convertedPositionIndex, + }, + fix: (fixer) => [ + fixer.replaceText(nodeToReplace, sourceCode.getText(currentNodePosition)), + fixer.replaceText(currentNodePosition, sourceCode.getText(nodeToReplace)), + ], + }); +}; diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/validateRules.test.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/validateRules.test.ts index e6b6531..38b1108 100644 --- a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/validateRules.test.ts +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/validateRules.test.ts @@ -1,21 +1,20 @@ import { TSESTree } from "@typescript-eslint/utils"; +import { Context } from "rules/fileComposition/fileComposition.types"; import { validateRules } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/validateRules"; describe("validateRules", () => { test("Should return undefined if !isSelectorAllowed", () => { expect( validateRules({ - fileRule: { - allowOnlySpecifiedSelectors: true, - rules: [{ selector: "arrowFunction" }], - }, + rules: [{ selector: "arrowFunction" }], filenamePath: "C:/somePath/src/features/Feature1/Feature1.tsx", - report: () => undefined, + context: { report: jest.fn() } as unknown as Context, name: "functionName", node: {} as TSESTree.VariableDeclarator, nodeType: "VariableDeclarator", errorMessageId: "prohibitedSelector", + scope: "file", }), ).toEqual(undefined); }); @@ -23,19 +22,18 @@ describe("validateRules", () => { test("Should return undefined if !isSelectorAllowed && !expressionName", () => { expect( validateRules({ - fileRule: { - allowOnlySpecifiedSelectors: true, - rules: [ - { selector: { type: "variableExpression", limitTo: "styled" } }, - ], - }, + allowOnlySpecifiedSelectors: true, + rules: [ + { selector: { type: "variableExpression", limitTo: "styled" } }, + ], filenamePath: "C:/somePath/src/features/Feature1/Feature1.tsx", - report: () => undefined, + context: { report: jest.fn() } as unknown as Context, name: "functionName", node: {} as TSESTree.VariableDeclarator, nodeType: "Expression", errorMessageId: "prohibitedSelector", expressionName: "expressionName", + scope: "file", }), ).toEqual(undefined); }); @@ -43,13 +41,15 @@ describe("validateRules", () => { test("Should return undefined if !isCorrectSelector", () => { expect( validateRules({ - fileRule: [{ selector: "arrowFunction" }], + rules: [{ selector: "arrowFunction" }], filenamePath: "C:/somePath/src/features/Feature1/Feature1.tsx", - report: () => undefined, + context: { report: jest.fn() } as unknown as Context, name: "functionName", node: {} as TSESTree.VariableDeclarator, nodeType: "VariableDeclarator", errorMessageId: "prohibitedSelector", + scope: "file", + allowOnlySpecifiedSelectors: true, }), ).toEqual(undefined); }); @@ -57,9 +57,9 @@ describe("validateRules", () => { test("Should return undefined if isValidExport", () => { expect( validateRules({ - fileRule: [{ selector: "variable" }], + rules: [{ selector: "variable" }], filenamePath: "C:/somePath/src/features/Feature1/Feature1.tsx", - report: () => undefined, + context: { report: jest.fn() } as unknown as Context, name: "functionName", node: { parent: { @@ -70,6 +70,7 @@ describe("validateRules", () => { } as unknown as TSESTree.VariableDeclarator, nodeType: "VariableDeclarator", errorMessageId: "prohibitedSelector", + scope: "file", }), ).toEqual(undefined); }); @@ -78,9 +79,9 @@ describe("validateRules", () => { const reportMock = jest.fn(); validateRules({ - fileRule: [{ selector: "variable" }], + rules: [{ selector: "variable" }], filenamePath: "C:/somePath/src/features/Feature1/Feature1.tsx", - report: reportMock, + context: { report: reportMock } as unknown as Context, name: "SOME_NAME", node: { parent: { @@ -91,6 +92,7 @@ describe("validateRules", () => { } as unknown as TSESTree.VariableDeclarator, nodeType: "VariableDeclarator", errorMessageId: "prohibitedSelector", + scope: "file", }); expect(reportMock).toHaveBeenCalledWith({ diff --git a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/validateRules.ts b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/validateRules.ts index 8306d2b..83e18b6 100644 --- a/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/validateRules.ts +++ b/src/rules/fileComposition/helpers/validateFile/helpers/validateRules/validateRules.ts @@ -3,18 +3,19 @@ import { RegexParameters } from "types"; import { ESLINT_ERRORS } from "rules/fileComposition/fileComposition.consts"; import { Context, - FileRule, - FileRuleObject, + Rule, NodeType, + Scope, + FileRules, } from "rules/fileComposition/fileComposition.types"; import { getFileNameWithoutExtension } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/getFileNameWithoutExtension"; import { getFormatWithFilenameReferences } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/getFormatWithFilenameReferences"; -import { getRules } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/getRules"; import { isCorrectSelector } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isCorrectSelector"; import { isNameValid } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isNameValid"; import { isSelectorAllowed } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/isSelectorAllowed/isSelectorAllowed"; import { prepareFormat } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/prepareFormat/prepareFormat"; import { removeFilenameParts } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/removeFilenameParts"; +import { validatePositionIndex } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/helpers/validatePositionIndex/validatePositionIndex"; import { SELECTORS } from "rules/fileComposition/helpers/validateFile/helpers/validateRules/validateRules.consts"; import { ValidateFileProps } from "rules/fileComposition/helpers/validateFile/validateFile"; @@ -23,79 +24,101 @@ interface ValidateRulesProps { filenamePath: string; node: ValidateFileProps["node"]; nodeType: NodeType; - report: Context["report"]; - fileRule: FileRule[] | FileRuleObject; + rules: Rule[]; errorMessageId: keyof typeof ESLINT_ERRORS; regexParameters?: RegexParameters; expressionName?: string; + allowOnlySpecifiedSelectors?: FileRules["allowOnlySpecifiedSelectors"]; + scope: Scope; + context: Context; } export const validateRules = ({ nodeType, name, node, - report, filenamePath, - fileRule, + rules, errorMessageId, regexParameters, expressionName, + allowOnlySpecifiedSelectors, + scope, + context, + context: { report }, }: ValidateRulesProps): void => { const selectorType = SELECTORS[nodeType]; if ( !isSelectorAllowed({ - fileRule, + rules, + scope, + allowOnlySpecifiedSelectors, node, selectorType, report, errorMessageId, expressionName, - }) + }) || + name === "*" ) return; - getRules(fileRule).forEach((rule) => { - if ( - !isCorrectSelector({ - selectorType, - selector: rule.selector, - expressionName, - }) - ) - return; + const selectorTypeRules = rules.filter(({ selector }) => + isCorrectSelector({ + selectorType, + selector, + expressionName, + }), + ); - const { format, filenamePartsToRemove } = rule; + const formatWithoutReferences = selectorTypeRules + .map((rule) => { + const { format, filenamePartsToRemove, positionIndex } = rule; - const filenameWithoutExtension = getFileNameWithoutExtension(filenamePath); - const filenameWithoutParts = removeFilenameParts({ - filenameWithoutExtension, - filenamePartsToRemove, - }); - const { formatWithReferences, formatWithoutReferences } = prepareFormat({ - format, - filenameWithoutParts, - regexParameters, - }); - const isValidExport = isNameValid({ - formatWithoutReferences, - name, - }); + const filenameWithoutExtension = + getFileNameWithoutExtension(filenamePath); + const filenameWithoutParts = removeFilenameParts({ + filenameWithoutExtension, + filenamePartsToRemove, + }); + const { formatWithReferences, formatWithoutReferences } = prepareFormat({ + format, + filenameWithoutParts, + regexParameters, + }); + const isValid = isNameValid({ + formatWithoutReferences, + name, + }); - if (isValidExport || name === "*") return; + if (isValid) + return validatePositionIndex({ + node, + positionIndex, + selectorType, + context, + }); - const formatWithFilenameReferences = getFormatWithFilenameReferences({ - formatWithReferences, - filename: filenameWithoutParts, - }); + return getFormatWithFilenameReferences({ + formatWithReferences, + filename: filenameWithoutParts, + }); + }) + .filter((v): v is string[] => v !== undefined); - report({ - node, - messageId: "invalidName", - data: { - selectorType, - formatWithoutReferences: formatWithFilenameReferences.join(", "), - }, - }); + if ( + !formatWithoutReferences.length || + formatWithoutReferences.length !== selectorTypeRules.length + ) + return; + + report({ + node, + messageId: "invalidName", + data: { + selectorType, + formatWithoutReferences: formatWithoutReferences.flat().join(", "), + }, }); }; diff --git a/src/rules/fileComposition/helpers/validateFile/validateFile.test.ts b/src/rules/fileComposition/helpers/validateFile/validateFile.test.ts index 2ae5732..2460195 100644 --- a/src/rules/fileComposition/helpers/validateFile/validateFile.test.ts +++ b/src/rules/fileComposition/helpers/validateFile/validateFile.test.ts @@ -44,14 +44,7 @@ describe("validateFile", () => { node: {} as TSESTree.VariableDeclarator, nodeType: "VariableDeclarator", config: { - filesRules: [ - { - filePattern: "**/*.ts", - fileRootRules: [{ selector: "variable" }], - fileExportRules: [{ selector: "variable" }], - fileRules: [{ selector: "variable" }], - }, - ], + filesRules: [{ filePattern: "**/*.ts" }], }, fileConfig: undefined, }), @@ -83,35 +76,40 @@ describe("validateFile", () => { filesRules: [ { filePattern: "**/*.ts", - fileRootRules: [{ selector: "variable" }], - fileExportRules: [{ selector: "variable" }], - fileRules: [{ selector: "variable" }], + rules: [{ scope: "fileExport", selector: "variable" }], }, ], }, fileConfig: { filePattern: "**/*.ts", - fileRootRules: [{ selector: "variable" }], - fileExportRules: [{ selector: "variable" }], - fileRules: [{ selector: "variable" }], + rules: [{ scope: "fileExport", selector: "variable" }], }, }); expect(validateRulesMock).toHaveBeenCalledWith({ - fileRule: [{ selector: "variable" }], + rules: [{ scope: "fileExport", selector: "variable" }], name: "componentNameExport", nodeType: "VariableDeclarator", node: {}, - report: reportMock, filenamePath: path.relative( "C:/somePath", "C:/somePath/src/features/Feature1/Feature1.ts", ), errorMessageId: "prohibitedSelectorExport", + scope: "fileExport", + context: { + report: reportMock, + settings: {}, + cwd: "C:/somePath", + filename: "C:/somePath/src/features/Feature1/Feature1.ts", + }, + expressionName: undefined, + allowOnlySpecifiedSelectors: undefined, + regexParameters: undefined, }); }); - test("Should call fileExportRules for fileRootRules", () => { + test("Should call fileRootRules", () => { const validateRulesMock = jest.fn(); const reportMock = jest.fn(); @@ -137,35 +135,40 @@ describe("validateFile", () => { filesRules: [ { filePattern: "**/*.ts", - fileRootRules: [{ selector: "variable" }], - fileExportRules: [{ selector: "variable" }], - fileRules: [{ selector: "variable" }], + rules: [{ scope: "fileRoot", selector: "variable" }], }, ], }, fileConfig: { filePattern: "**/*.ts", - fileRootRules: [{ selector: "variable" }], - fileExportRules: [{ selector: "variable" }], - fileRules: [{ selector: "variable" }], + rules: [{ scope: "fileRoot", selector: "variable" }], }, }); expect(validateRulesMock).toHaveBeenCalledWith({ - fileRule: [{ selector: "variable" }], + rules: [{ scope: "fileRoot", selector: "variable" }], name: "componentName", nodeType: "VariableDeclarator", node: {}, - report: reportMock, filenamePath: path.relative( "C:/somePath", "C:/somePath/src/features/Feature1/Feature1.ts", ), errorMessageId: "prohibitedSelectorRoot", + scope: "fileRoot", + context: { + report: reportMock, + settings: {}, + cwd: "C:/somePath", + filename: "C:/somePath/src/features/Feature1/Feature1.ts", + }, + expressionName: undefined, + allowOnlySpecifiedSelectors: undefined, + regexParameters: undefined, }); }); - test("Should call fileExportRules for fileRules", () => { + test("Should call fileRules", () => { const validateRulesMock = jest.fn(); const reportMock = jest.fn(); @@ -191,31 +194,45 @@ describe("validateFile", () => { filesRules: [ { filePattern: "**/*.ts", - fileRootRules: [{ selector: "variable" }], - fileExportRules: [{ selector: "variable" }], - fileRules: [{ selector: "variable" }], + rules: [ + { scope: "file", selector: "variable" }, + { selector: "variable" }, + ], }, ], }, fileConfig: { filePattern: "**/*.ts", - fileRootRules: [{ selector: "variable" }], - fileExportRules: [{ selector: "variable" }], - fileRules: [{ selector: "variable" }], + rules: [ + { scope: "file", selector: "variable" }, + { selector: "variable" }, + ], }, }); expect(validateRulesMock).toHaveBeenCalledWith({ - fileRule: [{ selector: "variable" }], + rules: [ + { scope: "file", selector: "variable" }, + { selector: "variable" }, + ], name: "componentName", nodeType: "VariableDeclarator", node: {}, - report: reportMock, filenamePath: path.relative( "C:/somePath", "C:/somePath/src/features/Feature1/Feature1.ts", ), errorMessageId: "prohibitedSelector", + scope: "file", + context: { + report: reportMock, + settings: {}, + cwd: "C:/somePath", + filename: "C:/somePath/src/features/Feature1/Feature1.ts", + }, + expressionName: undefined, + allowOnlySpecifiedSelectors: undefined, + regexParameters: undefined, }); }); }); diff --git a/src/rules/fileComposition/helpers/validateFile/validateFile.ts b/src/rules/fileComposition/helpers/validateFile/validateFile.ts index 989067d..09d8c10 100644 --- a/src/rules/fileComposition/helpers/validateFile/validateFile.ts +++ b/src/rules/fileComposition/helpers/validateFile/validateFile.ts @@ -24,7 +24,8 @@ export interface ValidateFileProps { export const validateFile = ({ name, expressionName, - context: { report, cwd, filename }, + context, + context: { cwd, filename }, node, nodeType, fileConfig, @@ -32,31 +33,32 @@ export const validateFile = ({ }: ValidateFileProps): void => { if (!fileConfig) return; + const { rules, allowOnlySpecifiedSelectors } = fileConfig; + const fileExportRules = rules?.filter(({ scope }) => scope === "fileExport"); + const fileRootRules = rules?.filter(({ scope }) => scope === "fileRoot"); + const fileRules = rules?.filter(({ scope }) => scope === "file" || !scope); + const filenamePath = path.relative(cwd, filename); + const regexParameters = config.regexParameters; + const { isExportName, currentName, currentNode } = isExportedName({ nodeType, node, name, }); - const filenamePath = path.relative(cwd, filename); - const regexParameters = config.regexParameters; - const { - fileExportRules: fileExportRules, - fileRootRules, - fileRules, - } = fileConfig; - - if (fileExportRules && isExportName) { + if (fileExportRules?.length && isExportName) { return validateRules({ - fileRule: fileExportRules, + rules: fileExportRules, name: currentName, nodeType, node: currentNode, - report, + context, filenamePath, errorMessageId: "prohibitedSelectorExport", regexParameters, expressionName, + allowOnlySpecifiedSelectors, + scope: "fileExport", }); } @@ -65,31 +67,35 @@ export const validateFile = ({ node, }); - if (fileRootRules && isFileRootName) { + if (fileRootRules?.length && isFileRootName) { return validateRules({ - fileRule: fileRootRules, + rules: fileRootRules, name, nodeType, node, - report, + context, filenamePath, errorMessageId: "prohibitedSelectorRoot", regexParameters, expressionName, + allowOnlySpecifiedSelectors, + scope: "fileRoot", }); } - if (fileRules) { + if (fileRules?.length) { return validateRules({ - fileRule: fileRules, + rules: fileRules, name, nodeType, node, - report, filenamePath, errorMessageId: "prohibitedSelector", regexParameters, expressionName, + allowOnlySpecifiedSelectors, + scope: "file", + context, }); } };