diff --git a/plugins/official/mybatis/dist/plugin.wasm b/plugins/official/mybatis/dist/plugin.wasm index 4f71ecf9..5c7d59bc 100644 Binary files a/plugins/official/mybatis/dist/plugin.wasm and b/plugins/official/mybatis/dist/plugin.wasm differ diff --git a/plugins/official/mybatis/dist/plugin.wasm.sources b/plugins/official/mybatis/dist/plugin.wasm.sources index e59d759a..26af2f4b 100644 --- a/plugins/official/mybatis/dist/plugin.wasm.sources +++ b/plugins/official/mybatis/dist/plugin.wasm.sources @@ -1 +1 @@ -c29594e55d321ee5240f8f61223d72c9095695a5b19ff1f8a25c5a6402a4a736 +5e7f7adc2c49503d2278ed2a446f18f69561ce51df264100f77f50d69804c277 diff --git a/plugins/official/mybatis/src/lib.rs b/plugins/official/mybatis/src/lib.rs index d10b135a..f84cd018 100644 --- a/plugins/official/mybatis/src/lib.rs +++ b/plugins/official/mybatis/src/lib.rs @@ -70,6 +70,51 @@ fn ident(c: char) -> bool { c.is_alphanumeric() || c == '_' || c == '$' } +/// Index of the `(` opening a method's parameter list inside a declaration +/// fragment. `(`s that belong to annotation arguments — `@Anno(…)` — are +/// skipped so `@DataPermission(column = "id") int count()` finds the paren +/// after `count`, not the one after `DataPermission`. +fn method_paren(stmt: &str) -> Option { + let mut rest = stmt; + let mut base = 0; + while let Some(i) = rest.find('(') { + // Is the token right before this `(` an annotation name (`@Anno`, `@a.b.Anno`)? + let before = rest[..i].trim_end(); + let annotation = before.rfind('@').is_some_and(|a| before[a + 1..].chars().all(|c| ident(c) || c == '.')); + if !annotation { + return Some(base + i); + } + // Skip the annotation's argument list (it may nest and hold literals). + let mut depth = 1usize; + let mut end = None; + let mut chars = rest[i + 1..].char_indices().peekable(); + while let Some((j, c)) = chars.next() { + match c { + '"' | '\'' => { + for (_, d) in chars.by_ref() { + if d == c { + break; + } + } + } + '(' => depth += 1, + ')' => { + depth -= 1; + if depth == 0 { + end = Some(i + 1 + j + 1); + break; + } + } + _ => {} + } + } + let skip = end?; + base += skip; + rest = &rest[skip..]; + } + None +} + /// The interface's name and methods (with their parameter names). pub fn parse_java(text: &str) -> MapperJava { let package = @@ -95,7 +140,7 @@ pub fn parse_java(text: &str) -> MapperJava { } ';' if depth == 0 => { let stmt = &text[stmt_start..k]; - if let Some(paren) = stmt.find('(') { + if let Some(paren) = method_paren(stmt) { let before = stmt[..paren].trim_end(); let name_start = before.rfind(|c: char| !ident(c)).map_or(0, |p| p + 1); let method = &before[name_start..]; @@ -361,6 +406,17 @@ mod tests { assert_eq!(n.line_markers(&xml)[0].tooltip, "Go to UserMapper.findById()"); } + #[test] + fn method_annotations_do_not_swallow_the_method_name() { + // A method-level annotation with arguments: `stmt.find('(')` would land + // inside `@DataPermission(…)` and read `DataPermission` as the name. + let java = parse_java( + "package com.acme;\n\npublic interface SecuredMapper {\n @DataPermission(column = \"id\", scope = (@Scope(\"self\")))\n List findAll(@Param(\"org\") String org);\n\n @org.acme.Check(value = \"(nested)\")\n int count();\n}\n", + ); + let names: Vec<_> = java.methods.iter().map(|(m, p)| (m.name.as_str(), p.clone())).collect(); + assert_eq!(names, [("findAll", vec!["org".to_owned()]), ("count", Vec::::new())]); + } + #[test] fn completes_ids_and_parameters() { let _host = setup();