Skip to main content

comindly_core/lang/
spring.rs

1//! Spring MVC and OpenFeign contracts for Java and Kotlin: mapping annotations on controller
2//! methods serve routes, the same annotations on `@FeignClient` interfaces consume them.
3
4use tree_sitter::Node;
5
6use super::{named_children, text, RawContract};
7use crate::facts::{Args, ValueExpr};
8
9const MAPPINGS: &[(&str, &str)] = &[
10    ("GetMapping", "GET"),
11    ("PostMapping", "POST"),
12    ("PutMapping", "PUT"),
13    ("PatchMapping", "PATCH"),
14    ("DeleteMapping", "DELETE"),
15];
16
17/// RestTemplate methods and the verb they imply (`exchange` takes it as an argument).
18pub(crate) const REST_TEMPLATE: &[(&str, &str)] = &[
19    ("getForObject", "GET"),
20    ("getForEntity", "GET"),
21    ("postForObject", "POST"),
22    ("postForEntity", "POST"),
23    ("postForLocation", "POST"),
24    ("patchForObject", "PATCH"),
25    ("exchange", ""),
26];
27
28/// Receiver marking a WebClient request (`client.get().uri(url)`); the method is the verb.
29pub(crate) const WEBCLIENT: &str = "<webclient>";
30const VERBS: &[&str] = &["get", "post", "put", "patch", "delete"];
31
32/// `x.get().uri(...)`: the verb of the call a `uri(...)` call is chained on.
33pub(crate) fn webclient_verb(inner_method: &str) -> Option<String> {
34    VERBS
35        .contains(&inner_method)
36        .then(|| inner_method.to_uppercase())
37}
38
39struct Annotation<'t> {
40    name: String,
41    args: Vec<(Option<String>, Node<'t>)>,
42}
43
44/// A route declared by a mapping annotation on a Java method or Kotlin function.
45pub(crate) fn route(def: Node, src: &[u8]) -> Option<RawContract> {
46    if !matches!(def.kind(), "method_declaration" | "function_declaration") {
47        return None;
48    }
49    let anns = annotations(def, src);
50    let (verb, path) = anns.iter().find_map(|a| {
51        if let Some((_, v)) = MAPPINGS.iter().find(|(n, _)| *n == a.name) {
52            return Some((v.to_string(), mapping_path(a, src)));
53        }
54        if a.name == "RequestMapping" {
55            let verb = arg(a, "method", usize::MAX)
56                .and_then(|n| value(n, src))
57                .and_then(|v| match v {
58                    ValueExpr::Name(p) => p.last().cloned(),
59                    _ => None,
60                })
61                .unwrap_or_else(|| "ANY".into());
62            return Some((verb, mapping_path(a, src)));
63        }
64        None
65    })?;
66    let class = def
67        .parent()
68        .filter(|b| matches!(b.kind(), "class_body" | "interface_body"))
69        .and_then(|b| b.parent())?;
70    let class_anns = annotations(class, src);
71    let client = class_anns.iter().any(|a| a.name == "FeignClient");
72    let prefix = class_anns.iter().find_map(|a| match a.name.as_str() {
73        "RequestMapping" => Some(mapping_path(a, src)),
74        "FeignClient" => arg(a, "path", usize::MAX).and_then(|n| value(n, src)),
75        _ => None,
76    });
77    let handler = text(
78        def.child_by_field_name("name").or_else(|| name_of(def))?,
79        src,
80    );
81    Some(RawContract::Route {
82        handler: handler.to_string(),
83        receiver: Vec::new(),
84        method: verb.to_uppercase(),
85        path: ValueExpr::Join {
86            sep: String::new(),
87            parts: vec![prefix.unwrap_or(ValueExpr::Lit(String::new())), path],
88        },
89        client,
90    })
91}
92
93fn name_of(def: Node) -> Option<Node> {
94    named_children(def)
95        .into_iter()
96        .find(|c| matches!(c.kind(), "identifier" | "simple_identifier"))
97}
98
99fn mapping_path(a: &Annotation<'_>, src: &[u8]) -> ValueExpr {
100    arg(a, "value", 0)
101        .or_else(|| arg(a, "path", usize::MAX))
102        .and_then(|n| value(n, src))
103        .unwrap_or(ValueExpr::Lit(String::new()))
104}
105
106fn arg<'t>(a: &Annotation<'t>, key: &str, pos: usize) -> Option<Node<'t>> {
107    a.args
108        .iter()
109        .find(|(k, _)| k.as_deref() == Some(key))
110        .or_else(|| a.args.iter().filter(|(k, _)| k.is_none()).nth(pos))
111        .map(|(_, n)| *n)
112}
113
114fn annotations<'t>(def: Node<'t>, src: &[u8]) -> Vec<Annotation<'t>> {
115    let mut out = Vec::new();
116    for m in named_children(def)
117        .into_iter()
118        .filter(|c| c.kind() == "modifiers")
119    {
120        for a in named_children(m) {
121            if let Some(ann) = annotation(a, src) {
122                out.push(ann);
123            }
124        }
125    }
126    out
127}
128
129fn annotation<'t>(a: Node<'t>, src: &[u8]) -> Option<Annotation<'t>> {
130    match a.kind() {
131        // Java
132        "marker_annotation" | "annotation" if a.child_by_field_name("name").is_some() => {
133            let name = last(text(a.child_by_field_name("name")?, src));
134            let mut args = Vec::new();
135            if let Some(list) = a.child_by_field_name("arguments") {
136                for c in named_children(list) {
137                    if c.kind() == "element_value_pair" {
138                        let k = c
139                            .child_by_field_name("key")
140                            .map(|k| text(k, src).to_string());
141                        if let Some(v) = c.child_by_field_name("value") {
142                            args.push((k, v));
143                        }
144                    } else {
145                        args.push((None, c));
146                    }
147                }
148            }
149            Some(Annotation { name, args })
150        }
151        // Kotlin
152        "annotation" => {
153            let inner = named_children(a).into_iter().next()?;
154            match inner.kind() {
155                "user_type" => Some(Annotation {
156                    name: last(text(inner, src)),
157                    args: Vec::new(),
158                }),
159                "constructor_invocation" => {
160                    let kids = named_children(inner);
161                    let name = last(text(*kids.first()?, src));
162                    let mut args = Vec::new();
163                    if let Some(list) = kids.iter().find(|k| k.kind() == "value_arguments") {
164                        for va in named_children(*list) {
165                            let parts = named_children(va);
166                            match parts.as_slice() {
167                                [k, v] if k.kind() == "identifier" => {
168                                    args.push((Some(text(*k, src).to_string()), *v))
169                                }
170                                [v] => args.push((None, *v)),
171                                _ => {}
172                            }
173                        }
174                    }
175                    Some(Annotation { name, args })
176                }
177                _ => None,
178            }
179        }
180        _ => None,
181    }
182}
183
184fn last(s: &str) -> String {
185    s.rsplit('.').next().unwrap_or(s).trim().to_string()
186}
187
188/// A statically readable Java/Kotlin expression: literals, names, `+` concatenation,
189/// `String.format`, and the first element of an array/collection literal.
190pub(crate) fn value(node: Node, src: &[u8]) -> Option<ValueExpr> {
191    match node.kind() {
192        "string_literal" => {
193            let kids = named_children(node);
194            if kids
195                .iter()
196                .all(|k| matches!(k.kind(), "string_fragment" | "string_content"))
197            {
198                return Some(ValueExpr::Lit(
199                    kids.iter().map(|k| text(*k, src)).collect::<String>(),
200                ));
201            }
202            Some(ValueExpr::Join {
203                sep: String::new(),
204                parts: kids
205                    .iter()
206                    .map(|k| match k.kind() {
207                        "string_fragment" | "string_content" => {
208                            ValueExpr::Lit(text(*k, src).to_string())
209                        }
210                        _ => named_children(*k)
211                            .first()
212                            .and_then(|e| value(*e, src))
213                            .unwrap_or(ValueExpr::Name(Vec::new())),
214                    })
215                    .collect(),
216            })
217        }
218        "identifier" | "simple_identifier" => Some(ValueExpr::Name(vec![text(node, src).into()])),
219        "field_access" | "navigation_expression" => Some(ValueExpr::Name(
220            text(node, src)
221                .split('.')
222                .map(|s| s.trim().to_string())
223                .collect(),
224        )),
225        "binary_expression" | "additive_expression" => {
226            let kids = named_children(node);
227            if !text(node, src).contains('+') || kids.len() != 2 {
228                return None;
229            }
230            Some(ValueExpr::Join {
231                sep: String::new(),
232                parts: kids
233                    .iter()
234                    .map(|k| value(*k, src).unwrap_or(ValueExpr::Name(Vec::new())))
235                    .collect(),
236            })
237        }
238        "element_value_array_initializer" | "collection_literal" | "array_initializer" => {
239            value(*named_children(node).first()?, src)
240        }
241        "parenthesized_expression" => value(*named_children(node).first()?, src),
242        _ => None,
243    }
244}
245
246/// Call arguments as values (Java `argument_list`, Kotlin `value_arguments`).
247pub(crate) fn args(list: Node, src: &[u8]) -> Args {
248    let mut out = Args::default();
249    for a in named_children(list) {
250        let v = if a.kind() == "value_argument" {
251            named_children(a).last().copied()
252        } else {
253            Some(a)
254        };
255        out.positional.push(v.and_then(|v| value(v, src)));
256    }
257    out
258}