util.rs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. //! Shared utilities for the TS/JS walker: compiled regexes, UTF-16 position
  2. //! conversion, generated-file detection, and small text helpers — each
  3. //! mirroring a specific helper in src/extraction/tree-sitter.ts (noted inline).
  4. use regex::Regex;
  5. use std::sync::OnceLock;
  6. macro_rules! re {
  7. ($name:ident, $pat:expr) => {
  8. pub fn $name() -> &'static Regex {
  9. static RE: OnceLock<Regex> = OnceLock::new();
  10. RE.get_or_init(|| Regex::new($pat).expect(concat!("regex ", stringify!($name))))
  11. }
  12. };
  13. }
  14. // RTK_HOOK_NAME_RE (tree-sitter.ts)
  15. re!(rtk_hook_name, r"^use[A-Z][A-Za-z0-9]*(?:Query|Mutation)$");
  16. // reactComponentHoc's styled test
  17. re!(styled_callee, r"^styled\b");
  18. // PascalCase component gate (#841)
  19. re!(pascal_case, r"^[A-Z]");
  20. // extractCall parenthesized-conversion normalization
  21. re!(paren_conversion, r"^\(\s*\*?\s*([A-Za-z_][\w.]*)\s*\)$");
  22. // flushFnRefCandidates SIMPLE_NAME
  23. re!(simple_name, r"^[A-Za-z_$][A-Za-z0-9_$]*$");
  24. // flushFnRefCandidates QUALIFIED_IMPORT
  25. re!(qualified_import, r"^[A-Za-z_$][A-Za-z0-9_$.\\]*[.\\]([A-Za-z_$][A-Za-z0-9_$]*)$");
  26. // captureFnRefCandidates rhs param-storage skip — trailing identifier of LHS
  27. re!(lhs_last_name, r"([A-Za-z_$][A-Za-z0-9_$]*)\s*$");
  28. // extractTsTupleContractNames identifier test
  29. re!(ident_dollar, r"^[A-Za-z_$][A-Za-z0-9_$]*$");
  30. // looksLikeVueStoreFile signal (VUE_STORE_FILE_SIGNAL)
  31. re!(
  32. vue_store_signal,
  33. r"\bdefineStore\b|\bcreateStore\b|\bVuex\b|\bmutations\b|\bactions\b|\bgetters\b|\bnamespaced\b"
  34. );
  35. // value-ref target-name distinctiveness: /[A-Z_]/
  36. re!(has_upper_or_underscore, r"[A-Z_]");
  37. /// isGeneratedFile (src/extraction/generated-detection.ts) — full pattern list
  38. /// ported so future language walkers share it.
  39. pub fn is_generated_file(file_path: &str) -> bool {
  40. static RES: OnceLock<Vec<Regex>> = OnceLock::new();
  41. let patterns = RES.get_or_init(|| {
  42. [
  43. r"\.pb\.go$",
  44. r"\.pulsar\.go$",
  45. r"_grpc\.pb\.go$",
  46. r"_mock\.go$",
  47. r"_mocks\.go$",
  48. r"^mock_[^/]+\.go$",
  49. r"\.generated\.[jt]sx?$",
  50. r"\.gen\.[jt]sx?$",
  51. r"\.pb\.[jt]s$",
  52. r"_pb\.[jt]s$",
  53. r"_grpc_pb\.[jt]s$",
  54. r"\.min\.m?js$",
  55. r"_pb2(_grpc)?\.py$",
  56. r"_pb2\.pyi$",
  57. r"\.pb\.(cc|h)$",
  58. r"\.g\.cs$",
  59. r"Grpc\.cs$",
  60. r"OuterClass\.java$",
  61. r"Grpc\.java$",
  62. r"\.pb\.swift$",
  63. r"\.g\.dart$",
  64. r"\.freezed\.dart$",
  65. r"\.pb\.dart$",
  66. r"\.pbgrpc\.dart$",
  67. r"\.chopper\.dart$",
  68. r"\.generated\.rs$",
  69. ]
  70. .iter()
  71. .map(|p| Regex::new(p).expect("generated pattern"))
  72. .collect()
  73. });
  74. patterns.iter().any(|p| p.is_match(file_path))
  75. }
  76. /// Byte offsets of each line start, for UTF-16 column conversion.
  77. pub fn line_starts(src: &str) -> Vec<usize> {
  78. let mut out = vec![0usize];
  79. for (i, b) in src.bytes().enumerate() {
  80. if b == b'\n' {
  81. out.push(i + 1);
  82. }
  83. }
  84. out
  85. }
  86. /// UTF-16 code units in `s` — what web-tree-sitter (and JS string ops)
  87. /// count, so kernel-emitted columns are byte-identical to the wasm path's.
  88. pub fn utf16_len(s: &str) -> usize {
  89. s.chars().map(|c| c.len_utf16()).sum()
  90. }
  91. /// Column (UTF-16 units) of `byte_pos` on line `row`, given `line_starts`.
  92. pub fn col16(src: &str, starts: &[usize], row: usize, byte_pos: usize) -> u32 {
  93. let ls = starts.get(row).copied().unwrap_or(0);
  94. if byte_pos <= ls {
  95. return 0;
  96. }
  97. utf16_len(&src[ls..byte_pos]) as u32
  98. }
  99. /// JS `String.prototype.slice(0, n)` in UTF-16 units, without splitting a
  100. /// surrogate pair (when the cut would split one, we stop one code unit short —
  101. /// a lone surrogate isn't representable in Rust and never round-trips through
  102. /// SQLite anyway). Returns (sliced, was_truncated_at_or_beyond_n).
  103. pub fn slice_utf16(s: &str, n: usize) -> (String, bool) {
  104. let mut used = 0usize;
  105. let mut out = String::new();
  106. for c in s.chars() {
  107. let w = c.len_utf16();
  108. if used + w > n {
  109. return (out, true);
  110. }
  111. used += w;
  112. out.push(c);
  113. if used == n {
  114. // Exactly at the limit: truncated iff any source remains.
  115. let truncated = out.len() < s.len();
  116. return (out, truncated);
  117. }
  118. }
  119. (out, false)
  120. }
  121. /// objectKeyName (tree-sitter.ts): strip ONE leading and ONE trailing quote
  122. /// character (`'`, `"`, or backtick).
  123. pub fn object_key_name(s: &str) -> String {
  124. let mut out = s;
  125. if let Some(first) = out.chars().next() {
  126. if first == '\'' || first == '"' || first == '`' {
  127. out = &out[first.len_utf8()..];
  128. }
  129. }
  130. if let Some(last) = out.chars().last() {
  131. if last == '\'' || last == '"' || last == '`' {
  132. out = &out[..out.len() - last.len_utf8()];
  133. }
  134. }
  135. out.to_string()
  136. }
  137. /// The `= <first 100 UTF-16 units>[...]` initializer signature used by
  138. /// extractVariable (its `.length >= 100` check fires exactly when the slice
  139. /// hit the cap).
  140. pub fn init_signature(value_text: &str) -> String {
  141. let (sliced, _) = slice_utf16(value_text, 100);
  142. if utf16_len(&sliced) >= 100 {
  143. format!("= {sliced}...")
  144. } else {
  145. format!("= {sliced}")
  146. }
  147. }
  148. #[cfg(test)]
  149. mod tests {
  150. use super::*;
  151. #[test]
  152. fn utf16_cols() {
  153. let src = "aé😀b";
  154. // 'a'=1, 'é'=1, '😀'=2 utf16 units; bytes: a=1, é=2, 😀=4
  155. assert_eq!(utf16_len(src), 5);
  156. let starts = line_starts(src);
  157. assert_eq!(col16(src, &starts, 0, 1), 1); // after 'a'
  158. assert_eq!(col16(src, &starts, 0, 3), 2); // after 'é'
  159. assert_eq!(col16(src, &starts, 0, 7), 4); // after '😀'
  160. }
  161. #[test]
  162. fn init_sig_short_and_long() {
  163. assert_eq!(init_signature("[1, 2]"), "= [1, 2]");
  164. let long = "x".repeat(150);
  165. let sig = init_signature(&long);
  166. assert!(sig.starts_with("= "));
  167. assert!(sig.ends_with("..."));
  168. assert_eq!(utf16_len(&sig[2..sig.len() - 3]), 100);
  169. }
  170. #[test]
  171. fn generated_patterns() {
  172. assert!(is_generated_file("src/api.generated.ts"));
  173. assert!(is_generated_file("vendor/jquery.min.js"));
  174. assert!(!is_generated_file("src/app.ts"));
  175. }
  176. }