Skip to content

Commit dc4ec10

Browse files
committed
feat: Break up god class
1 parent f0c0067 commit dc4ec10

File tree

6 files changed

+2724
-2638
lines changed

6 files changed

+2724
-2638
lines changed
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import type Parser from "web-tree-sitter";
2+
import { cleanStringValue, getCapture } from "./ast-helpers.js";
3+
import type { LangFamily } from "./languages.js";
4+
import { CLIENT_NAMES } from "./languages.js";
5+
import type { ParserManager } from "./parser-manager.js";
6+
import type { DetectionConfig } from "./types.js";
7+
8+
const POSTHOG_CLASS_NAMES = new Set(["PostHog", "Posthog"]);
9+
const GO_CONSTRUCTOR_NAMES = new Set(["New", "NewWithConfig"]);
10+
11+
export function getEffectiveClients(config: DetectionConfig): Set<string> {
12+
const clients = new Set(CLIENT_NAMES);
13+
for (const name of config.additionalClientNames) {
14+
clients.add(name);
15+
}
16+
return clients;
17+
}
18+
19+
export function findAliases(
20+
pm: ParserManager,
21+
lang: Parser.Language,
22+
tree: Parser.Tree,
23+
family: LangFamily,
24+
): {
25+
clientAliases: Set<string>;
26+
destructuredCapture: Set<string>;
27+
destructuredFlag: Set<string>;
28+
} {
29+
const effectiveClients = getEffectiveClients(pm.config);
30+
const clientAliases = new Set<string>();
31+
const destructuredCapture = new Set<string>();
32+
const destructuredFlag = new Set<string>();
33+
34+
// Client aliases: const tracker = posthog
35+
const aliasQuery = pm.getQuery(lang, family.queries.clientAliases);
36+
if (aliasQuery) {
37+
for (const match of aliasQuery.matches(tree.rootNode)) {
38+
const aliasNode = getCapture(match.captures, "alias");
39+
const sourceNode = getCapture(match.captures, "source");
40+
if (aliasNode && sourceNode && effectiveClients.has(sourceNode.text)) {
41+
clientAliases.add(aliasNode.text);
42+
}
43+
}
44+
}
45+
46+
// Constructor aliases: new PostHog('phc_...') / posthog.New("token") / PostHog::Client.new(...)
47+
const constructorQuery = pm.getQuery(lang, family.queries.constructorAliases);
48+
if (constructorQuery) {
49+
for (const match of constructorQuery.matches(tree.rootNode)) {
50+
const aliasNode = getCapture(match.captures, "alias");
51+
const classNode = getCapture(match.captures, "class_name");
52+
const pkgNode = getCapture(match.captures, "pkg_name");
53+
const funcNode = getCapture(match.captures, "func_name");
54+
55+
if (aliasNode && classNode && POSTHOG_CLASS_NAMES.has(classNode.text)) {
56+
clientAliases.add(aliasNode.text);
57+
}
58+
if (
59+
aliasNode &&
60+
pkgNode &&
61+
funcNode &&
62+
pkgNode.text === "posthog" &&
63+
GO_CONSTRUCTOR_NAMES.has(funcNode.text)
64+
) {
65+
clientAliases.add(aliasNode.text);
66+
}
67+
const scopeNode = getCapture(match.captures, "scope_name");
68+
const methodNameNode = getCapture(match.captures, "method_name");
69+
if (
70+
aliasNode &&
71+
scopeNode &&
72+
classNode &&
73+
methodNameNode &&
74+
POSTHOG_CLASS_NAMES.has(scopeNode.text) &&
75+
classNode.text === "Client" &&
76+
methodNameNode.text === "new"
77+
) {
78+
clientAliases.add(aliasNode.text);
79+
}
80+
}
81+
}
82+
83+
// Destructured methods: const { capture, getFeatureFlag } = posthog
84+
if (family.queries.destructuredMethods) {
85+
const destructQuery = pm.getQuery(lang, family.queries.destructuredMethods);
86+
if (destructQuery) {
87+
for (const match of destructQuery.matches(tree.rootNode)) {
88+
const methodNode = getCapture(match.captures, "method_name");
89+
const sourceNode = getCapture(match.captures, "source");
90+
if (methodNode && sourceNode && effectiveClients.has(sourceNode.text)) {
91+
const name = methodNode.text;
92+
if (family.captureMethods.has(name)) {
93+
destructuredCapture.add(name);
94+
}
95+
if (family.flagMethods.has(name)) {
96+
destructuredFlag.add(name);
97+
}
98+
}
99+
}
100+
}
101+
}
102+
103+
return { clientAliases, destructuredCapture, destructuredFlag };
104+
}
105+
106+
export function buildConstantMap(
107+
pm: ParserManager,
108+
lang: Parser.Language,
109+
tree: Parser.Tree,
110+
): Map<string, string> {
111+
const constants = new Map<string, string>();
112+
113+
// JS: const/let/var declarations
114+
const jsQuery = pm.getQuery(
115+
lang,
116+
`
117+
(lexical_declaration
118+
(variable_declarator
119+
name: (identifier) @name
120+
value: (string (string_fragment) @value)))
121+
122+
(variable_declaration
123+
(variable_declarator
124+
name: (identifier) @name
125+
value: (string (string_fragment) @value)))
126+
`,
127+
);
128+
if (jsQuery) {
129+
for (const match of jsQuery.matches(tree.rootNode)) {
130+
const nameNode = getCapture(match.captures, "name");
131+
const valueNode = getCapture(match.captures, "value");
132+
if (nameNode && valueNode) {
133+
constants.set(nameNode.text, valueNode.text);
134+
}
135+
}
136+
}
137+
138+
// Python: simple assignment — NAME = "value"
139+
const pyQuery = pm.getQuery(
140+
lang,
141+
`
142+
(expression_statement
143+
(assignment
144+
left: (identifier) @name
145+
right: (string (string_content) @value)))
146+
`,
147+
);
148+
if (pyQuery) {
149+
for (const match of pyQuery.matches(tree.rootNode)) {
150+
const nameNode = getCapture(match.captures, "name");
151+
const valueNode = getCapture(match.captures, "value");
152+
if (nameNode && valueNode) {
153+
constants.set(nameNode.text, valueNode.text);
154+
}
155+
}
156+
}
157+
158+
// Go: short var declarations and const declarations
159+
const goVarQuery = pm.getQuery(
160+
lang,
161+
`
162+
(short_var_declaration
163+
left: (expression_list (identifier) @name)
164+
right: (expression_list (interpreted_string_literal) @value))
165+
`,
166+
);
167+
if (goVarQuery) {
168+
for (const match of goVarQuery.matches(tree.rootNode)) {
169+
const nameNode = getCapture(match.captures, "name");
170+
const valueNode = getCapture(match.captures, "value");
171+
if (nameNode && valueNode) {
172+
constants.set(nameNode.text, cleanStringValue(valueNode.text));
173+
}
174+
}
175+
}
176+
177+
const goConstQuery = pm.getQuery(
178+
lang,
179+
`
180+
(const_declaration
181+
(const_spec
182+
name: (identifier) @name
183+
value: (expression_list (interpreted_string_literal) @value)))
184+
`,
185+
);
186+
if (goConstQuery) {
187+
for (const match of goConstQuery.matches(tree.rootNode)) {
188+
const nameNode = getCapture(match.captures, "name");
189+
const valueNode = getCapture(match.captures, "value");
190+
if (nameNode && valueNode) {
191+
constants.set(nameNode.text, cleanStringValue(valueNode.text));
192+
}
193+
}
194+
}
195+
196+
// Ruby: assignment — local var or constant
197+
const rbQuery = pm.getQuery(
198+
lang,
199+
`
200+
(assignment
201+
left: (identifier) @name
202+
right: (string (string_content) @value))
203+
204+
(assignment
205+
left: (constant) @name
206+
right: (string (string_content) @value))
207+
`,
208+
);
209+
if (rbQuery) {
210+
for (const match of rbQuery.matches(tree.rootNode)) {
211+
const nameNode = getCapture(match.captures, "name");
212+
const valueNode = getCapture(match.captures, "value");
213+
if (nameNode && valueNode) {
214+
constants.set(nameNode.text, valueNode.text);
215+
}
216+
}
217+
}
218+
219+
return constants;
220+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import type Parser from "web-tree-sitter";
2+
3+
export interface Capture {
4+
name: string;
5+
node: Parser.SyntaxNode;
6+
}
7+
8+
export function getCapture(
9+
captures: Capture[],
10+
name: string,
11+
): Parser.SyntaxNode | null {
12+
const found = captures.find((c) => c.name === name);
13+
return found ? found.node : null;
14+
}
15+
16+
export function extractClientName(
17+
node: Parser.SyntaxNode,
18+
detectNested: boolean,
19+
): string | null {
20+
if (node.type === "identifier") {
21+
return node.text;
22+
}
23+
if (detectNested) {
24+
if (node.type === "member_expression" || node.type === "attribute") {
25+
const prop =
26+
node.childForFieldName("property") ||
27+
node.childForFieldName("attribute");
28+
if (prop) {
29+
return prop.text;
30+
}
31+
}
32+
if (node.type === "selector_expression") {
33+
const field = node.childForFieldName("field");
34+
if (field) {
35+
return field.text;
36+
}
37+
}
38+
if (node.type === "optional_chain_expression") {
39+
const inner = node.namedChildren[0];
40+
if (inner?.type === "member_expression") {
41+
const prop = inner.childForFieldName("property");
42+
if (prop) {
43+
return prop.text;
44+
}
45+
}
46+
}
47+
}
48+
return null;
49+
}
50+
51+
export function extractIdentifier(node: Parser.SyntaxNode): string | null {
52+
if (node.type === "identifier") {
53+
return node.text;
54+
}
55+
if (
56+
node.type === "parenthesized_expression" &&
57+
node.namedChildren.length === 1
58+
) {
59+
return extractIdentifier(node.namedChildren[0]);
60+
}
61+
return null;
62+
}
63+
64+
export function extractStringFromCaseValue(
65+
node: Parser.SyntaxNode,
66+
): string | null {
67+
if (node.type === "expression_list" && node.namedChildCount > 0) {
68+
return extractStringFromNode(node.namedChildren[0]);
69+
}
70+
return extractStringFromNode(node);
71+
}
72+
73+
export function extractStringFromNode(node: Parser.SyntaxNode): string | null {
74+
if (node.type === "string" || node.type === "template_string") {
75+
const content = node.namedChildren.find(
76+
(c) =>
77+
c.type === "string_fragment" ||
78+
c.type === "string_content" ||
79+
c.type === "string_value",
80+
);
81+
return content ? content.text : null;
82+
}
83+
if (
84+
node.type === "interpreted_string_literal" ||
85+
node.type === "raw_string_literal"
86+
) {
87+
return node.text.slice(1, -1);
88+
}
89+
if (node.type === "string_fragment" || node.type === "string_content") {
90+
return node.text;
91+
}
92+
return null;
93+
}
94+
95+
export function cleanStringValue(text: string): string {
96+
if (
97+
(text.startsWith('"') && text.endsWith('"')) ||
98+
(text.startsWith("'") && text.endsWith("'")) ||
99+
(text.startsWith("`") && text.endsWith("`"))
100+
) {
101+
return text.slice(1, -1);
102+
}
103+
return text;
104+
}
105+
106+
const PARAM_SKIP = new Set([
107+
"e",
108+
"ev",
109+
"event",
110+
"evt",
111+
"ctx",
112+
"context",
113+
"req",
114+
"res",
115+
"next",
116+
"err",
117+
"error",
118+
"_",
119+
"__",
120+
]);
121+
122+
export function extractParams(paramsText: string): string[] {
123+
let text = paramsText.trim();
124+
if (text.startsWith("(")) {
125+
text = text.slice(1);
126+
}
127+
if (text.endsWith(")")) {
128+
text = text.slice(0, -1);
129+
}
130+
if (!text.trim()) {
131+
return [];
132+
}
133+
134+
return text
135+
.split(",")
136+
.map((p) => {
137+
if (p.includes("{") || p.includes("}")) {
138+
return "";
139+
}
140+
const name = p.split(":")[0].split("=")[0].replace(/[?.]/g, "").trim();
141+
return name;
142+
})
143+
.filter((p) => p && !PARAM_SKIP.has(p) && !p.startsWith("..."));
144+
}
145+
146+
export function walkNodes(
147+
root: Parser.SyntaxNode,
148+
type: string,
149+
callback: (node: Parser.SyntaxNode) => void,
150+
): void {
151+
const visit = (node: Parser.SyntaxNode) => {
152+
if (node.type === type) {
153+
callback(node);
154+
}
155+
for (const child of node.namedChildren) {
156+
visit(child);
157+
}
158+
};
159+
visit(root);
160+
}

0 commit comments

Comments
 (0)