@claudiu-ceia/astkit 0.1.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/LICENSE +21 -0
- package/README.md +393 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/src/app.d.ts +1 -0
- package/dist/src/app.js +41 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +4 -0
- package/dist/src/code-rank/code-rank.d.ts +9 -0
- package/dist/src/code-rank/code-rank.js +71 -0
- package/dist/src/code-rank/index.d.ts +1 -0
- package/dist/src/code-rank/index.js +1 -0
- package/dist/src/code-rank/rank.d.ts +29 -0
- package/dist/src/code-rank/rank.js +185 -0
- package/dist/src/common/input.d.ts +5 -0
- package/dist/src/common/input.js +25 -0
- package/dist/src/nav/declarations.d.ts +19 -0
- package/dist/src/nav/declarations.js +106 -0
- package/dist/src/nav/definition.d.ts +14 -0
- package/dist/src/nav/definition.js +62 -0
- package/dist/src/nav/location.d.ts +6 -0
- package/dist/src/nav/location.js +35 -0
- package/dist/src/nav/references.d.ts +19 -0
- package/dist/src/nav/references.js +75 -0
- package/dist/src/patch/patch.d.ts +27 -0
- package/dist/src/patch/patch.js +345 -0
- package/dist/src/pattern/balance.d.ts +1 -0
- package/dist/src/pattern/balance.js +25 -0
- package/dist/src/pattern/index.d.ts +5 -0
- package/dist/src/pattern/index.js +4 -0
- package/dist/src/pattern/match.d.ts +2 -0
- package/dist/src/pattern/match.js +141 -0
- package/dist/src/pattern/render.d.ts +1 -0
- package/dist/src/pattern/render.js +32 -0
- package/dist/src/pattern/syntax.d.ts +3 -0
- package/dist/src/pattern/syntax.js +87 -0
- package/dist/src/pattern/types.d.ts +27 -0
- package/dist/src/pattern/types.js +1 -0
- package/dist/src/search/search.d.ts +16 -0
- package/dist/src/search/search.js +207 -0
- package/dist/src/service.d.ts +16 -0
- package/dist/src/service.js +72 -0
- package/dist/src/sgrep/index.d.ts +5 -0
- package/dist/src/sgrep/index.js +3 -0
- package/dist/src/sgrep/isomorphisms/expand.d.ts +2 -0
- package/dist/src/sgrep/isomorphisms/expand.js +51 -0
- package/dist/src/sgrep/isomorphisms/index.d.ts +3 -0
- package/dist/src/sgrep/isomorphisms/index.js +2 -0
- package/dist/src/sgrep/isomorphisms/registry.d.ts +2 -0
- package/dist/src/sgrep/isomorphisms/registry.js +8 -0
- package/dist/src/sgrep/isomorphisms/rules/commutative-binary.d.ts +2 -0
- package/dist/src/sgrep/isomorphisms/rules/commutative-binary.js +51 -0
- package/dist/src/sgrep/isomorphisms/rules/object-literal-property-order.d.ts +2 -0
- package/dist/src/sgrep/isomorphisms/rules/object-literal-property-order.js +82 -0
- package/dist/src/sgrep/isomorphisms/rules/redundant-parentheses.d.ts +2 -0
- package/dist/src/sgrep/isomorphisms/rules/redundant-parentheses.js +43 -0
- package/dist/src/sgrep/isomorphisms/template-ast.d.ts +2 -0
- package/dist/src/sgrep/isomorphisms/template-ast.js +59 -0
- package/dist/src/sgrep/isomorphisms/types.d.ts +15 -0
- package/dist/src/sgrep/isomorphisms/types.js +0 -0
- package/dist/src/sgrep/phases/output.d.ts +9 -0
- package/dist/src/sgrep/phases/output.js +11 -0
- package/dist/src/sgrep/phases/parse.d.ts +10 -0
- package/dist/src/sgrep/phases/parse.js +11 -0
- package/dist/src/sgrep/phases/search.d.ts +11 -0
- package/dist/src/sgrep/phases/search.js +111 -0
- package/dist/src/sgrep/sgrep.d.ts +3 -0
- package/dist/src/sgrep/sgrep.js +17 -0
- package/dist/src/sgrep/types.d.ts +32 -0
- package/dist/src/sgrep/types.js +3 -0
- package/dist/src/spatch/files.d.ts +7 -0
- package/dist/src/spatch/files.js +51 -0
- package/dist/src/spatch/index.d.ts +3 -0
- package/dist/src/spatch/index.js +2 -0
- package/dist/src/spatch/patch-document.d.ts +9 -0
- package/dist/src/spatch/patch-document.js +64 -0
- package/dist/src/spatch/phases/output.d.ts +9 -0
- package/dist/src/spatch/phases/output.js +15 -0
- package/dist/src/spatch/phases/parse.d.ts +11 -0
- package/dist/src/spatch/phases/parse.js +16 -0
- package/dist/src/spatch/phases/rewrite.d.ts +14 -0
- package/dist/src/spatch/phases/rewrite.js +111 -0
- package/dist/src/spatch/spatch.d.ts +3 -0
- package/dist/src/spatch/spatch.js +17 -0
- package/dist/src/spatch/template.d.ts +2 -0
- package/dist/src/spatch/template.js +1 -0
- package/dist/src/spatch/text.d.ts +5 -0
- package/dist/src/spatch/text.js +35 -0
- package/dist/src/spatch/types.d.ts +40 -0
- package/dist/src/spatch/types.js +20 -0
- package/package.json +69 -0
- package/skills/astkit-tooling/SKILL.md +101 -0
- package/skills/astkit-tooling/agents/openai.yaml +4 -0
- package/skills/astkit-tooling/references/cognitive-model.md +61 -0
- package/skills/astkit-tooling/references/non-goals.md +11 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type TextToken = {
|
|
2
|
+
kind: "text";
|
|
3
|
+
value: string;
|
|
4
|
+
};
|
|
5
|
+
export type EllipsisToken = {
|
|
6
|
+
kind: "ellipsis";
|
|
7
|
+
index: number;
|
|
8
|
+
};
|
|
9
|
+
export type HoleToken = {
|
|
10
|
+
kind: "hole";
|
|
11
|
+
name: string;
|
|
12
|
+
anonymous: boolean;
|
|
13
|
+
constraintSource: string | null;
|
|
14
|
+
constraintRegex: RegExp | null;
|
|
15
|
+
};
|
|
16
|
+
export type TemplateToken = TextToken | HoleToken | EllipsisToken;
|
|
17
|
+
export type CompiledTemplate = {
|
|
18
|
+
source: string;
|
|
19
|
+
tokens: TemplateToken[];
|
|
20
|
+
};
|
|
21
|
+
export type TemplateMatch = {
|
|
22
|
+
start: number;
|
|
23
|
+
end: number;
|
|
24
|
+
text: string;
|
|
25
|
+
captures: Record<string, string>;
|
|
26
|
+
};
|
|
27
|
+
export declare const ELLIPSIS_CAPTURE_PREFIX = "__ellipsis_";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ELLIPSIS_CAPTURE_PREFIX = "__ellipsis_";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ChalkInstance } from "chalk";
|
|
2
|
+
import type { SgrepResult } from "../sgrep/types.ts";
|
|
3
|
+
export type SearchCommandFlags = {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
"no-isomorphisms"?: boolean;
|
|
6
|
+
"no-color"?: boolean;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function runSearchCommand(patternInput: string, scope: string | undefined, flags: SearchCommandFlags): Promise<SgrepResult>;
|
|
10
|
+
type FormatSearchOutputOptions = {
|
|
11
|
+
color?: boolean;
|
|
12
|
+
chalkInstance?: ChalkInstance;
|
|
13
|
+
};
|
|
14
|
+
export declare function formatSearchOutput(result: SgrepResult, options?: FormatSearchOutputOptions): string;
|
|
15
|
+
export declare const searchCommand: import("@stricli/core").Command<import("@stricli/core").CommandContext>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { buildCommand } from "@stricli/core";
|
|
2
|
+
import chalk, { Chalk } from "chalk";
|
|
3
|
+
import { searchProject } from "../sgrep/sgrep.js";
|
|
4
|
+
export async function runSearchCommand(patternInput, scope, flags) {
|
|
5
|
+
return searchProject(patternInput, {
|
|
6
|
+
cwd: flags.cwd,
|
|
7
|
+
isomorphisms: !(flags["no-isomorphisms"] ?? false),
|
|
8
|
+
scope: scope ?? ".",
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export function formatSearchOutput(result, options = {}) {
|
|
12
|
+
if (result.files.length === 0) {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
const chalkInstance = buildChalk(options);
|
|
16
|
+
const useColor = chalkInstance.level > 0;
|
|
17
|
+
const captureColorMap = useColor
|
|
18
|
+
? buildCaptureColorMap(result)
|
|
19
|
+
: new Map();
|
|
20
|
+
const lines = [];
|
|
21
|
+
for (const file of result.files) {
|
|
22
|
+
lines.push(useColor ? chalkInstance.gray(`//${file.file}`) : `//${file.file}`);
|
|
23
|
+
for (const match of file.matches) {
|
|
24
|
+
const preview = buildMatchPreview(match.matched);
|
|
25
|
+
const linePrefix = useColor
|
|
26
|
+
? chalkInstance.gray(`${match.line}: `)
|
|
27
|
+
: `${match.line}: `;
|
|
28
|
+
const highlightedPreview = useColor
|
|
29
|
+
? highlightCaptures(preview.text, match.captures, chalkInstance, captureColorMap)
|
|
30
|
+
: preview.text;
|
|
31
|
+
const previewSuffix = preview.truncated
|
|
32
|
+
? useColor
|
|
33
|
+
? chalkInstance.gray(" ...")
|
|
34
|
+
: " ..."
|
|
35
|
+
: "";
|
|
36
|
+
lines.push(`${linePrefix}${highlightedPreview}${previewSuffix}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
41
|
+
function buildMatchPreview(matchedText) {
|
|
42
|
+
const normalized = matchedText.replaceAll("\r\n", "\n");
|
|
43
|
+
const firstLine = normalized.split("\n")[0] ?? "";
|
|
44
|
+
return {
|
|
45
|
+
text: firstLine,
|
|
46
|
+
truncated: normalized.includes("\n"),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function buildChalk(options) {
|
|
50
|
+
if (options.chalkInstance) {
|
|
51
|
+
return options.chalkInstance;
|
|
52
|
+
}
|
|
53
|
+
const shouldColor = options.color ?? false;
|
|
54
|
+
if (!shouldColor) {
|
|
55
|
+
return new Chalk({ level: 0 });
|
|
56
|
+
}
|
|
57
|
+
const level = chalk.level > 0 ? chalk.level : 1;
|
|
58
|
+
return new Chalk({ level });
|
|
59
|
+
}
|
|
60
|
+
function highlightCaptures(preview, captures, chalkInstance, captureColorMap) {
|
|
61
|
+
if (preview.length === 0) {
|
|
62
|
+
return preview;
|
|
63
|
+
}
|
|
64
|
+
const colorPalette = [
|
|
65
|
+
chalkInstance.cyan,
|
|
66
|
+
chalkInstance.green,
|
|
67
|
+
chalkInstance.yellow,
|
|
68
|
+
chalkInstance.magenta,
|
|
69
|
+
chalkInstance.blue,
|
|
70
|
+
chalkInstance.red,
|
|
71
|
+
];
|
|
72
|
+
const captureEntries = Object.entries(captures)
|
|
73
|
+
.map(([name, value]) => [name, toPreviewSearchValue(value)])
|
|
74
|
+
.filter(([name, value]) => name.length > 0 && value.length > 0);
|
|
75
|
+
if (captureEntries.length === 0) {
|
|
76
|
+
return preview;
|
|
77
|
+
}
|
|
78
|
+
const ranges = [];
|
|
79
|
+
const sortedEntries = [...captureEntries].sort((left, right) => right[1].length - left[1].length || left[0].localeCompare(right[0]));
|
|
80
|
+
for (let entryIndex = 0; entryIndex < sortedEntries.length; entryIndex += 1) {
|
|
81
|
+
const [name, value] = sortedEntries[entryIndex];
|
|
82
|
+
const colorIndex = captureColorMap.get(name) ?? (entryIndex % colorPalette.length);
|
|
83
|
+
let fromIndex = 0;
|
|
84
|
+
while (fromIndex < preview.length) {
|
|
85
|
+
const matchIndex = preview.indexOf(value, fromIndex);
|
|
86
|
+
if (matchIndex < 0) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
const matchEnd = matchIndex + value.length;
|
|
90
|
+
const overlaps = ranges.some((range) => matchIndex < range.end && range.start < matchEnd);
|
|
91
|
+
if (!overlaps) {
|
|
92
|
+
ranges.push({
|
|
93
|
+
start: matchIndex,
|
|
94
|
+
end: matchEnd,
|
|
95
|
+
colorIndex,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
fromIndex = matchIndex + value.length;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (ranges.length === 0) {
|
|
102
|
+
return preview;
|
|
103
|
+
}
|
|
104
|
+
ranges.sort((left, right) => left.start - right.start);
|
|
105
|
+
const parts = [];
|
|
106
|
+
let cursor = 0;
|
|
107
|
+
for (const range of ranges) {
|
|
108
|
+
if (range.start > cursor) {
|
|
109
|
+
parts.push(preview.slice(cursor, range.start));
|
|
110
|
+
}
|
|
111
|
+
const colorize = colorPalette[range.colorIndex] ?? chalkInstance.white;
|
|
112
|
+
parts.push(colorize(preview.slice(range.start, range.end)));
|
|
113
|
+
cursor = range.end;
|
|
114
|
+
}
|
|
115
|
+
if (cursor < preview.length) {
|
|
116
|
+
parts.push(preview.slice(cursor));
|
|
117
|
+
}
|
|
118
|
+
return parts.join("");
|
|
119
|
+
}
|
|
120
|
+
function toPreviewSearchValue(rawValue) {
|
|
121
|
+
const normalized = rawValue.replaceAll("\r\n", "\n");
|
|
122
|
+
return normalized.split("\n")[0] ?? "";
|
|
123
|
+
}
|
|
124
|
+
function buildCaptureColorMap(result) {
|
|
125
|
+
const captureNames = [];
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
for (const file of result.files) {
|
|
128
|
+
for (const match of file.matches) {
|
|
129
|
+
for (const name of Object.keys(match.captures)) {
|
|
130
|
+
if (seen.has(name)) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
seen.add(name);
|
|
134
|
+
captureNames.push(name);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
captureNames.sort((left, right) => left.localeCompare(right));
|
|
139
|
+
const captureColorMap = new Map();
|
|
140
|
+
for (let index = 0; index < captureNames.length; index += 1) {
|
|
141
|
+
const name = captureNames[index];
|
|
142
|
+
if (name) {
|
|
143
|
+
captureColorMap.set(name, index);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return captureColorMap;
|
|
147
|
+
}
|
|
148
|
+
export const searchCommand = buildCommand({
|
|
149
|
+
async func(flags, patternInput, scope) {
|
|
150
|
+
const result = await runSearchCommand(patternInput, scope, flags);
|
|
151
|
+
if (flags.json ?? false) {
|
|
152
|
+
this.process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const output = formatSearchOutput(result, {
|
|
156
|
+
color: Boolean(process.stdout.isTTY) && !(flags["no-color"] ?? false),
|
|
157
|
+
});
|
|
158
|
+
if (output.length > 0) {
|
|
159
|
+
this.process.stdout.write(`${output}\n`);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
parameters: {
|
|
163
|
+
flags: {
|
|
164
|
+
json: {
|
|
165
|
+
kind: "boolean",
|
|
166
|
+
optional: true,
|
|
167
|
+
brief: "Output structured JSON instead of compact text",
|
|
168
|
+
},
|
|
169
|
+
"no-color": {
|
|
170
|
+
kind: "boolean",
|
|
171
|
+
optional: true,
|
|
172
|
+
brief: "Disable colored output",
|
|
173
|
+
},
|
|
174
|
+
"no-isomorphisms": {
|
|
175
|
+
kind: "boolean",
|
|
176
|
+
optional: true,
|
|
177
|
+
brief: "Disable isomorphism expansion during matching",
|
|
178
|
+
},
|
|
179
|
+
cwd: {
|
|
180
|
+
kind: "parsed",
|
|
181
|
+
optional: true,
|
|
182
|
+
brief: "Working directory for resolving pattern file and scope",
|
|
183
|
+
placeholder: "path",
|
|
184
|
+
parse: (input) => input,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
positional: {
|
|
188
|
+
kind: "tuple",
|
|
189
|
+
parameters: [
|
|
190
|
+
{
|
|
191
|
+
brief: "Pattern text or path to pattern file",
|
|
192
|
+
placeholder: "pattern",
|
|
193
|
+
parse: (input) => input,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
brief: "Scope file or directory (defaults to current directory)",
|
|
197
|
+
placeholder: "scope",
|
|
198
|
+
parse: (input) => input,
|
|
199
|
+
optional: true,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
docs: {
|
|
205
|
+
brief: "Run structural search (sgrep-style) from a pattern",
|
|
206
|
+
},
|
|
207
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
export interface Service {
|
|
3
|
+
service: ts.LanguageService;
|
|
4
|
+
program: ts.Program;
|
|
5
|
+
projectRoot: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function createService(projectDir: string, targetFile?: string | readonly string[]): Service;
|
|
8
|
+
/** Convert 1-indexed line:character to 0-indexed offset */
|
|
9
|
+
export declare function toPosition(sourceFile: ts.SourceFile, line: number, character: number): number;
|
|
10
|
+
/** Convert 0-indexed offset to 1-indexed { line, character } */
|
|
11
|
+
export declare function fromPosition(sourceFile: ts.SourceFile, offset: number): {
|
|
12
|
+
line: number;
|
|
13
|
+
character: number;
|
|
14
|
+
};
|
|
15
|
+
/** Get relative path from project root */
|
|
16
|
+
export declare function relativePath(projectRoot: string, filePath: string): string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function createService(projectDir, targetFile) {
|
|
4
|
+
const configPath = ts.findConfigFile(projectDir, ts.sys.fileExists);
|
|
5
|
+
let compilerOptions;
|
|
6
|
+
let fileNames;
|
|
7
|
+
let projectRoot;
|
|
8
|
+
if (configPath) {
|
|
9
|
+
const { config, error } = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
10
|
+
if (error) {
|
|
11
|
+
throw new Error(`Failed to read tsconfig: ${ts.flattenDiagnosticMessageText(error.messageText, "\n")}`);
|
|
12
|
+
}
|
|
13
|
+
projectRoot = path.dirname(configPath);
|
|
14
|
+
const parsed = ts.parseJsonConfigFileContent(config, ts.sys, projectRoot);
|
|
15
|
+
compilerOptions = parsed.options;
|
|
16
|
+
fileNames = parsed.fileNames;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
compilerOptions = ts.getDefaultCompilerOptions();
|
|
20
|
+
fileNames = [];
|
|
21
|
+
projectRoot = projectDir;
|
|
22
|
+
}
|
|
23
|
+
// Ensure requested target files are in the language-service file set.
|
|
24
|
+
const targetFiles = normalizeTargetFiles(targetFile);
|
|
25
|
+
for (const requestedFile of targetFiles) {
|
|
26
|
+
const resolved = path.resolve(requestedFile);
|
|
27
|
+
if (!fileNames.includes(resolved)) {
|
|
28
|
+
fileNames.push(resolved);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const host = {
|
|
32
|
+
getScriptFileNames: () => fileNames,
|
|
33
|
+
getScriptVersion: () => "0",
|
|
34
|
+
getScriptSnapshot: (file) => {
|
|
35
|
+
const content = ts.sys.readFile(file);
|
|
36
|
+
if (content === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
return ts.ScriptSnapshot.fromString(content);
|
|
39
|
+
},
|
|
40
|
+
getCurrentDirectory: () => projectRoot,
|
|
41
|
+
getCompilationSettings: () => compilerOptions,
|
|
42
|
+
getDefaultLibFileName: ts.getDefaultLibFilePath,
|
|
43
|
+
fileExists: ts.sys.fileExists,
|
|
44
|
+
readFile: ts.sys.readFile,
|
|
45
|
+
readDirectory: ts.sys.readDirectory,
|
|
46
|
+
};
|
|
47
|
+
const service = ts.createLanguageService(host);
|
|
48
|
+
const program = service.getProgram();
|
|
49
|
+
return { service, program, projectRoot };
|
|
50
|
+
}
|
|
51
|
+
function normalizeTargetFiles(targetFile) {
|
|
52
|
+
if (targetFile === undefined) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
if (typeof targetFile === "string") {
|
|
56
|
+
return [targetFile];
|
|
57
|
+
}
|
|
58
|
+
return [...targetFile];
|
|
59
|
+
}
|
|
60
|
+
/** Convert 1-indexed line:character to 0-indexed offset */
|
|
61
|
+
export function toPosition(sourceFile, line, character) {
|
|
62
|
+
return sourceFile.getPositionOfLineAndCharacter(line - 1, character - 1);
|
|
63
|
+
}
|
|
64
|
+
/** Convert 0-indexed offset to 1-indexed { line, character } */
|
|
65
|
+
export function fromPosition(sourceFile, offset) {
|
|
66
|
+
const lc = sourceFile.getLineAndCharacterOfPosition(offset);
|
|
67
|
+
return { line: lc.line + 1, character: lc.character + 1 };
|
|
68
|
+
}
|
|
69
|
+
/** Get relative path from project root */
|
|
70
|
+
export function relativePath(projectRoot, filePath) {
|
|
71
|
+
return path.relative(projectRoot, filePath);
|
|
72
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { searchProject, sgrep, } from "./sgrep.ts";
|
|
2
|
+
export type { SgrepFileResult, SgrepMatch, SgrepOptions, SgrepResult, } from "./types.ts";
|
|
3
|
+
export { DEFAULT_SEARCHABLE_EXTENSIONS, DEFAULT_SEARCH_EXCLUDED_DIRECTORIES, } from "./types.ts";
|
|
4
|
+
export { DEFAULT_ISOMORPHISM_RULES, expandPatternIsomorphisms, } from "./isomorphisms/index.ts";
|
|
5
|
+
export type { ExpandIsomorphismsOptions, IsomorphismRule, } from "./isomorphisms/index.ts";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DEFAULT_ISOMORPHISM_RULES } from "./registry.js";
|
|
2
|
+
import { parseTemplateIsomorphismContext } from "./template-ast.js";
|
|
3
|
+
const DEFAULT_MAX_ISOMORPHISM_VARIANTS = 24;
|
|
4
|
+
export function expandPatternIsomorphisms(pattern, options = {}) {
|
|
5
|
+
if (!(options.enabled ?? true)) {
|
|
6
|
+
return [pattern];
|
|
7
|
+
}
|
|
8
|
+
const rules = options.rules ?? DEFAULT_ISOMORPHISM_RULES;
|
|
9
|
+
if (rules.length === 0) {
|
|
10
|
+
return [pattern];
|
|
11
|
+
}
|
|
12
|
+
const maxVariants = normalizeMaxVariants(options.maxVariants);
|
|
13
|
+
const variants = [{ pattern, applied: [] }];
|
|
14
|
+
const seenPatterns = new Set([pattern]);
|
|
15
|
+
for (let variantIndex = 0; variantIndex < variants.length && variants.length < maxVariants; variantIndex += 1) {
|
|
16
|
+
const current = variants[variantIndex];
|
|
17
|
+
if (!current) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const context = parseTemplateIsomorphismContext(current.pattern);
|
|
21
|
+
if (!context) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
for (const rule of rules) {
|
|
25
|
+
const produced = rule.apply(context);
|
|
26
|
+
for (const nextPattern of produced) {
|
|
27
|
+
if (seenPatterns.has(nextPattern)) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
seenPatterns.add(nextPattern);
|
|
31
|
+
variants.push({
|
|
32
|
+
pattern: nextPattern,
|
|
33
|
+
applied: [...current.applied, rule.id],
|
|
34
|
+
});
|
|
35
|
+
if (variants.length >= maxVariants) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (variants.length >= maxVariants) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return variants.map((variant) => variant.pattern);
|
|
45
|
+
}
|
|
46
|
+
function normalizeMaxVariants(maxVariants) {
|
|
47
|
+
if (maxVariants === undefined) {
|
|
48
|
+
return DEFAULT_MAX_ISOMORPHISM_VARIANTS;
|
|
49
|
+
}
|
|
50
|
+
return Math.max(1, Math.floor(maxVariants));
|
|
51
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { commutativeBinaryRule } from "./rules/commutative-binary.js";
|
|
2
|
+
import { objectLiteralPropertyOrderRule } from "./rules/object-literal-property-order.js";
|
|
3
|
+
import { redundantParenthesesRule } from "./rules/redundant-parentheses.js";
|
|
4
|
+
export const DEFAULT_ISOMORPHISM_RULES = [
|
|
5
|
+
commutativeBinaryRule,
|
|
6
|
+
objectLiteralPropertyOrderRule,
|
|
7
|
+
redundantParenthesesRule,
|
|
8
|
+
];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
const COMMUTATIVE_OPERATOR_KINDS = new Set([
|
|
3
|
+
ts.SyntaxKind.PlusToken,
|
|
4
|
+
ts.SyntaxKind.AsteriskToken,
|
|
5
|
+
ts.SyntaxKind.AmpersandToken,
|
|
6
|
+
ts.SyntaxKind.BarToken,
|
|
7
|
+
ts.SyntaxKind.CaretToken,
|
|
8
|
+
ts.SyntaxKind.EqualsEqualsToken,
|
|
9
|
+
ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
10
|
+
ts.SyntaxKind.ExclamationEqualsToken,
|
|
11
|
+
ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
12
|
+
]);
|
|
13
|
+
export const commutativeBinaryRule = {
|
|
14
|
+
id: "commutative-binary",
|
|
15
|
+
description: "Swap operands of commutative binary operators.",
|
|
16
|
+
apply: (context) => {
|
|
17
|
+
const variants = new Set();
|
|
18
|
+
visitNode(context.ast, (node) => {
|
|
19
|
+
if (!ts.isBinaryExpression(node)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!COMMUTATIVE_OPERATOR_KINDS.has(node.operatorToken.kind)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const expressionStart = node.getStart(context.ast);
|
|
26
|
+
const expressionEnd = node.end;
|
|
27
|
+
const leftStart = node.left.getStart(context.ast);
|
|
28
|
+
const leftEnd = node.left.end;
|
|
29
|
+
const rightStart = node.right.getStart(context.ast);
|
|
30
|
+
const rightEnd = node.right.end;
|
|
31
|
+
const leftText = context.source.slice(leftStart, leftEnd);
|
|
32
|
+
const middleText = context.source.slice(leftEnd, rightStart);
|
|
33
|
+
const rightText = context.source.slice(rightStart, rightEnd);
|
|
34
|
+
if (leftText.length === 0 || middleText.length === 0 || rightText.length === 0) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const swappedExpression = `${rightText}${middleText}${leftText}`;
|
|
38
|
+
const variant = context.source.slice(0, expressionStart) +
|
|
39
|
+
swappedExpression +
|
|
40
|
+
context.source.slice(expressionEnd);
|
|
41
|
+
if (variant !== context.source) {
|
|
42
|
+
variants.add(variant);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return [...variants];
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
function visitNode(node, visitor) {
|
|
49
|
+
visitor(node);
|
|
50
|
+
ts.forEachChild(node, (child) => visitNode(child, visitor));
|
|
51
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
export const objectLiteralPropertyOrderRule = {
|
|
3
|
+
id: "object-literal-property-order",
|
|
4
|
+
description: "Swap adjacent object literal entries where key/value order is semantically irrelevant.",
|
|
5
|
+
apply: (context) => {
|
|
6
|
+
const variants = new Set();
|
|
7
|
+
visitNode(context.ast, (node) => {
|
|
8
|
+
if (!ts.isObjectLiteralExpression(node)) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const properties = [...node.properties];
|
|
12
|
+
if (properties.length < 2) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (!isReorderableMapObject(properties)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
for (let index = 0; index < properties.length - 1; index += 1) {
|
|
19
|
+
const left = properties[index];
|
|
20
|
+
const right = properties[index + 1];
|
|
21
|
+
if (!left || !right) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const leftStart = left.getStart(context.ast);
|
|
25
|
+
const leftEnd = left.end;
|
|
26
|
+
const rightStart = right.getStart(context.ast);
|
|
27
|
+
const rightEnd = right.end;
|
|
28
|
+
if (leftEnd > rightStart) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const separator = context.source.slice(leftEnd, rightStart);
|
|
32
|
+
if (!separator.includes(",")) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const swappedSegment = context.source.slice(rightStart, rightEnd) +
|
|
36
|
+
separator +
|
|
37
|
+
context.source.slice(leftStart, leftEnd);
|
|
38
|
+
const variant = context.source.slice(0, leftStart) +
|
|
39
|
+
swappedSegment +
|
|
40
|
+
context.source.slice(rightEnd);
|
|
41
|
+
if (variant !== context.source) {
|
|
42
|
+
variants.add(variant);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return [...variants];
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
function isReorderableMapObject(properties) {
|
|
50
|
+
const seenKeys = new Set();
|
|
51
|
+
for (const property of properties) {
|
|
52
|
+
if (!ts.isPropertyAssignment(property) &&
|
|
53
|
+
!ts.isShorthandPropertyAssignment(property)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const key = getStablePropertyKey(property.name);
|
|
57
|
+
if (key === null) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (seenKeys.has(key)) {
|
|
61
|
+
// Duplicate keys are order-sensitive ("last write wins").
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
seenKeys.add(key);
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
function getStablePropertyKey(name) {
|
|
69
|
+
if (ts.isIdentifier(name)) {
|
|
70
|
+
return name.text;
|
|
71
|
+
}
|
|
72
|
+
if (ts.isStringLiteral(name) ||
|
|
73
|
+
ts.isNumericLiteral(name) ||
|
|
74
|
+
ts.isNoSubstitutionTemplateLiteral(name)) {
|
|
75
|
+
return name.text;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function visitNode(node, visitor) {
|
|
80
|
+
visitor(node);
|
|
81
|
+
ts.forEachChild(node, (child) => visitNode(child, visitor));
|
|
82
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
export const redundantParenthesesRule = {
|
|
3
|
+
id: "redundant-parentheses",
|
|
4
|
+
description: "Add or remove extra parentheses around binary expressions.",
|
|
5
|
+
apply: (context) => {
|
|
6
|
+
const variants = new Set();
|
|
7
|
+
visitNode(context.ast, (node) => {
|
|
8
|
+
if (ts.isParenthesizedExpression(node)) {
|
|
9
|
+
const start = node.getStart(context.ast);
|
|
10
|
+
const end = node.end;
|
|
11
|
+
const innerStart = node.expression.getStart(context.ast);
|
|
12
|
+
const innerEnd = node.expression.end;
|
|
13
|
+
const innerText = context.source.slice(innerStart, innerEnd);
|
|
14
|
+
if (innerText.length > 0) {
|
|
15
|
+
const variant = context.source.slice(0, start) +
|
|
16
|
+
innerText +
|
|
17
|
+
context.source.slice(end);
|
|
18
|
+
if (variant !== context.source) {
|
|
19
|
+
variants.add(variant);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (ts.isBinaryExpression(node)) {
|
|
24
|
+
const start = node.getStart(context.ast);
|
|
25
|
+
const end = node.end;
|
|
26
|
+
const expressionText = context.source.slice(start, end);
|
|
27
|
+
if (expressionText.length > 0) {
|
|
28
|
+
const variant = context.source.slice(0, start) +
|
|
29
|
+
`(${expressionText})` +
|
|
30
|
+
context.source.slice(end);
|
|
31
|
+
if (variant !== context.source) {
|
|
32
|
+
variants.add(variant);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return [...variants];
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
function visitNode(node, visitor) {
|
|
41
|
+
visitor(node);
|
|
42
|
+
ts.forEachChild(node, (child) => visitNode(child, visitor));
|
|
43
|
+
}
|