A lexicon-driven AppView for ATProto.
0

Configure Feed

Select the types of activity you want to include in your feed.

happyview / src / lua_analysis.rs
7.5 kB 240 lines
1use regex::Regex; 2use std::sync::LazyLock; 3 4/// Matches an xrpc.query or xrpc.procedure call and captures the method name. 5static XRPC_CALL_RE: LazyLock<Regex> = LazyLock::new(|| { 6 Regex::new(r#"xrpc\.(?:query|procedure)\(\s*["']([a-zA-Z][a-zA-Z0-9]*(?:\.[a-zA-Z][a-zA-Z0-9]*)*)["']"#).unwrap() 7}); 8 9/// Matches a Lua line comment at the start of the non-whitespace content on a line. 10/// Used to detect lines that are fully commented out before any code. 11static LUA_COMMENT_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*--").unwrap()); 12 13/// Strips Lua block comments (`--[[ ... ]]`) from source, including multi-line ones. 14static BLOCK_COMMENT_RE: LazyLock<Regex> = 15 LazyLock::new(|| Regex::new(r"--\[\[[\s\S]*?\]\]").unwrap()); 16 17pub fn extract_outbound_xrpcs(source: &str) -> Vec<String> { 18 let mut seen = std::collections::HashSet::new(); 19 let mut result = Vec::new(); 20 21 let stripped = BLOCK_COMMENT_RE.replace_all(source, ""); 22 23 for line in stripped.lines() { 24 if LUA_COMMENT_RE.is_match(line) { 25 continue; 26 } 27 28 for cap in XRPC_CALL_RE.captures_iter(line) { 29 if let Some(method) = cap.get(1) { 30 let method = method.as_str().to_string(); 31 if seen.insert(method.clone()) { 32 result.push(method); 33 } 34 } 35 } 36 } 37 38 result 39} 40 41#[cfg(test)] 42mod tests { 43 use super::*; 44 45 #[test] 46 fn empty_script_returns_empty() { 47 let result = extract_outbound_xrpcs(""); 48 assert!(result.is_empty()); 49 } 50 51 #[test] 52 fn no_xrpc_calls_returns_empty() { 53 let source = r#" 54 local record = params.record 55 return { records = { record } } 56 "#; 57 let result = extract_outbound_xrpcs(source); 58 assert!(result.is_empty()); 59 } 60 61 #[test] 62 fn detects_xrpc_query_call() { 63 let source = r#" 64 local result = xrpc.query("games.birb.chess.getGame", { uri = params.uri }) 65 return result 66 "#; 67 let result = extract_outbound_xrpcs(source); 68 assert_eq!(result, vec!["games.birb.chess.getGame"]); 69 } 70 71 #[test] 72 fn detects_xrpc_procedure_call() { 73 let source = r#" 74 xrpc.procedure("games.birb.chess.makeMove", { game = params.game, move = params.move }) 75 "#; 76 let result = extract_outbound_xrpcs(source); 77 assert_eq!(result, vec!["games.birb.chess.makeMove"]); 78 } 79 80 #[test] 81 fn detects_multiple_calls() { 82 let source = r#" 83 local game = xrpc.query("games.birb.chess.getGame", { uri = params.uri }) 84 xrpc.procedure("games.birb.chess.makeMove", { game = game.uri, move = params.move }) 85 local games = xrpc.query("games.birb.chess.listGames", {}) 86 "#; 87 let result = extract_outbound_xrpcs(source); 88 assert_eq!( 89 result, 90 vec![ 91 "games.birb.chess.getGame", 92 "games.birb.chess.makeMove", 93 "games.birb.chess.listGames", 94 ] 95 ); 96 } 97 98 #[test] 99 fn deduplicates_repeated_calls() { 100 let source = r#" 101 local a = xrpc.query("games.birb.chess.getGame", { uri = "a" }) 102 local b = xrpc.query("games.birb.chess.getGame", { uri = "b" }) 103 "#; 104 let result = extract_outbound_xrpcs(source); 105 assert_eq!(result, vec!["games.birb.chess.getGame"]); 106 } 107 108 #[test] 109 fn ignores_commented_out_calls() { 110 let source = r#" 111 -- local result = xrpc.query("games.birb.chess.getGame", { uri = params.uri }) 112 return {} 113 "#; 114 let result = extract_outbound_xrpcs(source); 115 assert!(result.is_empty()); 116 } 117 118 #[test] 119 fn handles_single_quotes_and_double_quotes() { 120 let source = r#" 121 local a = xrpc.query('games.birb.chess.getGame', {}) 122 local b = xrpc.query("games.birb.chess.listGames", {}) 123 "#; 124 let result = extract_outbound_xrpcs(source); 125 assert_eq!( 126 result, 127 vec!["games.birb.chess.getGame", "games.birb.chess.listGames",] 128 ); 129 } 130 131 #[test] 132 fn handles_multiline_scripts() { 133 let source = r#" 134function handle(input, params) 135 local game = xrpc.query("games.birb.chess.getGame", 136 { uri = params.uri }) 137 local result = xrpc.procedure("games.birb.chess.makeMove", 138 { game = game.uri, 139 move = params.move }) 140 return result 141end 142 "#; 143 let result = extract_outbound_xrpcs(source); 144 assert_eq!( 145 result, 146 vec!["games.birb.chess.getGame", "games.birb.chess.makeMove",] 147 ); 148 } 149 150 #[test] 151 fn handles_mixed_comments_and_code() { 152 let source = r#" 153function handle(input, params) 154 -- This is a comment about the next call 155 local game = xrpc.query("games.birb.chess.getGame", { uri = params.uri }) 156 -- local old = xrpc.query("games.birb.chess.oldEndpoint", {}) 157 -- xrpc.procedure("games.birb.chess.deprecatedMove", {}) 158 xrpc.procedure("games.birb.chess.makeMove", { game = game.uri }) 159 return game 160end 161 "#; 162 let result = extract_outbound_xrpcs(source); 163 assert_eq!( 164 result, 165 vec!["games.birb.chess.getGame", "games.birb.chess.makeMove",] 166 ); 167 } 168 169 #[test] 170 fn ignores_block_comment_single_line() { 171 let source = r#" 172 --[[ local result = xrpc.query("games.birb.chess.getGame", { uri = params.uri }) ]] 173 return {} 174 "#; 175 let result = extract_outbound_xrpcs(source); 176 assert!(result.is_empty()); 177 } 178 179 #[test] 180 fn ignores_block_comment_multiline() { 181 let source = r#" 182 --[[ 183 local result = xrpc.query("games.birb.chess.getGame", { uri = params.uri }) 184 xrpc.procedure("games.birb.chess.makeMove", {}) 185 ]] 186 local active = xrpc.query("games.birb.chess.listGames", {}) 187 "#; 188 let result = extract_outbound_xrpcs(source); 189 assert_eq!(result, vec!["games.birb.chess.listGames"]); 190 } 191 192 #[test] 193 fn dynamic_method_names_not_detected() { 194 let source = r#" 195 local method = "games.birb.chess.getGame" 196 local result = xrpc.query(method, {}) 197 "#; 198 let result = extract_outbound_xrpcs(source); 199 assert!( 200 result.is_empty(), 201 "dynamically constructed method names should not be detected" 202 ); 203 } 204 205 #[test] 206 fn extracts_from_complex_lua() { 207 let source = r#" 208function handle(input, params) 209 local results = {} 210 211 if params.include_profile then 212 local profile = xrpc.query("app.bsky.actor.getProfile", { actor = params.did }) 213 table.insert(results, profile) 214 end 215 216 for i = 1, params.count do 217 local feed = xrpc.query("app.bsky.feed.getAuthorFeed", { actor = params.did, limit = 10 }) 218 for _, post in ipairs(feed.feed) do 219 table.insert(results, post) 220 end 221 end 222 223 if params.should_notify then 224 xrpc.procedure("games.birb.chess.sendNotification", { target = params.did }) 225 end 226 227 return { items = results } 228end 229 "#; 230 let result = extract_outbound_xrpcs(source); 231 assert_eq!( 232 result, 233 vec![ 234 "app.bsky.actor.getProfile", 235 "app.bsky.feed.getAuthorFeed", 236 "games.birb.chess.sendNotification", 237 ] 238 ); 239 } 240}