@polariens/kitsune-lint 1.0.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,216 @@
1
+ import eslint from '@eslint/js';
2
+ import tseslint from 'typescript-eslint';
3
+
4
+ import noNullInTypes from '../rules/no-null-in-types.mjs';
5
+ import { IGNORE_PATTERNS, resolveFiles } from '../utils.mjs';
6
+
7
+ /**
8
+ * @typedef {Object} TypescriptOptions
9
+ * @property {string[]} [files] - File patterns override
10
+ * @property {string[]} [ignores] - Patterns a ignorar
11
+ * @property {Record<string, unknown>} [rules] - Regras adicionais ou overrides
12
+ */
13
+
14
+ /**
15
+ * Configuração TypeScript com regras recomendadas e naming conventions.
16
+ * @param {TypescriptOptions} [options={}]
17
+ * @returns {import('eslint').Linter.Config[]}
18
+ */
19
+ export function typescript(options = {}) {
20
+ const { files, ignores = [], replaceIgnores, rules: extraRules = {} } = options;
21
+ const resolvedFiles = resolveFiles('all', files);
22
+
23
+ return [
24
+ {
25
+ ignores: replaceIgnores ?? [
26
+ '**/*.config.{js,mjs,cjs,ts}',
27
+ '**/.prettierrc.*',
28
+ '**/_/*',
29
+ '**/dist/',
30
+ '**/.vitepress/',
31
+ '.rules/',
32
+ '.idea',
33
+ '.npm',
34
+ '.github',
35
+ '.vscode',
36
+ '.quasar',
37
+ '.coverage',
38
+ '.husky',
39
+ ...IGNORE_PATTERNS,
40
+ ...ignores,
41
+ ],
42
+ },
43
+ {
44
+ ...eslint.configs.recommended,
45
+ files: resolvedFiles,
46
+ },
47
+ ...tseslint.configs.recommended.map((config) => ({
48
+ ...config,
49
+ files: resolvedFiles,
50
+ })),
51
+ {
52
+ plugins: {
53
+ kitsune: {
54
+ rules: {
55
+ 'no-null-in-types': noNullInTypes,
56
+ },
57
+ },
58
+ },
59
+ },
60
+ {
61
+ files: resolvedFiles,
62
+ name: '@kitsune/typescript/rules',
63
+ plugins: {
64
+ '@typescript-eslint': tseslint.plugin,
65
+ },
66
+ languageOptions: {
67
+ parser: tseslint.parser,
68
+ parserOptions: {
69
+ projectService: true,
70
+ },
71
+ },
72
+ rules: {
73
+ // Prefer `===` or `!==` (never == or !=)
74
+ eqeqeq: ['error', 'always'],
75
+ '@typescript-eslint/no-empty-object-type': [
76
+ 'error',
77
+ {
78
+ allowObjectTypes: 'never',
79
+ allowInterfaces: 'with-single-extends',
80
+ },
81
+ ],
82
+ '@typescript-eslint/naming-convention': [
83
+ 'error',
84
+ { selector: 'default', format: ['camelCase'] },
85
+ { selector: 'variable', format: ['camelCase', 'UPPER_CASE'] },
86
+ { selector: 'function', format: ['camelCase'] },
87
+ { selector: 'class', format: ['PascalCase'] },
88
+ {
89
+ selector: 'interface',
90
+ format: ['PascalCase'],
91
+ custom: { regex: '^(?!I[A-Z])(?!.*Interface$)(OAuth|[A-Z][a-z]).*$', match: true },
92
+ },
93
+ {
94
+ selector: 'typeAlias',
95
+ format: ['PascalCase'],
96
+ custom: { regex: '^(?!I[A-Z])(?!.*Type$)(OAuth|[A-Z][a-z]).*$', match: true },
97
+ },
98
+ {
99
+ // camelCase for properties of interfaces/types
100
+ selector: 'typeProperty',
101
+ format: ['camelCase'],
102
+ leadingUnderscore: 'allow',
103
+ trailingUnderscore: 'allow',
104
+ },
105
+ {
106
+ // camelCase for class members
107
+ selector: 'classProperty',
108
+ format: ['camelCase'],
109
+ leadingUnderscore: 'allow',
110
+ },
111
+ {
112
+ // camelCase for methods of classes
113
+ selector: 'classMethod',
114
+ format: ['camelCase'],
115
+ },
116
+ {
117
+ // PascalCase for parameters of generic types of only one char
118
+ selector: 'typeParameter',
119
+ format: ['PascalCase'],
120
+ custom: { regex: '^[A-Z]$', match: true },
121
+ },
122
+ // PascalCase for enums names
123
+ { selector: 'enum', format: ['PascalCase'] },
124
+ {
125
+ // UPPER_CASE for enum members
126
+ selector: 'enumMember',
127
+ format: null,
128
+ custom: { regex: `^['"]?[A-Z]+([-_][A-Z]+)*['"]?$`, match: true },
129
+ },
130
+ // camelCase for object literal properties
131
+ { selector: 'objectLiteralProperty', format: null },
132
+ // camelCase or PascalCase for imports
133
+ { selector: 'import', format: ['camelCase', 'PascalCase'] },
134
+ ],
135
+ '@typescript-eslint/no-explicit-any': 'error',
136
+ '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
137
+ '@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
138
+ '@typescript-eslint/no-non-null-assertion': 'warn',
139
+ '@typescript-eslint/array-type': 'error',
140
+ 'no-shadow': 'off',
141
+ '@typescript-eslint/no-shadow': 'warn',
142
+ // Block `console.log` only
143
+ 'no-console': ['error', { allow: ['warn', 'error', 'info'] }],
144
+ // Disallows concatenation of string literals that can be combined into a single literal (e.g., 'foo' + 'bar' should be 'foobar').
145
+ 'no-useless-concat': 'error',
146
+ 'no-unused-vars': [
147
+ 'error',
148
+ {
149
+ argsIgnorePattern: '^_',
150
+ varsIgnorePattern: '^_',
151
+ },
152
+ ],
153
+ '@typescript-eslint/no-unused-vars': [
154
+ 'error',
155
+ {
156
+ argsIgnorePattern: '^_',
157
+ varsIgnorePattern: '^_',
158
+ },
159
+ ],
160
+ 'no-restricted-imports': [
161
+ 'error',
162
+ {
163
+ patterns: [
164
+ {
165
+ regex: '^\\.\\.\\/.*',
166
+ message: 'Use o alias @/ ou #/ ao invés de imports relativos com ../',
167
+ },
168
+ ],
169
+ },
170
+ ],
171
+ 'sort-imports': [
172
+ 'error',
173
+ {
174
+ allowSeparatedGroups: true,
175
+ ignoreCase: false,
176
+ ignoreDeclarationSort: true,
177
+ ignoreMemberSort: false,
178
+ memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
179
+ },
180
+ ],
181
+ 'no-var': 'error',
182
+ 'prefer-const': 'error',
183
+ 'prefer-rest-params': 'error',
184
+ 'prefer-spread': 'error',
185
+ 'no-restricted-syntax': [
186
+ 'error',
187
+ { selector: 'ExportDefaultDeclaration', message: 'Prefer named exports' },
188
+ { selector: 'ImportDeclaration[specifiers.length = 0]', message: 'Empty imports are not allowed' },
189
+ ],
190
+ 'no-duplicate-imports': 'error',
191
+ // No null in types/interfaces (allowed only with `Api` term)
192
+ 'kitsune/no-null-in-types': [
193
+ 'warn',
194
+ {
195
+ skipPattern: ['Api'],
196
+ },
197
+ ],
198
+ ...extraRules,
199
+ },
200
+ },
201
+ {
202
+ // Disable naming convention rule for env.d.ts files
203
+ files: ['**/env.d.ts'],
204
+ rules: {
205
+ '@typescript-eslint/naming-convention': 'off',
206
+ },
207
+ },
208
+ {
209
+ // Disable export/import default in router plugin and config files
210
+ files: ['src/router/*.ts', '**/*.d.{ts,js}', '**/*.config.{ts,js}', '.*/**/*.{ts,js}'],
211
+ rules: {
212
+ 'no-restricted-syntax': 'off',
213
+ },
214
+ },
215
+ ];
216
+ }
@@ -0,0 +1,20 @@
1
+ import type { Linter } from 'eslint';
2
+
3
+ export type VitestFn = 'test' | 'it';
4
+
5
+ export interface VitestOptions {
6
+ /** File patterns override */
7
+ files?: string[];
8
+ /** Regex para validação de títulos de testes @default Gherkin PT-BR */
9
+ titlePattern?: string;
10
+ /** Mensagem de erro para títulos inválidos */
11
+ titleMessage?: string;
12
+ /** Função de teste preferida @default 'test' */
13
+ fn?: VitestFn;
14
+ /** Máximo de describe aninhados @default 3 */
15
+ maxNestedDescribe?: number;
16
+ /** Regras adicionais ou overrides */
17
+ rules?: Partial<Linter.RulesRecord>;
18
+ }
19
+
20
+ export declare function vitest(options?: VitestOptions): Promise<Linter.Config[]>;
@@ -0,0 +1,76 @@
1
+ import { resolveFiles } from '../utils.mjs';
2
+
3
+ /**
4
+ * @typedef {Object} VitestOptions
5
+ * @property {string[]} [files] - File patterns override
6
+ * @property {string} [titlePattern] - Regex para validação de títulos de testes
7
+ * @property {string} [titleMessage] - Mensagem de erro para títulos inválidos
8
+ * @property {'test' | 'it'} [fn='test'] - Função de teste preferida
9
+ * @property {number} [maxNestedDescribe=3] - Máximo de describe aninhados
10
+ * @property {Record<string, unknown>} [rules] - Regras adicionais ou overrides
11
+ */
12
+
13
+ // Padrões de título de teste em português.
14
+ // Gherkin padrão (Dado, Quando, Então) e também "Deve ...".
15
+ const GHERKIN_PT = '^Dado[^\\n]+(?:\\n\\s*(?:E|Mas)\\b[^\\n]+)*\\n\\s*Quando[^\\n]+(?:\\n\\s*(?:E|Mas)\\b[^\\n]+)*\\n\\s*Então[^\\n]+';
16
+ const DEVE_PT = '^Deve\\s.+$';
17
+ // Mensagem combinada para ambos os padrões.
18
+ const TITLE_PATTERN_MESSAGE =
19
+ 'O título do test() deve seguir o padrão Gherkin em português ("Dado ...\\nQuando ...\\nEntão ...") ou iniciar com "Deve ..."';
20
+
21
+ /**
22
+ * Regras do plugin Vitest para padronização de testes.
23
+ * @param {VitestOptions} [options={}]
24
+ * @returns {Promise<import('eslint').Linter.Config[]>}
25
+ */
26
+ export async function vitest(options = {}) {
27
+ const {
28
+ files,
29
+ // Aceita string ou array de strings para múltiplos padrões.
30
+ titlePattern = [GHERKIN_PT, DEVE_PT],
31
+ titleMessage = TITLE_PATTERN_MESSAGE,
32
+ fn = 'test',
33
+ maxNestedDescribe = 3,
34
+ rules: extraRules = {},
35
+ } = options;
36
+
37
+ const vitestPlugin = await import('@vitest/eslint-plugin').then((m) => m.default ?? m);
38
+
39
+ return [
40
+ {
41
+ files: resolveFiles('tests', files),
42
+ name: '@kitsune/vitest/rules',
43
+ plugins: { vitest: vitestPlugin },
44
+ rules: {
45
+ ...vitestPlugin.configs.recommended.rules,
46
+ 'vitest/consistent-test-filename': 'error',
47
+ 'vitest/consistent-test-it': ['error', { fn }],
48
+ // Constrói o padrão combinado (string ou array) para a regra.
49
+ 'vitest/valid-title': [
50
+ 'error',
51
+ {
52
+ mustMatch: {
53
+ [fn]: [
54
+ // Se for array, une com "|" para regex alternativo.
55
+ Array.isArray(titlePattern) ? titlePattern.join('|') : titlePattern,
56
+ titleMessage,
57
+ ],
58
+ },
59
+ },
60
+ ],
61
+ 'vitest/require-top-level-describe': 'error',
62
+ 'vitest/no-identical-title': 'error',
63
+ 'vitest/no-focused-tests': 'error',
64
+ 'vitest/no-disabled-tests': 'warn',
65
+ 'vitest/no-duplicate-hooks': 'error',
66
+ 'vitest/prefer-hooks-on-top': 'error',
67
+ 'vitest/prefer-hooks-in-order': 'error',
68
+ 'vitest/prefer-to-be': 'error',
69
+ 'vitest/prefer-each': 'error',
70
+ 'vitest/no-mocks-import': 'off',
71
+ 'vitest/max-nested-describe': ['error', { max: maxNestedDescribe }],
72
+ ...extraRules,
73
+ },
74
+ },
75
+ ];
76
+ }
@@ -0,0 +1,25 @@
1
+ import type { Linter } from 'eslint';
2
+
3
+ export type VueApiStyle = 'script-setup' | 'composition' | 'options';
4
+
5
+ export type NameCasing = 'camelCase' | 'PascalCase' | 'kebab-case' | 'snake_case';
6
+
7
+
8
+ export interface VueOptions {
9
+ /** File patterns override */
10
+ files?: string[];
11
+ /** Estilo de API Vue preferido @default 'script-setup' */
12
+ apiStyle?: VueApiStyle;
13
+ /** Nomeclatura para nomes de componentes @default 'PascalCase' */
14
+ componentsNameCasing?: NameCasing;
15
+ /** Padrões de componentes ignorados para validação de nomenclatura @default 'PascalCase' */
16
+ componentsNameCasingIgnores?: string[];
17
+ /** Nomeclatura para nomes de propriedades @default 'camelCase' */
18
+ propNameCasing?: NameCasing;
19
+ /** Nomeclatura para nomes de slots @default 'kebab-case' */
20
+ slotNameCasing?: NameCasing;
21
+ /** Regras adicionais ou overrides */
22
+ rules?: Partial<Linter.RulesRecord>;
23
+ }
24
+
25
+ export declare function vue(options?: VueOptions): Linter.Config[];
@@ -0,0 +1,73 @@
1
+ import pluginVue from 'eslint-plugin-vue';
2
+ import tseslint from 'typescript-eslint';
3
+ import vueParser from 'vue-eslint-parser';
4
+
5
+ import { resolveFiles } from '../utils.mjs';
6
+
7
+ /**
8
+ * @typedef {Object} VueOptions
9
+ * @property {string[]} [files] - File patterns override
10
+ * @property {'script-setup' | 'composition' | 'options'} [apiStyle='script-setup']
11
+ * @property {Record<string, unknown>} [rules] - Regras adicionais ou overrides
12
+ */
13
+
14
+ /**
15
+ * Regras para projetos Vue 3 com TypeScript.
16
+ * @param {VueOptions} [options={}]
17
+ * @returns {import('eslint').Linter.Config[]}
18
+ */
19
+ export function vue(options = {}) {
20
+ const {
21
+ files,
22
+ apiStyle = 'script-setup',
23
+ rules: extraRules = {},
24
+ componentsNameCasing = 'PascalCase',
25
+ componentsNameCasingIgnores = [],
26
+ propNameCasing = 'camelCase',
27
+ slotNameCasing = 'kebab-case',
28
+ } = options;
29
+
30
+ return [
31
+ ...pluginVue.configs['flat/recommended'],
32
+ {
33
+ files: resolveFiles('vue', files),
34
+ name: '@kitsune/vue/rules',
35
+ languageOptions: {
36
+ parser: vueParser,
37
+ parserOptions: {
38
+ parser: tseslint.parser,
39
+ extraFileExtensions: ['.vue'],
40
+ projectService: true,
41
+ },
42
+ },
43
+ rules: {
44
+ 'no-undef': 'off',
45
+ '@typescript-eslint/explicit-function-return-type': 'off',
46
+ 'vue/block-lang': ['error', { script: { lang: 'ts' } }],
47
+ 'vue/block-order': ['error', { order: ['script', 'template', 'style'] }],
48
+ 'vue/block-tag-newline': 'error',
49
+ 'vue/component-api-style': ['error', [apiStyle]],
50
+ 'vue/define-props-declaration': ['error', 'type-based'],
51
+ 'vue/define-emits-declaration': ['error', 'type-based'],
52
+ 'vue/no-setup-props-reactivity-loss': 'error',
53
+ 'vue/no-undef-properties': 'error',
54
+ 'vue/no-unused-emit-declarations': 'error',
55
+ 'vue/no-useless-v-bind': 'error',
56
+ 'vue/padding-line-between-blocks': ['error', 'always'],
57
+ 'vue/no-static-inline-styles': 'error',
58
+ 'vue/require-typed-ref': 'error',
59
+ 'vue/prop-name-casing': ['error', propNameCasing],
60
+ 'vue/slot-name-casing': ['error', slotNameCasing],
61
+ 'vue/component-name-in-template-casing': [
62
+ 'error',
63
+ componentsNameCasing,
64
+ {
65
+ registeredComponentsOnly: false,
66
+ ignores: componentsNameCasingIgnores,
67
+ },
68
+ ],
69
+ ...extraRules,
70
+ },
71
+ },
72
+ ];
73
+ }
@@ -0,0 +1,51 @@
1
+ import type { Linter } from 'eslint';
2
+
3
+ import type { BaseOptions } from './configs/base.mjs';
4
+ import type { CleanCodeOptions } from './configs/clean-code.mjs';
5
+ import type { PiniaOptions } from './configs/pinia.mjs';
6
+ import type { SecurityOptions } from './configs/security.mjs';
7
+ import type { TestsOptions } from './configs/tests.mjs';
8
+ import type { TypescriptOptions } from './configs/typescript.mjs';
9
+ import type { VitestOptions } from './configs/vitest.mjs';
10
+ import type { VueOptions } from './configs/vue.mjs';
11
+
12
+ export type { BaseOptions } from './configs/base.mjs';
13
+ export type { CleanCodeOptions } from './configs/clean-code.mjs';
14
+ export type { PiniaOptions } from './configs/pinia.mjs';
15
+ export type { SecurityOptions } from './configs/security.mjs';
16
+ export type { TestsOptions } from './configs/tests.mjs';
17
+ export type { TypescriptOptions } from './configs/typescript.mjs';
18
+ export type { VitestFn, VitestOptions } from './configs/vitest.mjs';
19
+ export type { VueApiStyle, VueOptions } from './configs/vue.mjs';
20
+
21
+ export { base } from './configs/base.mjs';
22
+ export { cleanCode } from './configs/clean-code.mjs';
23
+ export { pinia } from './configs/pinia.mjs';
24
+ export { security } from './configs/security.mjs';
25
+ export { tests } from './configs/tests.mjs';
26
+ export { typescript } from './configs/typescript.mjs';
27
+ export { vitest } from './configs/vitest.mjs';
28
+ export { vue } from './configs/vue.mjs';
29
+
30
+ export interface CreateKitsuneConfigOptions {
31
+ /** Configuração base (globals do ambiente) @default true */
32
+ base?: BaseOptions | boolean;
33
+ /** Regras TypeScript + naming conventions @default true */
34
+ typescript?: TypescriptOptions | boolean;
35
+ /** Regras de segurança (eval, XSS, injection) @default true */
36
+ security?: SecurityOptions | boolean;
37
+ /** Regras de clean code (SRP, legibilidade, imutabilidade) @default true */
38
+ cleanCode?: CleanCodeOptions | boolean;
39
+ /** Regras Vue 3 (script setup, block order) @default false */
40
+ vue?: VueOptions | boolean;
41
+ /** Regras Pinia (naming, organização de stores) @default false */
42
+ pinia?: PiniaOptions | boolean;
43
+ /** Relaxamentos para arquivos de teste @default false */
44
+ tests?: TestsOptions | boolean;
45
+ /** Regras Vitest (plugin vitest) @default false */
46
+ vitest?: VitestOptions | boolean;
47
+ /** Configs adicionais a incluir no final (ex: eslint-config-prettier) */
48
+ extend?: Linter.Config[];
49
+ }
50
+
51
+ export declare function createKitsuneConfig(options?: CreateKitsuneConfigOptions): Promise<Linter.Config[]>;
@@ -0,0 +1,85 @@
1
+ export { base } from './configs/base.mjs';
2
+ export { cleanCode } from './configs/clean-code.mjs';
3
+ export { pinia } from './configs/pinia.mjs';
4
+ export { security } from './configs/security.mjs';
5
+ export { tests } from './configs/tests.mjs';
6
+ export { typescript } from './configs/typescript.mjs';
7
+ export { vitest } from './configs/vitest.mjs';
8
+ export { vue } from './configs/vue.mjs';
9
+
10
+ /**
11
+ * @typedef {Object} CreateKitsuneConfigOptions
12
+ * @property {import('./configs/base.mjs').BaseOptions | boolean} [base=true]
13
+ * @property {import('./configs/typescript.mjs').TypescriptOptions | boolean} [typescript=true]
14
+ * @property {import('./configs/security.mjs').SecurityOptions | boolean} [security=true]
15
+ * @property {import('./configs/clean-code.mjs').CleanCodeOptions | boolean} [cleanCode=true]
16
+ * @property {import('./configs/vue.mjs').VueOptions | boolean} [vue=false]
17
+ * @property {import('./configs/pinia.mjs').PiniaOptions | boolean} [pinia=false]
18
+ * @property {import('./configs/tests.mjs').TestsOptions | boolean} [tests=false]
19
+ * @property {import('./configs/vitest.mjs').VitestOptions | boolean} [vitest=false]
20
+ * @property {import('eslint').Linter.Config[]} [extend] - Configs adicionais a incluir no final
21
+ */
22
+
23
+ /**
24
+ * Factory que compõe uma configuração ESLint completa a partir de módulos selecionados.
25
+ *
26
+ * @example
27
+ * import { createKitsuneConfig } from '@polariens/kitsune-lint/eslint';
28
+ * export default await createKitsuneConfig();
29
+ *
30
+ * @example
31
+ * import { createKitsuneConfig } from '@polariens/kitsune-lint/eslint';
32
+ * export default await createKitsuneConfig({ vue: true, pinia: true, tests: true, vitest: true });
33
+ *
34
+ * @example
35
+ * import { createKitsuneConfig } from '@polariens/kitsune-lint/eslint';
36
+ * export default await createKitsuneConfig({
37
+ * base: { environment: 'node' },
38
+ * cleanCode: { maxDepth: 3, maxParams: 3, complexity: 10 },
39
+ * vue: { apiStyle: 'composition' },
40
+ * pinia: true,
41
+ * tests: true,
42
+ * vitest: true,
43
+ * });
44
+ *
45
+ * @param {CreateKitsuneConfigOptions} [options={}]
46
+ * @returns {Promise<import('eslint').Linter.Config[]>}
47
+ */
48
+ export async function createKitsuneConfig(options = {}) {
49
+ const {
50
+ base: baseOpt = true,
51
+ typescript: tsOpt = true,
52
+ security: secOpt = true,
53
+ cleanCode: cleanOpt = true,
54
+ vue: vueOpt = false,
55
+ pinia: piniaOpt = false,
56
+ tests: testsOpt = false,
57
+ vitest: vitestOpt = false,
58
+ extend = [],
59
+ } = options;
60
+
61
+ const configs = [];
62
+
63
+ const moduleMap = [
64
+ { opt: baseOpt, name: 'base', file: 'base' },
65
+ { opt: tsOpt, name: 'typescript', file: 'typescript' },
66
+ { opt: secOpt, name: 'security', file: 'security' },
67
+ { opt: cleanOpt, name: 'cleanCode', file: 'clean-code' },
68
+ { opt: vueOpt, name: 'vue', file: 'vue' },
69
+ { opt: piniaOpt, name: 'pinia', file: 'pinia' },
70
+ { opt: testsOpt, name: 'tests', file: 'tests' },
71
+ { opt: vitestOpt, name: 'vitest', file: 'vitest' },
72
+ ];
73
+
74
+ for (const { opt, name, file } of moduleMap) {
75
+ if (opt) {
76
+ const mod = await import(`./configs/${file}.mjs`);
77
+ const configArr = await mod[name](opt === true ? {} : opt);
78
+ configs.push(...configArr);
79
+ }
80
+ }
81
+
82
+ configs.push(...extend);
83
+
84
+ return configs;
85
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Custom ESLint rule: disallow `null` in interfaces/types unless
3
+ * the interface/type name contains a specific string defined via
4
+ * configuration. Default is "Api".
5
+ *
6
+ * @file
7
+ */
8
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
9
+
10
+ export default {
11
+ meta: {
12
+ type: 'problem',
13
+ docs: {
14
+ description:
15
+ 'Disallow `null` in interfaces/types unless the name contains a configured string.',
16
+ category: 'Possible Errors',
17
+ recommended: false,
18
+ },
19
+ messages: {
20
+ noNull:
21
+ 'Type "{{name}}" uses `null`; only interfaces/types whose name contains "{{skipPattern}}" may contain `null`.',
22
+ },
23
+ schema: [
24
+ {
25
+ type: 'object',
26
+ properties: {
27
+ skipPattern: {
28
+ oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }],
29
+ description:
30
+ 'String or array of strings that, if present in the type name, skips the null check.',
31
+ default: 'Api',
32
+ },
33
+ },
34
+ additionalProperties: false,
35
+ },
36
+ ],
37
+ },
38
+
39
+ create(context) {
40
+ const options = context.options?.[0] ?? {};
41
+ let skipPatterns = options.skipPattern ?? 'Api';
42
+ if (!Array.isArray(skipPatterns)) {
43
+ skipPatterns = [skipPatterns];
44
+ }
45
+
46
+ /**
47
+ * Detects if a type node contains a `null` literal.
48
+ *
49
+ * @param {TSESTree.TSType} node
50
+ * @returns {boolean}
51
+ */
52
+ function containsNull(node) {
53
+ if (!node) return false;
54
+
55
+ switch (node.type) {
56
+ case AST_NODE_TYPES.TSNullKeyword:
57
+ return true;
58
+ case AST_NODE_TYPES.TSUnionType:
59
+ return node.types.some(containsNull);
60
+ case AST_NODE_TYPES.TSTypeAnnotation:
61
+ return containsNull(node.typeAnnotation);
62
+ case AST_NODE_TYPES.TSTypeLiteral:
63
+ // Check members only for object literal types
64
+ return node.members.some(
65
+ (m) =>
66
+ m.type === AST_NODE_TYPES.TSPropertySignature &&
67
+ containsNull(m.typeAnnotation?.typeAnnotation),
68
+ );
69
+ default:
70
+ return false;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Handles property signatures inside interfaces or type literals.
76
+ *
77
+ * @param {TSESTree.TSPropertySignature} prop
78
+ * @param {string} typeName
79
+ */
80
+ function checkProperty(prop, typeName) {
81
+ const typeNode = prop.typeAnnotation?.typeAnnotation;
82
+ if (typeNode && containsNull(typeNode)) {
83
+ context.report({
84
+ node: prop,
85
+ messageId: 'noNull',
86
+ data: { name: typeName, skipPattern: skipPatterns.join(', ') },
87
+ });
88
+ }
89
+ }
90
+
91
+ return {
92
+ TSInterfaceDeclaration(node) {
93
+ const name = node.id?.name;
94
+ if (!name || skipPatterns.some((pattern) => name.includes(pattern))) return; // skip based on config
95
+
96
+ node.body.body.forEach((member) => {
97
+ if (member.type === AST_NODE_TYPES.TSPropertySignature) {
98
+ checkProperty(member, name);
99
+ }
100
+ });
101
+ },
102
+
103
+ TSTypeAliasDeclaration(node) {
104
+ const name = node.id?.name;
105
+ if (!name || skipPatterns.some((pattern) => name.includes(pattern))) return; // skip based on config
106
+
107
+ if (node.typeAnnotation.type === 'TSTypeLiteral') {
108
+ node.typeAnnotation.members.forEach((member) => {
109
+ if (member.type === AST_NODE_TYPES.TSPropertySignature) {
110
+ checkProperty(member, name);
111
+ }
112
+ });
113
+ }
114
+ },
115
+ };
116
+ },
117
+ };
@@ -0,0 +1,7 @@
1
+ export type FilePatternKey = 'all' | 'tests' | 'vue' | 'pinia' | 'configs';
2
+
3
+ export declare const FILE_PATTERNS: Record<FilePatternKey, string[]>;
4
+
5
+ export declare const IGNORE_PATTERNS: string[];
6
+
7
+ export declare function resolveFiles(key: FilePatternKey, custom?: string[]): string[];