pi-rtk-optimizer 0.3.2 → 0.4.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/CHANGELOG.md +83 -49
- package/README.md +292 -283
- package/config/config.example.json +36 -35
- package/package.json +60 -59
- package/src/additional-coverage-test.ts +197 -0
- package/src/boolean-format.ts +3 -0
- package/src/command-rewriter-test.ts +160 -0
- package/src/command-rewriter.ts +594 -349
- package/src/config-modal-test.ts +168 -0
- package/src/config-modal.ts +613 -600
- package/src/config-store.ts +224 -217
- package/src/index-test.ts +54 -0
- package/src/index.ts +320 -291
- package/src/output-compactor-test.ts +334 -120
- package/src/output-compactor.ts +418 -343
- package/src/record-utils.ts +6 -0
- package/src/rewrite-bypass.ts +332 -0
- package/src/rewrite-pipeline-safety.ts +154 -0
- package/src/rewrite-rules.ts +255 -248
- package/src/runtime-guard-test.ts +42 -0
- package/src/runtime-guard.ts +14 -0
- package/src/techniques/build.ts +155 -155
- package/src/techniques/git.ts +229 -229
- package/src/techniques/index.ts +8 -16
- package/src/techniques/linter.ts +151 -161
- package/src/techniques/path-utils.ts +67 -0
- package/src/techniques/search.ts +67 -76
- package/src/techniques/source.ts +253 -230
- package/src/techniques/test-output.ts +172 -172
- package/src/test-helpers.ts +10 -0
- package/src/types-shims.d.ts +189 -131
- package/src/types.ts +103 -114
- package/src/compat-commands.ts +0 -207
package/src/techniques/linter.ts
CHANGED
|
@@ -1,161 +1,151 @@
|
|
|
1
|
-
import { matchesCommandPatterns, normalizeCommandForDetection } from "./command-detection.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/^(?:npx\s+)?
|
|
6
|
-
/^
|
|
7
|
-
/^
|
|
8
|
-
/^
|
|
9
|
-
/^
|
|
10
|
-
/^
|
|
11
|
-
/^
|
|
12
|
-
/^
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
if (/^
|
|
84
|
-
if (/^
|
|
85
|
-
if (/^
|
|
86
|
-
if (
|
|
87
|
-
if (
|
|
88
|
-
if (
|
|
89
|
-
return "
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
result += "
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const topRules = Array.from(fileRules.entries())
|
|
153
|
-
.sort((left, right) => right[1] - left[1])
|
|
154
|
-
.slice(0, 3);
|
|
155
|
-
for (const [rule, count] of topRules) {
|
|
156
|
-
result += ` ${rule} (${count})\n`;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return result;
|
|
161
|
-
}
|
|
1
|
+
import { matchesCommandPatterns, normalizeCommandForDetection } from "./command-detection.js";
|
|
2
|
+
import { compactPath } from "./path-utils.js";
|
|
3
|
+
|
|
4
|
+
const LINTER_COMMAND_PATTERNS = [
|
|
5
|
+
/^(?:pnpm\s+)?(?:npx\s+)?eslint\b/,
|
|
6
|
+
/^(?:npx\s+)?prettier\b/,
|
|
7
|
+
/^ruff\b/,
|
|
8
|
+
/^pylint\b/,
|
|
9
|
+
/^mypy\b/,
|
|
10
|
+
/^flake8\b/,
|
|
11
|
+
/^black\b/,
|
|
12
|
+
/^cargo\s+clippy\b/,
|
|
13
|
+
/^golangci-lint\b/,
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
interface Issue {
|
|
17
|
+
severity: "ERROR" | "WARNING";
|
|
18
|
+
rule: string;
|
|
19
|
+
file: string;
|
|
20
|
+
line?: number;
|
|
21
|
+
message: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isLinterCommand(command: string | undefined | null): boolean {
|
|
25
|
+
return matchesCommandPatterns(command, LINTER_COMMAND_PATTERNS);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseLine(line: string): Issue | null {
|
|
29
|
+
const fileLinePattern = /^(.+):(\d+):(\d+):\s*(.+)$/;
|
|
30
|
+
const rustPattern = /^(error|warning):\s*(.+?)\s+at\s+(.+):(\d+):(\d+)$/;
|
|
31
|
+
|
|
32
|
+
const fileLineMatch = line.match(fileLinePattern);
|
|
33
|
+
if (fileLineMatch) {
|
|
34
|
+
const file = fileLineMatch[1] ?? "unknown";
|
|
35
|
+
const lineNumber = Number.parseInt(fileLineMatch[2] ?? "0", 10);
|
|
36
|
+
const content = fileLineMatch[4] ?? line;
|
|
37
|
+
const severity = /warning/i.test(content) ? "WARNING" : "ERROR";
|
|
38
|
+
const rule = content.match(/\[(.+?)\]$/)?.[1] ?? "unknown";
|
|
39
|
+
return {
|
|
40
|
+
severity,
|
|
41
|
+
rule,
|
|
42
|
+
file,
|
|
43
|
+
line: Number.isNaN(lineNumber) ? undefined : lineNumber,
|
|
44
|
+
message: content,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const rustMatch = line.match(rustPattern);
|
|
49
|
+
if (rustMatch) {
|
|
50
|
+
const severity = (rustMatch[1]?.toUpperCase() ?? "ERROR") as "ERROR" | "WARNING";
|
|
51
|
+
const message = rustMatch[2] ?? line;
|
|
52
|
+
const file = rustMatch[3] ?? "unknown";
|
|
53
|
+
const lineNumber = Number.parseInt(rustMatch[4] ?? "0", 10);
|
|
54
|
+
return {
|
|
55
|
+
severity,
|
|
56
|
+
rule: "unknown",
|
|
57
|
+
file,
|
|
58
|
+
line: Number.isNaN(lineNumber) ? undefined : lineNumber,
|
|
59
|
+
message,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseIssues(output: string): Issue[] {
|
|
67
|
+
const issues: Issue[] = [];
|
|
68
|
+
for (const line of output.split("\n")) {
|
|
69
|
+
const parsed = parseLine(line);
|
|
70
|
+
if (parsed) {
|
|
71
|
+
issues.push(parsed);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return issues;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function detectLinterType(command: string | undefined | null): string {
|
|
78
|
+
const normalized = normalizeCommandForDetection(command);
|
|
79
|
+
if (!normalized) {
|
|
80
|
+
return "Linter";
|
|
81
|
+
}
|
|
82
|
+
if (/(?:^|\s)eslint\b/.test(normalized)) return "ESLint";
|
|
83
|
+
if (/^ruff\b/.test(normalized)) return "Ruff";
|
|
84
|
+
if (/^pylint\b/.test(normalized)) return "Pylint";
|
|
85
|
+
if (/^mypy\b/.test(normalized)) return "MyPy";
|
|
86
|
+
if (/^flake8\b/.test(normalized)) return "Flake8";
|
|
87
|
+
if (/clippy\b/.test(normalized)) return "Clippy";
|
|
88
|
+
if (/^golangci-lint\b/.test(normalized)) return "GolangCI-Lint";
|
|
89
|
+
if (/prettier\b/.test(normalized)) return "Prettier";
|
|
90
|
+
return "Linter";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function aggregateLinterOutput(output: string, command: string | undefined | null): string | null {
|
|
94
|
+
if (!isLinterCommand(command)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const linterType = detectLinterType(command);
|
|
99
|
+
const issues = parseIssues(output);
|
|
100
|
+
|
|
101
|
+
if (issues.length === 0) {
|
|
102
|
+
return `[OK] ${linterType}: No issues found`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const errors = issues.filter((issue) => issue.severity === "ERROR").length;
|
|
106
|
+
const warnings = issues.filter((issue) => issue.severity === "WARNING").length;
|
|
107
|
+
|
|
108
|
+
const byRule = new Map<string, number>();
|
|
109
|
+
for (const issue of issues) {
|
|
110
|
+
byRule.set(issue.rule, (byRule.get(issue.rule) ?? 0) + 1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const byFile = new Map<string, Issue[]>();
|
|
114
|
+
for (const issue of issues) {
|
|
115
|
+
const existing = byFile.get(issue.file) ?? [];
|
|
116
|
+
existing.push(issue);
|
|
117
|
+
byFile.set(issue.file, existing);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let result = `${linterType}: ${errors} errors, ${warnings} warnings in ${byFile.size} files\n`;
|
|
121
|
+
result += "═══════════════════════════════════════\n";
|
|
122
|
+
|
|
123
|
+
result += "Top rules:\n";
|
|
124
|
+
const sortedRules = Array.from(byRule.entries())
|
|
125
|
+
.sort((left, right) => right[1] - left[1])
|
|
126
|
+
.slice(0, 10);
|
|
127
|
+
for (const [rule, count] of sortedRules) {
|
|
128
|
+
result += ` ${rule} (${count}x)\n`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
result += "\nTop files:\n";
|
|
132
|
+
const sortedFiles = Array.from(byFile.entries())
|
|
133
|
+
.sort((left, right) => right[1].length - left[1].length)
|
|
134
|
+
.slice(0, 10);
|
|
135
|
+
|
|
136
|
+
for (const [file, fileIssues] of sortedFiles) {
|
|
137
|
+
result += ` ${compactPath(file, 40)} (${fileIssues.length} issues)\n`;
|
|
138
|
+
const fileRules = new Map<string, number>();
|
|
139
|
+
for (const issue of fileIssues) {
|
|
140
|
+
fileRules.set(issue.rule, (fileRules.get(issue.rule) ?? 0) + 1);
|
|
141
|
+
}
|
|
142
|
+
const topRules = Array.from(fileRules.entries())
|
|
143
|
+
.sort((left, right) => right[1] - left[1])
|
|
144
|
+
.slice(0, 3);
|
|
145
|
+
for (const [rule, count] of topRules) {
|
|
146
|
+
result += ` ${rule} (${count})\n`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function detectPathSeparator(path: string): "/" | "\\" {
|
|
2
|
+
return path.includes("\\") && !path.includes("/") ? "\\" : "/";
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function detectPathPrefix(path: string, separator: "/" | "\\"): string {
|
|
6
|
+
if (/^[A-Za-z]:[\\/]/.test(path)) {
|
|
7
|
+
return `${path.slice(0, 2)}${separator}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (path.startsWith("\\\\") || path.startsWith("//")) {
|
|
11
|
+
const parts = path.split(/[\\/]+/).filter((part) => part.length > 0);
|
|
12
|
+
if (parts.length >= 2) {
|
|
13
|
+
return `${separator}${separator}${parts[0]}${separator}${parts[1]}${separator}`;
|
|
14
|
+
}
|
|
15
|
+
return `${separator}${separator}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (path.startsWith("/") || path.startsWith("\\")) {
|
|
19
|
+
return separator;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function joinPathSegments(prefix: string, separator: "/" | "\\", segments: string[]): string {
|
|
26
|
+
if (segments.length === 0) {
|
|
27
|
+
return prefix || "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const joined = segments.join(separator);
|
|
31
|
+
return prefix ? `${prefix}${joined}` : joined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function compactPath(path: string, maxLength: number): string {
|
|
35
|
+
if (path.length <= maxLength) {
|
|
36
|
+
return path;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (maxLength < 2) {
|
|
40
|
+
return path.slice(0, maxLength);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const separator = detectPathSeparator(path);
|
|
44
|
+
const prefix = detectPathPrefix(path, separator);
|
|
45
|
+
const segments = path
|
|
46
|
+
.slice(prefix.length)
|
|
47
|
+
.split(/[\\/]+/)
|
|
48
|
+
.filter((segment) => segment.length > 0);
|
|
49
|
+
|
|
50
|
+
const lastSegment = segments[segments.length - 1] ?? path.slice(-(maxLength - 1));
|
|
51
|
+
const previousSegment = segments[segments.length - 2];
|
|
52
|
+
|
|
53
|
+
const candidates = [
|
|
54
|
+
joinPathSegments(prefix, separator, ["…", ...(previousSegment ? [previousSegment] : []), lastSegment]),
|
|
55
|
+
joinPathSegments("", separator, ["…", ...(previousSegment ? [previousSegment] : []), lastSegment]),
|
|
56
|
+
joinPathSegments("", separator, ["…", lastSegment]),
|
|
57
|
+
`…${path.slice(-(maxLength - 1))}`,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
for (const candidate of candidates) {
|
|
61
|
+
if (candidate.length <= maxLength) {
|
|
62
|
+
return candidate;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return `…${lastSegment.slice(-(maxLength - 1))}`;
|
|
67
|
+
}
|
package/src/techniques/search.ts
CHANGED
|
@@ -1,76 +1,67 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
outputText += "\n";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (results.length > shown) {
|
|
72
|
-
outputText += `... +${results.length - shown} more\n`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return outputText;
|
|
76
|
-
}
|
|
1
|
+
import { compactPath } from "./path-utils.js";
|
|
2
|
+
|
|
3
|
+
interface SearchResult {
|
|
4
|
+
file: string;
|
|
5
|
+
lineNumber: string;
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function groupSearchResults(output: string, maxResults = 50): string | null {
|
|
10
|
+
const results: SearchResult[] = [];
|
|
11
|
+
for (const line of output.split("\n")) {
|
|
12
|
+
if (!line.trim()) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const match = line.match(/^(.+?):(\d+)?:(.+)$/);
|
|
16
|
+
if (!match) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
results.push({
|
|
20
|
+
file: match[1] ?? "unknown",
|
|
21
|
+
lineNumber: match[2] ?? "?",
|
|
22
|
+
content: match[3] ?? "",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (results.length === 0) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const byFile = new Map<string, SearchResult[]>();
|
|
31
|
+
for (const result of results) {
|
|
32
|
+
const existing = byFile.get(result.file) ?? [];
|
|
33
|
+
existing.push(result);
|
|
34
|
+
byFile.set(result.file, existing);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let outputText = `${results.length} matches in ${byFile.size} files:\n\n`;
|
|
38
|
+
const sortedFiles = Array.from(byFile.entries()).sort((left, right) =>
|
|
39
|
+
left[0].localeCompare(right[0]),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
let shown = 0;
|
|
43
|
+
for (const [file, matches] of sortedFiles) {
|
|
44
|
+
if (shown >= maxResults) {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
outputText += `> ${compactPath(file, 50)} (${matches.length} matches):\n`;
|
|
48
|
+
for (const match of matches.slice(0, 10)) {
|
|
49
|
+
let cleaned = match.content.trim();
|
|
50
|
+
if (cleaned.length > 70) {
|
|
51
|
+
cleaned = `${cleaned.slice(0, 67)}...`;
|
|
52
|
+
}
|
|
53
|
+
outputText += ` ${match.lineNumber}: ${cleaned}\n`;
|
|
54
|
+
shown++;
|
|
55
|
+
}
|
|
56
|
+
if (matches.length > 10) {
|
|
57
|
+
outputText += ` +${matches.length - 10} more\n`;
|
|
58
|
+
}
|
|
59
|
+
outputText += "\n";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (results.length > shown) {
|
|
63
|
+
outputText += `... +${results.length - shown} more\n`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return outputText;
|
|
67
|
+
}
|