catlint 1.0.0
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.
- package/.editorconfig +13 -0
- package/.gitattributes +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +41 -0
- package/.github/pull_request_template.md +31 -0
- package/.github/workflows/security.yaml +22 -0
- package/.husky/pre-commit +2 -0
- package/.prettierignore +3 -0
- package/.prettierrc +8 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +38 -0
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/SECURITY.md +21 -0
- package/bun.lock +609 -0
- package/eslint.config.ts +45 -0
- package/legacy/group/model.ts +6 -0
- package/legacy/group/service.ts +13 -0
- package/legacy/index.ts +72 -0
- package/legacy/shared/yagni/index.ts +4 -0
- package/legacy/shared/yagni/noUnusedFeatures/index.ts +4 -0
- package/legacy/shared/yagni/noUnusedFeatures/main.ts +10 -0
- package/legacy/shared/yagni/noUnusedFeatures/noUnusedClasses.ts +26 -0
- package/legacy/shared/yagni/noUnusedFeatures/noUnusedFunctions.ts +26 -0
- package/legacy/shared/yagni/noUnusedFeatures/noUnusedVariables.ts +26 -0
- package/legacy/shared/yagni/utils.ts +5 -0
- package/package.json +51 -0
- package/src/cli/commands/init.ts +23 -0
- package/src/cli/commands/lint.ts +7 -0
- package/src/cli/index.ts +20 -0
- package/src/config/api/defineConfig.ts +10 -0
- package/src/config/api/loadConfig.ts +27 -0
- package/src/config/index.ts +3 -0
- package/src/config/types/config.ts +27 -0
- package/src/config/types/configAdapter.ts +10 -0
- package/src/core/api/lintProject.ts +20 -0
- package/src/core/api/linter.ts +22 -0
- package/src/core/models/lintResult/model.ts +8 -0
- package/src/ir/models/File.ts +7 -0
- package/src/ir/models/IRParser.ts +3 -0
- package/src/ir/models/Node.ts +14 -0
- package/src/rules/helpers/defaultAdapter.ts +20 -0
- package/src/rules/models/message/model.ts +11 -0
- package/src/rules/models/rule/model.ts +15 -0
- package/src/rules/models/rule/service.ts +14 -0
- package/src/shared/pattern/loadFiles.ts +17 -0
- package/tests/basic.test.ts +8 -0
- package/tsconfig.json +32 -0
- package/vitest.config.ts +12 -0
package/eslint.config.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
import globals from 'globals';
|
|
3
|
+
import tseslint from 'typescript-eslint';
|
|
4
|
+
import json from '@eslint/json';
|
|
5
|
+
import markdown from '@eslint/markdown';
|
|
6
|
+
import css from '@eslint/css';
|
|
7
|
+
import { defineConfig } from 'eslint/config';
|
|
8
|
+
import eslintConfigPrettier from 'eslint-config-prettier/flat';
|
|
9
|
+
|
|
10
|
+
export default defineConfig([
|
|
11
|
+
{
|
|
12
|
+
files: ['**/*.{js,mjs,cjs,ts,mts,cts}'],
|
|
13
|
+
plugins: { js },
|
|
14
|
+
extends: ['js/recommended'],
|
|
15
|
+
languageOptions: { globals: globals.browser },
|
|
16
|
+
},
|
|
17
|
+
tseslint.configs.recommended,
|
|
18
|
+
{
|
|
19
|
+
files: ['**/*.json'],
|
|
20
|
+
plugins: { json },
|
|
21
|
+
language: 'json/json',
|
|
22
|
+
extends: ['json/recommended'],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
files: ['**/*.jsonc'],
|
|
26
|
+
plugins: { json },
|
|
27
|
+
language: 'json/jsonc',
|
|
28
|
+
extends: ['json/recommended'],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
files: ['**/*.json5'],
|
|
32
|
+
plugins: { json },
|
|
33
|
+
language: 'json/json5',
|
|
34
|
+
extends: ['json/recommended'],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
files: ['**/*.md'],
|
|
38
|
+
plugins: { markdown },
|
|
39
|
+
language: 'markdown/gfm',
|
|
40
|
+
extends: ['markdown/recommended'],
|
|
41
|
+
},
|
|
42
|
+
{ files: ['**/*.css'], plugins: { css }, language: 'css/css', extends: ['css/recommended'] },
|
|
43
|
+
{ ignores: ['**/dist/**', '**/legacy/**', '**/node_modules/**'] },
|
|
44
|
+
eslintConfigPrettier,
|
|
45
|
+
]);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Rule } from '@rules/$/models/rule/model';
|
|
2
|
+
import type { Group } from './model';
|
|
3
|
+
|
|
4
|
+
export const createGroup = (
|
|
5
|
+
name: string,
|
|
6
|
+
method: (push: (rule: Rule) => void, subgroup: (group: Group) => void) => void,
|
|
7
|
+
): Group => {
|
|
8
|
+
const rules: Rule[] = [];
|
|
9
|
+
const push = (rule: Rule) => rules.push(rule);
|
|
10
|
+
const subgroup = (group: Group) => group.rules.forEach(push);
|
|
11
|
+
method(push, subgroup);
|
|
12
|
+
return { name, rules };
|
|
13
|
+
};
|
package/legacy/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { lintFile } from "@core/linter";
|
|
2
|
+
import type { LintResult } from "@core/models/lintResult/model";
|
|
3
|
+
import type { IRFile } from "@ir/$/models/File";
|
|
4
|
+
import { IRKind } from "@ir/$/models/Node";
|
|
5
|
+
import { yagniRules } from "@rules/yagni";
|
|
6
|
+
const file: IRFile = {
|
|
7
|
+
name: "test.ts",
|
|
8
|
+
extensions: ["ts"],
|
|
9
|
+
program: [
|
|
10
|
+
{
|
|
11
|
+
line: 1,
|
|
12
|
+
kind: IRKind.Codeline,
|
|
13
|
+
name: "test",
|
|
14
|
+
value: [
|
|
15
|
+
{
|
|
16
|
+
line: 2,
|
|
17
|
+
kind: IRKind.Function,
|
|
18
|
+
name: "test",
|
|
19
|
+
value: [
|
|
20
|
+
{
|
|
21
|
+
line: 3,
|
|
22
|
+
kind: IRKind.Variable,
|
|
23
|
+
name: "test",
|
|
24
|
+
value: [],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
line: 4,
|
|
32
|
+
kind: IRKind.Class,
|
|
33
|
+
name: "test2",
|
|
34
|
+
value: [],
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
line: 5,
|
|
39
|
+
kind: IRKind.Variable,
|
|
40
|
+
name: "test9",
|
|
41
|
+
value: [],
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
line: 6,
|
|
46
|
+
kind: IRKind.Literal,
|
|
47
|
+
name: "test",
|
|
48
|
+
value: [],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const rules = [yagniRules]
|
|
54
|
+
|
|
55
|
+
const lres = lintFile(file, rules);
|
|
56
|
+
|
|
57
|
+
console.log(JSON.stringify(lres, null, 2));
|
|
58
|
+
|
|
59
|
+
let indent = 0;
|
|
60
|
+
|
|
61
|
+
const print = (result: LintResult) => {
|
|
62
|
+
const char = result.isCorrect ? "✓" : "✗";
|
|
63
|
+
console.log(`${" ".repeat(indent)}${char} ${result.name}`);
|
|
64
|
+
indent += 2;
|
|
65
|
+
result.messages.forEach((message) => {
|
|
66
|
+
console.log(`${" ".repeat(indent)}[${message.level}] Line ${message.line}: ${message.message}`);
|
|
67
|
+
});
|
|
68
|
+
result.subrules.forEach(print);
|
|
69
|
+
indent -= 2;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
lres.forEach(print);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { noUnusedClasses } from './noUnusedClasses';
|
|
2
|
+
import { noUnusedFunctions } from './noUnusedFunctions';
|
|
3
|
+
import { noUnusedVariables } from './noUnusedVariables';
|
|
4
|
+
import { createRule } from '@rules/models/rule/service';
|
|
5
|
+
|
|
6
|
+
export const noUnusedFeatures = createRule('No unused features', () => [], [
|
|
7
|
+
noUnusedVariables,
|
|
8
|
+
noUnusedFunctions,
|
|
9
|
+
noUnusedClasses,
|
|
10
|
+
]);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { IRKind } from '@ir/models/Node';
|
|
2
|
+
import { Level, type Message } from '@rules/models/message/model';
|
|
3
|
+
import type { Rule } from '@rules/models/rule/model';
|
|
4
|
+
import { flattenNodes } from '../utils';
|
|
5
|
+
import { createRule } from '@rules/models/rule/service';
|
|
6
|
+
|
|
7
|
+
export const noUnusedClasses: Rule = createRule('No unused classes', (file) => {
|
|
8
|
+
const messages: Message[] = [];
|
|
9
|
+
const allNodes = flattenNodes(file.program);
|
|
10
|
+
const declaredClasses = allNodes.filter((n) => n.kind === IRKind.Class);
|
|
11
|
+
const referencedNames = new Set(
|
|
12
|
+
allNodes.filter((n) => n.kind !== IRKind.Class).map((n) => n.name),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
for (const cls of declaredClasses) {
|
|
16
|
+
if (!referencedNames.has(cls.name)) {
|
|
17
|
+
messages.push({
|
|
18
|
+
message: `Class '${cls.name}' is declared but never used.`,
|
|
19
|
+
line: cls.line,
|
|
20
|
+
level: Level.Error,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return messages;
|
|
26
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { IRKind } from '@ir/models/Node';
|
|
2
|
+
import { Level, type Message } from '@rules/models/message/model';
|
|
3
|
+
import type { Rule } from '@rules/models/rule/model';
|
|
4
|
+
import { flattenNodes } from '../utils';
|
|
5
|
+
import { createRule } from '@rules/models/rule/service';
|
|
6
|
+
|
|
7
|
+
export const noUnusedFunctions: Rule = createRule('No unused functions', (file) => {
|
|
8
|
+
const messages: Message[] = [];
|
|
9
|
+
const allNodes = flattenNodes(file.program);
|
|
10
|
+
const declaredFunctions = allNodes.filter((n) => n.kind === IRKind.Function);
|
|
11
|
+
const referencedNames = new Set(
|
|
12
|
+
allNodes.filter((n) => n.kind !== IRKind.Function).map((n) => n.name),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
for (const fn of declaredFunctions) {
|
|
16
|
+
if (!referencedNames.has(fn.name)) {
|
|
17
|
+
messages.push({
|
|
18
|
+
message: `Function '${fn.name}' is declared but never called.`,
|
|
19
|
+
line: fn.line,
|
|
20
|
+
level: Level.Error,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return messages;
|
|
26
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { IRKind } from '@ir/models/Node';
|
|
2
|
+
import { Level, type Message } from '@rules/models/message/model';
|
|
3
|
+
import type { Rule } from '@rules/models/rule/model';
|
|
4
|
+
import { flattenNodes } from '../utils';
|
|
5
|
+
import { createRule } from '@rules/models/rule/service';
|
|
6
|
+
|
|
7
|
+
export const noUnusedVariables: Rule = createRule('No unused variables', (file) => {
|
|
8
|
+
const messages: Message[] = [];
|
|
9
|
+
const allNodes = flattenNodes(file.program);
|
|
10
|
+
const declaredVariables = allNodes.filter((n) => n.kind === IRKind.Variable);
|
|
11
|
+
const referencedNames = new Set(
|
|
12
|
+
allNodes.filter((n) => n.kind !== IRKind.Variable).map((n) => n.name),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
for (const variable of declaredVariables) {
|
|
16
|
+
if (!referencedNames.has(variable.name)) {
|
|
17
|
+
messages.push({
|
|
18
|
+
message: `Variable '${variable.name}' is declared but never used.`,
|
|
19
|
+
line: variable.line,
|
|
20
|
+
level: Level.Error,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return messages;
|
|
26
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "catlint",
|
|
3
|
+
"module": "src/index.ts",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"lint": "eslint src --ext .ts",
|
|
8
|
+
"format": "prettier --write src",
|
|
9
|
+
"check": "bun run lint && bun run format",
|
|
10
|
+
"test": "vitest",
|
|
11
|
+
"prepare": "husky"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": "^5.3.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@eslint/css": "^1.0.0",
|
|
18
|
+
"@eslint/js": "^10.0.1",
|
|
19
|
+
"@eslint/json": "^1.2.0",
|
|
20
|
+
"@eslint/markdown": "^8.0.0",
|
|
21
|
+
"@types/bun": "latest",
|
|
22
|
+
"@types/node": "^25.5.0",
|
|
23
|
+
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
|
24
|
+
"@typescript-eslint/parser": "^8.57.2",
|
|
25
|
+
"eslint": "^10.1.0",
|
|
26
|
+
"eslint-config-prettier": "^10.1.8",
|
|
27
|
+
"globals": "^17.4.0",
|
|
28
|
+
"husky": "^9.1.7",
|
|
29
|
+
"prettier": "3.8.1",
|
|
30
|
+
"typescript-eslint": "^8.57.2",
|
|
31
|
+
"vitest": "^4.1.1"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"typescript": "^5"
|
|
35
|
+
},
|
|
36
|
+
"lint-staged": {
|
|
37
|
+
"*.{ts,js}": [
|
|
38
|
+
"eslint --fix",
|
|
39
|
+
"prettier --write"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"catlint": ".",
|
|
44
|
+
"chalk": "^5.6.2",
|
|
45
|
+
"commander": "^14.0.3",
|
|
46
|
+
"glob": "^13.0.6",
|
|
47
|
+
"inquirer": "^13.3.2",
|
|
48
|
+
"jiti": "^2.6.1",
|
|
49
|
+
"minimatch": "^10.2.5"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { defaultConfig } from '@config/index';
|
|
4
|
+
import { inspect } from 'util';
|
|
5
|
+
|
|
6
|
+
export const init = async () => {
|
|
7
|
+
const { useTypescript } = await inquirer.prompt([
|
|
8
|
+
{
|
|
9
|
+
type: 'confirm',
|
|
10
|
+
name: 'useTypescript',
|
|
11
|
+
message: 'Do you want to use typescript?',
|
|
12
|
+
default: true,
|
|
13
|
+
},
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
fs.writeFileSync(
|
|
17
|
+
`catlint.config.${useTypescript ? 'ts' : 'js'}`,
|
|
18
|
+
`import { defineConfig } from 'catlint/config';
|
|
19
|
+
|
|
20
|
+
export default defineConfig(${inspect(defaultConfig)});
|
|
21
|
+
`,
|
|
22
|
+
);
|
|
23
|
+
};
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { init } from './commands/init';
|
|
4
|
+
import { lint } from './commands/lint';
|
|
5
|
+
|
|
6
|
+
const program = new Command();
|
|
7
|
+
|
|
8
|
+
program.name('catlint').description('A linter for detecting code bad practices').version('0.0.1');
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.command('init')
|
|
12
|
+
.description('Inits CatLint in your current project folder')
|
|
13
|
+
.action(async () => await init());
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.command('lint')
|
|
17
|
+
.description('Lints the current project')
|
|
18
|
+
.action(async () => await lint());
|
|
19
|
+
|
|
20
|
+
program.parse();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Config, NormalizedConfig } from '@config/types/config';
|
|
2
|
+
import { defaultAdapter } from '@rules/helpers/defaultAdapter';
|
|
3
|
+
|
|
4
|
+
export function defineConfig(config: Config): NormalizedConfig {
|
|
5
|
+
const normalizedConfig: NormalizedConfig = {
|
|
6
|
+
...config,
|
|
7
|
+
rules: config.rules.map((rule) => defaultAdapter(rule)),
|
|
8
|
+
};
|
|
9
|
+
return normalizedConfig;
|
|
10
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { NormalizedConfig } from '@config/types/config';
|
|
2
|
+
import { adaptConfig } from '@config/types/configAdapter';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
5
|
+
|
|
6
|
+
export const loadConfigFromFile = async (filePath: string): Promise<NormalizedConfig | null> => {
|
|
7
|
+
try {
|
|
8
|
+
const config = await import(pathToFileURL(filePath).href);
|
|
9
|
+
return adaptConfig(config.default);
|
|
10
|
+
} catch {
|
|
11
|
+
console.log(`No config file found at ${filePath}`);
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const loadConfig = async (projectPath: string): Promise<NormalizedConfig> => {
|
|
17
|
+
const configPath = path.join(process.cwd(), projectPath) + '/catlint.config';
|
|
18
|
+
|
|
19
|
+
const config =
|
|
20
|
+
(await loadConfigFromFile(configPath + '.js')) ??
|
|
21
|
+
(await loadConfigFromFile(configPath + '.ts'));
|
|
22
|
+
if (!config) {
|
|
23
|
+
throw new Error('No config file found');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return config;
|
|
27
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IRParser } from '@ir/models/IRParser';
|
|
2
|
+
import type { AdaptedRule, Rule } from '@rules/models/rule/model';
|
|
3
|
+
|
|
4
|
+
export interface Config {
|
|
5
|
+
lint: {
|
|
6
|
+
includes: string[];
|
|
7
|
+
excludes: string[];
|
|
8
|
+
};
|
|
9
|
+
parsers: {
|
|
10
|
+
pattern: string;
|
|
11
|
+
parser: IRParser;
|
|
12
|
+
}[];
|
|
13
|
+
rules: Rule[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface NormalizedConfig extends Config {
|
|
17
|
+
rules: AdaptedRule[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const defaultConfig: NormalizedConfig = {
|
|
21
|
+
lint: {
|
|
22
|
+
includes: ['**/*.ts'],
|
|
23
|
+
excludes: ['node_modules/**'],
|
|
24
|
+
},
|
|
25
|
+
parsers: [],
|
|
26
|
+
rules: [],
|
|
27
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defaultAdapter } from '@rules/helpers/defaultAdapter';
|
|
2
|
+
import type { Config, NormalizedConfig } from './config';
|
|
3
|
+
|
|
4
|
+
export const adaptConfig: (c: Config) => NormalizedConfig = (config: Config) => {
|
|
5
|
+
const normalizedConfig: NormalizedConfig = {
|
|
6
|
+
...config,
|
|
7
|
+
rules: config.rules.map((rule) => defaultAdapter(rule)),
|
|
8
|
+
};
|
|
9
|
+
return normalizedConfig;
|
|
10
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { NormalizedConfig } from '@config/types/config';
|
|
2
|
+
import { loadFiles } from 'src/shared/pattern/loadFiles';
|
|
3
|
+
import { lintFile } from './linter';
|
|
4
|
+
import type { IRFile } from '@ir/models/File';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { Minimatch } from 'minimatch';
|
|
7
|
+
|
|
8
|
+
export const lintProject = async (config: NormalizedConfig, projectDir: string) => {
|
|
9
|
+
const files = await loadFiles(projectDir, config.lint.includes, config.lint.excludes);
|
|
10
|
+
return files
|
|
11
|
+
.map((filePath: string) => {
|
|
12
|
+
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
13
|
+
const parser = config.parsers.find((parser) =>
|
|
14
|
+
new Minimatch(parser.pattern).match(filePath),
|
|
15
|
+
);
|
|
16
|
+
if (!parser) console.error(`No parser found for file ${filePath}`);
|
|
17
|
+
return parser?.parser(fileContent, filePath) ?? ({} as IRFile);
|
|
18
|
+
})
|
|
19
|
+
.map((file: IRFile) => lintFile(file, config.rules));
|
|
20
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LintResult } from '@core/models/lintResult/model';
|
|
2
|
+
import type { IRFile } from '@ir/models/File';
|
|
3
|
+
import type { AdaptedRule } from '@rules/models/rule/model';
|
|
4
|
+
|
|
5
|
+
export function travelFile(file: IRFile) {
|
|
6
|
+
const travelRule: (rule: AdaptedRule) => LintResult = (rule) => {
|
|
7
|
+
return {
|
|
8
|
+
name: rule.name,
|
|
9
|
+
messages: rule.fn(file),
|
|
10
|
+
subrules: rule.subrules.map(travelRule),
|
|
11
|
+
isCorrect:
|
|
12
|
+
rule.fn(file).length === 0 && rule.subrules.every((r) => travelRule(r).isCorrect),
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
return travelRule;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function lintFile(file: IRFile, rules: AdaptedRule[]): LintResult[] {
|
|
19
|
+
const results: LintResult[] = rules.map((rule) => travelFile(file)(rule));
|
|
20
|
+
|
|
21
|
+
return results;
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AdaptedRule, Rule } from '@rules/models/rule/model';
|
|
2
|
+
|
|
3
|
+
const EMPTY_FN = () => [];
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @function defaultAdapter recursively adapts a Rule to an AdaptedRule by providing default values for undefined/null fields.
|
|
7
|
+
*
|
|
8
|
+
* @param rule the rule to adapt
|
|
9
|
+
* @returns a AdaptedRule without undefined/null fields
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
export const defaultAdapter = (rule: Rule): AdaptedRule => {
|
|
13
|
+
const { name, fn = EMPTY_FN, subrules = [] } = rule;
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
name,
|
|
17
|
+
fn,
|
|
18
|
+
subrules: subrules.map(defaultAdapter),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IRFile } from '@ir/models/File';
|
|
2
|
+
import type { Message } from '@rules/models/message/model';
|
|
3
|
+
|
|
4
|
+
export type RuleFn = (file: IRFile) => Message[];
|
|
5
|
+
|
|
6
|
+
export interface Rule {
|
|
7
|
+
name: string;
|
|
8
|
+
fn?: RuleFn;
|
|
9
|
+
subrules?: Rule[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AdaptedRule extends Rule {
|
|
13
|
+
fn: RuleFn;
|
|
14
|
+
subrules: AdaptedRule[];
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Rule, RuleFn } from './model';
|
|
2
|
+
|
|
3
|
+
export const createRule = (
|
|
4
|
+
name: string,
|
|
5
|
+
fn: (subrule: (subrule: Rule) => void) => RuleFn,
|
|
6
|
+
): Rule => {
|
|
7
|
+
const subrules: Rule[] = [];
|
|
8
|
+
const ruleFn: RuleFn = fn((subrule: Rule) => subrules.push(subrule));
|
|
9
|
+
return {
|
|
10
|
+
name,
|
|
11
|
+
fn: ruleFn,
|
|
12
|
+
subrules,
|
|
13
|
+
};
|
|
14
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { glob } from 'glob';
|
|
2
|
+
|
|
3
|
+
export const loadFiles = async (baseDir: string, includes: string[], excludes: string[] = []) => {
|
|
4
|
+
const included = (
|
|
5
|
+
await Promise.all(
|
|
6
|
+
includes.map((pattern) => glob(pattern, { cwd: baseDir, absolute: true })),
|
|
7
|
+
)
|
|
8
|
+
).flat();
|
|
9
|
+
|
|
10
|
+
const excluded = (
|
|
11
|
+
await Promise.all(
|
|
12
|
+
excludes.map((pattern) => glob(pattern, { cwd: baseDir, absolute: true })),
|
|
13
|
+
)
|
|
14
|
+
).flat();
|
|
15
|
+
|
|
16
|
+
return included.filter((file) => !excluded.includes(file));
|
|
17
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": ".",
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
|
|
16
|
+
"strict": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"noFallthroughCasesInSwitch": true,
|
|
19
|
+
"noUncheckedIndexedAccess": true,
|
|
20
|
+
"noImplicitOverride": true,
|
|
21
|
+
|
|
22
|
+
"noUnusedLocals": true,
|
|
23
|
+
"noUnusedParameters": true,
|
|
24
|
+
|
|
25
|
+
"paths": {
|
|
26
|
+
"@core/*": ["./src/core/*"],
|
|
27
|
+
"@rules/*": ["./src/rules/*"],
|
|
28
|
+
"@ir/*": ["./src/ir/*"],
|
|
29
|
+
"@config/*": ["./src/config/*"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
package/vitest.config.ts
ADDED