1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use once_cell::sync::Lazy;
5use rayon::prelude::*;
6use regex::Regex;
7use rustc_hash::FxHashSet;
8
9use crate::config::fragmentation::FRAGMENTATION;
10use crate::config::limits::LIMITS;
11use crate::config::tokenization::TOKENIZATION;
12use crate::git::{self, CatFileBatch};
13use crate::parsers::fragment_file;
14use crate::tokenizer::count_tokens;
15use crate::types::{Fragment, FragmentId, FragmentKind, extract_identifiers};
16
17static BINARY_CTRL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\x00").unwrap());
22
23static GENERATED_FILENAME_PATTERNS: Lazy<FxHashSet<&'static str>> = Lazy::new(|| {
24 [
25 ".pb.go",
26 "_pb2.py",
27 "_pb2_grpc.py",
28 ".pb.h",
29 ".pb.cc",
30 ".pb.swift",
31 ".min.js",
32 ".min.css",
33 ".designer.cs",
34 ".api",
35 ]
36 .into_iter()
37 .collect()
38});
39
40const GENERATED_FILENAME_SUFFIXES: &[&str] = &["_generated.", "OuterClass.java"];
41
42static GENERATED_PATH_SEGMENTS: Lazy<FxHashSet<&'static str>> = Lazy::new(|| {
43 [
44 "generated",
45 "gen-java",
46 "gen-go",
47 "gen-py",
48 "gen-cpp",
49 "gen-swift",
50 "__generated__",
51 "autogen",
52 "codegen",
53 ]
54 .into_iter()
55 .collect()
56});
57
58const GENERATED_CONTENT_MARKERS: &[&str] = &[
59 "@generated",
60 "do not edit",
61 "code generated",
62 "auto-generated",
63 "this file is generated",
64 "generated by",
65 "automatically generated",
66 "auto generated",
67];
68
69static KNOWN_BINARY_EXTENSIONS: Lazy<FxHashSet<&'static str>> = Lazy::new(|| {
70 [
71 ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".svg", ".webp", ".mp3", ".mp4", ".wav",
72 ".ogg", ".flac", ".avi", ".mkv", ".mov", ".zip", ".gz", ".tar", ".bz2", ".xz", ".7z",
73 ".rar", ".jar", ".war", ".ear", ".class", ".pyc", ".pyo", ".o", ".a", ".so", ".dylib",
74 ".dll", ".exe", ".bin", ".dat", ".db", ".sqlite", ".pdf", ".doc", ".docx", ".xls", ".xlsx",
75 ".ppt", ".pptx", ".woff", ".woff2", ".ttf", ".otf", ".eot",
76 ]
77 .into_iter()
78 .collect()
79});
80
81fn looks_binary(content: &str) -> bool {
82 let mut check_len = content
83 .len()
84 .min(FRAGMENTATION.binary_detection_buffer_size);
85 while check_len > 0 && !content.is_char_boundary(check_len) {
86 check_len -= 1;
87 }
88 BINARY_CTRL_RE.is_match(&content[..check_len])
89}
90
91fn has_generated_filename(name: &str) -> bool {
92 GENERATED_FILENAME_PATTERNS
93 .iter()
94 .any(|p| name.ends_with(p))
95 || GENERATED_FILENAME_SUFFIXES
96 .iter()
97 .any(|s| name.ends_with(s))
98}
99
100fn has_generated_path_segment(path: &Path) -> bool {
101 path.components().any(|c| {
102 let s = c.as_os_str().to_string_lossy().to_lowercase();
103 GENERATED_PATH_SEGMENTS.contains(s.as_str())
104 })
105}
106
107fn has_generated_content_marker(content: &str) -> bool {
108 let header: String = content
109 .lines()
110 .take(FRAGMENTATION.generated_marker_header_lines)
111 .collect::<Vec<_>>()
112 .join("\n")
113 .to_lowercase();
114 for marker in GENERATED_CONTENT_MARKERS {
115 if !header.contains(marker) {
116 continue;
117 }
118 if *marker != "@generated" {
119 return true;
120 }
121 if header.contains("@generated") {
122 let after_idx = header.find("@generated").unwrap() + "@generated".len();
123 let next_char = header[after_idx..].chars().next();
124 if next_char.is_none() || !next_char.unwrap().is_ascii_lowercase() {
125 return true;
126 }
127 }
128 }
129 false
130}
131
132fn is_generated_file(path: &Path, content: &str) -> bool {
133 let name = path
134 .file_name()
135 .map(|n| n.to_string_lossy().to_string())
136 .unwrap_or_default();
137 has_generated_filename(&name)
138 || has_generated_path_segment(path)
139 || has_generated_content_marker(content)
140}
141
142fn truncate_generated_fragments(file_frags: Vec<Fragment>) -> Vec<Fragment> {
143 let max_lines = LIMITS.max_generated_lines as u32;
144 file_frags
145 .into_iter()
146 .map(|frag| {
147 if frag.line_count() <= max_lines {
148 return frag;
149 }
150 let lines: Vec<&str> = frag.content.lines().collect();
151 let remaining = lines.len() - max_lines as usize;
152 let truncated_lines = &lines[..max_lines as usize];
153 let truncated_content = format!(
154 "{}\n# ... [{} more lines]",
155 truncated_lines.join("\n"),
156 remaining
157 );
158 let new_end = frag.start_line() + max_lines - 1;
159 let identifiers = extract_identifiers(
160 &truncated_content,
161 TOKENIZATION.fragment_min_identifier_length,
162 );
163 Fragment {
164 id: FragmentId::new(frag.id.path.clone(), frag.start_line(), new_end),
165 kind: frag.kind,
166 content: Arc::from(truncated_content),
167 identifiers,
168 token_count: 0,
169 symbol_name: frag.symbol_name,
170 }
171 })
172 .collect()
173}
174
175fn dedup_fragments(raw_frags: Vec<Fragment>, seen: &mut FxHashSet<FragmentId>) -> Vec<Fragment> {
176 let mut result = Vec::new();
177 for f in raw_frags {
178 if !seen.contains(&f.id) {
179 seen.insert(f.id.clone());
180 result.push(f);
181 }
182 }
183 result
184}
185
186fn normalize_path(path: &Path, root_dir: &Path) -> PathBuf {
187 if path.is_absolute() {
188 path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
189 } else {
190 let joined = root_dir.join(path);
191 joined.canonicalize().unwrap_or_else(|_| joined)
192 }
193}
194
195fn read_file_content(
196 file_path: &Path,
197 root_dir: &Path,
198 preferred_revs: &[String],
199 mut batch_reader: Option<&mut CatFileBatch>,
200 is_changed: bool,
201) -> Option<String> {
202 let ext = file_path
203 .extension()
204 .map(|e| format!(".{}", e.to_string_lossy().to_lowercase()))
205 .unwrap_or_default();
206 if KNOWN_BINARY_EXTENSIONS.contains(ext.as_str()) {
207 return None;
208 }
209
210 let abs_path = normalize_path(file_path, root_dir);
211 let resolved_root = root_dir
212 .canonicalize()
213 .unwrap_or_else(|_| root_dir.to_path_buf());
214 let rel = abs_path.strip_prefix(&resolved_root).ok()?;
215
216 let max_size = if is_changed {
217 LIMITS.max_changed_file_size
218 } else {
219 LIMITS.max_file_size
220 };
221 for rev in preferred_revs {
222 if let Some(reader) = batch_reader.as_deref_mut() {
223 match reader.get(rev, rel) {
224 Ok(content) if content.len() <= max_size && !looks_binary(&content) => {
225 return Some(content);
226 }
227 _ => continue,
228 }
229 } else {
230 match git::show_file_at_revision(root_dir, rev, rel) {
231 Ok(content) if content.len() <= max_size && !looks_binary(&content) => {
232 return Some(content);
233 }
234 _ => continue,
235 }
236 }
237 }
238
239 if abs_path.exists() && abs_path.is_file() {
240 if let Ok(meta) = std::fs::metadata(&abs_path) {
241 if meta.len() as usize > max_size {
242 return None;
243 }
244 }
245 if let Ok(content) = std::fs::read_to_string(&abs_path) {
246 if !looks_binary(&content) {
247 return Some(content);
248 }
249 }
250 }
251
252 None
253}
254
255pub fn process_files_for_fragments(
256 files: &[PathBuf],
257 root_dir: &Path,
258 preferred_revs: &[String],
259 seen_frag_ids: &mut FxHashSet<FragmentId>,
260 mut batch_reader: Option<&mut CatFileBatch>,
261 is_changed: bool,
262) -> Vec<Fragment> {
263 let max_frags = LIMITS.max_fragments;
264 let max_generated = LIMITS.max_generated_fragments;
265
266 let chunk_size = rayon::current_num_threads().max(1);
270 let mut parsed: Vec<Vec<Fragment>> = Vec::with_capacity(files.len());
271 for chunk in files.chunks(chunk_size) {
272 let chunk_contents: Vec<(PathBuf, String)> = chunk
273 .iter()
274 .filter_map(|file_path| {
275 let content = read_file_content(
276 file_path,
277 root_dir,
278 preferred_revs,
279 batch_reader.as_deref_mut(),
280 is_changed,
281 )?;
282 Some((file_path.clone(), content))
283 })
284 .collect();
285 parsed.extend(
286 chunk_contents
287 .par_iter()
288 .map(|(file_path, content)| {
289 let path_arc: Arc<str> = Arc::from(file_path.to_string_lossy().as_ref());
290 let mut raw_frags = fragment_file(path_arc, content);
291 let generated = !is_changed && is_generated_file(file_path, content);
296 let cap = if generated {
302 max_generated
303 } else if is_changed {
304 max_frags.saturating_mul(10)
305 } else {
306 max_frags
307 };
308 if raw_frags.len() > cap {
309 raw_frags.sort_by(|a, b| b.line_count().cmp(&a.line_count()));
310 raw_frags.truncate(cap);
311 }
312 if generated {
313 raw_frags = truncate_generated_fragments(raw_frags);
314 }
315 raw_frags
316 })
317 .collect::<Vec<_>>(),
318 );
319 }
321
322 let mut fragments: Vec<Fragment> = Vec::new();
323 for file_frags in parsed {
324 for frag in dedup_fragments(file_frags, seen_frag_ids) {
325 seen_frag_ids.insert(frag.id.clone());
326 fragments.push(frag);
327 }
328 }
329
330 fragments
331}
332
333pub fn create_whole_file_fragment(
334 path: &Path,
335 root_dir: &Path,
336 preferred_revs: &[String],
337 batch_reader: Option<&mut CatFileBatch>,
338) -> Option<Fragment> {
339 let content = read_file_content(path, root_dir, preferred_revs, batch_reader, true)?;
340 let trimmed = content.trim();
341 if trimmed.is_empty() {
342 return None;
343 }
344
345 let content = if is_generated_file(path, &content) {
346 let lines: Vec<&str> = content.lines().collect();
347 let max_lines = LIMITS.max_generated_lines;
348 if lines.len() > max_lines {
349 let remaining = lines.len() - max_lines;
350 format!(
351 "{}\n# ... [{} more lines]",
352 lines[..max_lines].join("\n"),
353 remaining
354 )
355 } else {
356 content
357 }
358 } else {
359 content
360 };
361
362 let lines: Vec<&str> = content.lines().collect();
363 let line_count = lines.len() as u32;
364 let path_arc: Arc<str> = Arc::from(path.to_string_lossy().as_ref());
365 let token_count = count_tokens(&content) + LIMITS.overhead_per_fragment;
366 let identifiers = extract_identifiers(&content, TOKENIZATION.fragment_min_identifier_length);
367
368 Some(Fragment {
369 id: FragmentId::new(path_arc, 1, line_count),
370 kind: FragmentKind::Chunk,
371 content: Arc::from(content),
372 identifiers,
373 token_count,
374 symbol_name: None,
375 })
376}