Skip to content

Commit f1fa9a0

Browse files
committed
feat: add expression resolution
1 parent 51613bc commit f1fa9a0

1 file changed

Lines changed: 291 additions & 0 deletions

File tree

src/analysis/resolution.ts

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import { childForField } from "../cpp-ast.js";
2+
import type { AliasTarget, FunctionParameter } from "../types.js";
3+
import type { SyntaxNode } from "tree-sitter";
4+
import {
5+
extractStringLiteral,
6+
extractSubscriptIndexNode,
7+
parseCallExpressionNode,
8+
parseFieldExpression,
9+
unwrapExpressionNode,
10+
} from "./syntax.js";
11+
12+
export function resolveExpressionNode(
13+
expressionNode: SyntaxNode,
14+
aliases: ReadonlyMap<string, AliasTarget>,
15+
constants: ReadonlyMap<string, string>,
16+
): AliasTarget | undefined {
17+
const normalizedNode = unwrapExpressionNode(expressionNode);
18+
19+
const requestRoot = resolveRequestRoot(normalizedNode);
20+
if (requestRoot !== undefined) return requestRoot;
21+
22+
const directAlias = getDirectAlias(normalizedNode, aliases);
23+
if (directAlias !== undefined) return directAlias;
24+
25+
if (normalizedNode.type === "call_expression") {
26+
return resolveCallExpression(normalizedNode, aliases, constants);
27+
}
28+
29+
if (normalizedNode.type === "subscript_expression") {
30+
return resolveSubscriptExpression(normalizedNode, aliases, constants);
31+
}
32+
33+
return undefined;
34+
}
35+
36+
function getDirectAlias(
37+
node: SyntaxNode,
38+
aliases: ReadonlyMap<string, AliasTarget>,
39+
): AliasTarget | undefined {
40+
if (node.type !== "identifier") {
41+
return undefined;
42+
}
43+
return aliases.get(node.text);
44+
}
45+
46+
function resolveCallExpression(
47+
node: SyntaxNode,
48+
aliases: ReadonlyMap<string, AliasTarget>,
49+
constants: ReadonlyMap<string, string>,
50+
): AliasTarget | undefined {
51+
const callInfo = parseCallExpressionNode(node);
52+
if (callInfo === undefined) {
53+
return undefined;
54+
}
55+
56+
if (callInfo.methodName === undefined) {
57+
return resolveFreeCallExpression(callInfo, aliases, constants);
58+
}
59+
60+
return resolveMethodCallExpression(callInfo, aliases, constants);
61+
}
62+
63+
function resolveFreeCallExpression(
64+
callInfo: NonNullable<ReturnType<typeof parseCallExpressionNode>>,
65+
aliases: ReadonlyMap<string, AliasTarget>,
66+
constants: ReadonlyMap<string, string>,
67+
): AliasTarget | undefined {
68+
const calleeName = extractTerminalName(callInfo.calleeName);
69+
const isJsonParseCall =
70+
callInfo.calleeName === "nlohmann::json::parse" || calleeName === "parse";
71+
if (!isJsonParseCall) {
72+
return undefined;
73+
}
74+
75+
const firstArgument = callInfo.arguments[0];
76+
if (firstArgument === undefined) {
77+
return undefined;
78+
}
79+
80+
const firstTarget = resolveExpressionNode(firstArgument, aliases, constants);
81+
if (firstTarget?.location !== "body-root") {
82+
return undefined;
83+
}
84+
85+
return { location: "body-root", segments: [] };
86+
}
87+
88+
function resolveMethodCallExpression(
89+
callInfo: NonNullable<ReturnType<typeof parseCallExpressionNode>>,
90+
aliases: ReadonlyMap<string, AliasTarget>,
91+
constants: ReadonlyMap<string, string>,
92+
): AliasTarget | undefined {
93+
if (callInfo.baseNode === undefined) {
94+
return undefined;
95+
}
96+
97+
const baseTarget = resolveExpressionNode(
98+
callInfo.baseNode,
99+
aliases,
100+
constants,
101+
);
102+
if (baseTarget === undefined) {
103+
return undefined;
104+
}
105+
106+
if (callInfo.methodName === "items") {
107+
return baseTarget;
108+
}
109+
110+
if (callInfo.methodName === "value") {
111+
if (callInfo.arguments.length === 0) {
112+
return baseTarget;
113+
}
114+
return resolveIndexedMethodTarget(
115+
baseTarget,
116+
callInfo.arguments[0],
117+
constants,
118+
);
119+
}
120+
121+
if (callInfo.methodName === "find" || callInfo.methodName === "at") {
122+
return resolveIndexedMethodTarget(
123+
baseTarget,
124+
callInfo.arguments[0],
125+
constants,
126+
);
127+
}
128+
129+
return undefined;
130+
}
131+
132+
function resolveIndexedMethodTarget(
133+
baseTarget: AliasTarget,
134+
firstArgument: SyntaxNode | undefined,
135+
constants: ReadonlyMap<string, string>,
136+
): AliasTarget | undefined {
137+
if (firstArgument === undefined) {
138+
return undefined;
139+
}
140+
141+
const segment = resolveStringTokenNode(firstArgument, constants);
142+
if (segment === undefined) {
143+
return undefined;
144+
}
145+
146+
return {
147+
location: baseTarget.location,
148+
segments: [...baseTarget.segments, segment],
149+
};
150+
}
151+
152+
function resolveSubscriptExpression(
153+
node: SyntaxNode,
154+
aliases: ReadonlyMap<string, AliasTarget>,
155+
constants: ReadonlyMap<string, string>,
156+
): AliasTarget | undefined {
157+
const baseNode = childForField(node, "argument");
158+
const indexNode = extractSubscriptIndexNode(node);
159+
if (baseNode === undefined || indexNode === undefined) {
160+
return undefined;
161+
}
162+
163+
const baseTarget = resolveExpressionNode(baseNode, aliases, constants);
164+
if (baseTarget === undefined) {
165+
return undefined;
166+
}
167+
168+
const segment = resolveStringTokenNode(indexNode, constants) ?? "[]";
169+
return {
170+
location: baseTarget.location,
171+
segments: [...baseTarget.segments, segment],
172+
};
173+
}
174+
175+
function extractTerminalName(value: string): string | undefined {
176+
const parts = value.split("::");
177+
return parts[parts.length - 1];
178+
}
179+
180+
export function resolveLoopItemTarget(
181+
expressionNode: SyntaxNode,
182+
aliases: ReadonlyMap<string, AliasTarget>,
183+
constants: ReadonlyMap<string, string>,
184+
): AliasTarget | undefined {
185+
const resolved = resolveExpressionNode(expressionNode, aliases, constants);
186+
if (resolved === undefined || resolved.location === "query-root") {
187+
return undefined;
188+
}
189+
190+
return {
191+
location: "body-root",
192+
segments: [...resolved.segments, "[]"],
193+
};
194+
}
195+
196+
export function resolveStringTokenNode(
197+
node: SyntaxNode,
198+
constants: ReadonlyMap<string, string>,
199+
): string | undefined {
200+
const normalizedNode = unwrapExpressionNode(node);
201+
if (normalizedNode.type === "string_literal") {
202+
return extractStringLiteral(normalizedNode.text);
203+
}
204+
205+
if (normalizedNode.type === "concatenated_string") {
206+
return resolveConcatenatedString(normalizedNode);
207+
}
208+
209+
if (normalizedNode.type === "identifier") {
210+
return constants.get(normalizedNode.text);
211+
}
212+
213+
if (normalizedNode.type === "qualified_identifier") {
214+
return resolveQualifiedIdentifierToken(normalizedNode.text, constants);
215+
}
216+
217+
return undefined;
218+
}
219+
220+
function resolveConcatenatedString(node: SyntaxNode): string | undefined {
221+
return node.namedChildren.reduce<string | undefined>((combined, child) => {
222+
if (combined === undefined || child.type !== "string_literal") {
223+
return undefined;
224+
}
225+
226+
const value = extractStringLiteral(child.text);
227+
if (value === undefined) {
228+
return undefined;
229+
}
230+
231+
return `${combined}${value}`;
232+
}, "");
233+
}
234+
235+
function resolveQualifiedIdentifierToken(
236+
tokenText: string,
237+
constants: ReadonlyMap<string, string>,
238+
): string | undefined {
239+
if (tokenText.startsWith("fields::")) {
240+
return tokenText.slice("fields::".length);
241+
}
242+
243+
const terminalName = extractTerminalName(tokenText);
244+
if (terminalName === undefined) {
245+
return undefined;
246+
}
247+
248+
return constants.get(terminalName);
249+
}
250+
251+
export function bootstrapAliases(
252+
parameters: readonly FunctionParameter[],
253+
inherited: ReadonlyMap<string, AliasTarget>,
254+
): Map<string, AliasTarget> {
255+
const aliases = new Map(inherited);
256+
257+
parameters
258+
.filter((parameter) =>
259+
parameter.typeText.includes("std::map<std::string, std::string>"),
260+
)
261+
.forEach((parameter) =>
262+
aliases.set(parameter.name, { location: "query-root", segments: [] }),
263+
);
264+
265+
return aliases;
266+
}
267+
268+
function resolveRequestRoot(node: SyntaxNode): AliasTarget | undefined {
269+
const fieldParts = parseFieldExpression(node);
270+
if (fieldParts === undefined) {
271+
return undefined;
272+
}
273+
274+
if (
275+
fieldParts.operator !== "->" ||
276+
fieldParts.baseNode.type !== "identifier" ||
277+
fieldParts.baseNode.text !== "req"
278+
) {
279+
return undefined;
280+
}
281+
282+
if (fieldParts.fieldName === "params") {
283+
return { location: "params-root", segments: [] };
284+
}
285+
286+
if (fieldParts.fieldName === "body") {
287+
return { location: "body-root", segments: [] };
288+
}
289+
290+
return undefined;
291+
}

0 commit comments

Comments
 (0)