1use crate::license_detection::LicenseDetectionError;
10use crate::license_detection::index::LicenseIndex;
11use crate::license_detection::index::dictionary::{KnownToken, QueryToken, TokenId, TokenKind};
12use crate::license_detection::models::PositionSpan;
13use crate::license_detection::position_set::PositionSet;
14use crate::license_detection::spdx_lid::split_spdx_lid;
15use crate::license_detection::tokenize::STOPWORDS;
16use crate::license_detection::tokenize::tokenize_as_ids;
17use regex::Regex;
18use std::cell::{OnceCell, RefCell};
19use std::collections::HashMap;
20use std::sync::LazyLock;
21use std::time::Instant;
22
23static QUERY_PATTERN: LazyLock<Regex> =
24 LazyLock::new(|| Regex::new(r"[^_\W]+\+?[^_\W]*").expect("valid query regex"));
25static MATCHED_TEXT_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
26 Regex::new(r"(?P<token>[^_\W]+\+?[^_\W]*)|(?P<punct>[_\W\s\+]+[_\W\s]?)")
27 .expect("valid matched text regex")
28});
29
30#[derive(Clone)]
31struct MatchedTextToken {
32 value: String,
33 line_num: usize,
34 pos: Option<usize>,
35 is_text: bool,
36 is_matched: bool,
37}
38
39#[derive(Debug)]
51pub struct Query<'a> {
52 pub text: String,
56
57 pub tokens: Vec<TokenId>,
61
62 pub line_by_pos: Vec<usize>,
69
70 pub unknowns_by_pos: HashMap<Option<usize>, usize>,
78
79 pub stopwords_by_pos: HashMap<Option<usize>, usize>,
85
86 pub shorts_and_digits_pos: PositionSet,
92
93 pub high_matchables: PositionSet,
99
100 pub low_matchables: PositionSet,
106
107 pub is_binary: bool,
111
112 pub(crate) query_run_ranges: Vec<(usize, Option<usize>)>,
118
119 pub spdx_lines: Vec<(String, usize, usize)>,
126
127 pub(crate) line_content_ranges: std::sync::OnceLock<Vec<std::ops::Range<usize>>>,
135
136 pub index: &'a LicenseIndex,
138}
139
140pub fn matched_text_from_text(text: &str, start_line: usize, end_line: usize) -> String {
141 if start_line == 0 || end_line == 0 || start_line > end_line {
142 return String::new();
143 }
144
145 text.lines()
146 .enumerate()
147 .filter_map(|(idx, line)| {
148 let line_num = idx + 1;
149 if line_num >= start_line && line_num <= end_line {
150 Some(line)
151 } else {
152 None
153 }
154 })
155 .collect::<Vec<_>>()
156 .join("\n")
157}
158
159pub fn matched_text_diagnostics_from_text(
160 text: &str,
161 query: &Query<'_>,
162 matched_positions: &PositionSet,
163 start_pos: usize,
164 end_pos: usize,
165 start_line: usize,
166 end_line: usize,
167) -> String {
168 let tokens = tokenize_matched_text(text, query);
169 let reportable_tokens = collect_reportable_tokens(
170 tokens,
171 matched_positions,
172 start_pos,
173 end_pos,
174 start_line,
175 end_line,
176 );
177 let line_endings = collect_line_endings(text);
178
179 render_diagnostic_tokens(&reportable_tokens, &line_endings)
180}
181
182pub fn matched_text_from_tokens(
192 text: &str,
193 query: &Query<'_>,
194 matched_positions: &PositionSet,
195 start_pos: usize,
196 end_pos: usize,
197 start_line: usize,
198 end_line: usize,
199) -> String {
200 let tokens = tokenize_matched_text(text, query);
201 let reportable_tokens = collect_reportable_tokens(
202 tokens,
203 matched_positions,
204 start_pos,
205 end_pos,
206 start_line,
207 end_line,
208 );
209 let line_endings = collect_line_endings(text);
210
211 render_plain_tokens(&reportable_tokens, &line_endings)
212}
213
214fn render_plain_tokens(tokens: &[MatchedTextToken], line_endings: &[String]) -> String {
215 let mut rendered = String::new();
216 let mut previous_line: Option<usize> = None;
217
218 for token in tokens {
219 if let Some(prev_line) = previous_line
220 && token.line_num > prev_line
221 {
222 for line in prev_line..token.line_num {
223 if let Some(line_ending) = line_endings.get(line.saturating_sub(1)) {
224 rendered.push_str(line_ending.as_str());
225 }
226 }
227 }
228
229 let token_value = if token.is_text {
230 token.value.as_str()
231 } else {
232 token
233 .value
234 .strip_suffix("\r\n")
235 .or_else(|| token.value.strip_suffix('\n'))
236 .unwrap_or(token.value.as_str())
237 };
238
239 rendered.push_str(token_value);
240
241 previous_line = Some(token.line_num);
242 }
243
244 rendered
245}
246
247fn tokenize_matched_text(text: &str, query: &Query<'_>) -> Vec<MatchedTextToken> {
248 let mut tokens = Vec::new();
249 let mut pos = 0usize;
250 for (line_num, line) in (1usize..).zip(text.split_inclusive('\n')) {
251 for capture in MATCHED_TEXT_PATTERN.captures_iter(line) {
252 if let Some(token_match) = capture.name("token") {
253 let token_text = token_match.as_str();
254 let retokenized: Vec<String> = QUERY_PATTERN
255 .find_iter(&token_text.to_lowercase())
256 .map(|m| m.as_str().to_string())
257 .filter(|token| !STOPWORDS.contains(token.as_str()))
258 .collect();
259
260 if retokenized.is_empty() {
261 tokens.push(MatchedTextToken {
262 value: token_text.to_string(),
263 line_num,
264 pos: None,
265 is_text: true,
266 is_matched: false,
267 });
268 } else if retokenized.len() == 1 {
269 let token = &retokenized[0];
270 let token_pos = if query.index.dictionary.get(token).is_some() {
271 let current_pos = pos;
272 pos += 1;
273 Some(current_pos)
274 } else {
275 None
276 };
277
278 tokens.push(MatchedTextToken {
279 value: token_text.to_string(),
280 line_num,
281 pos: token_pos,
282 is_text: true,
283 is_matched: false,
284 });
285 } else {
286 for token in retokenized {
287 let token_pos = if query.index.dictionary.get(&token).is_some() {
288 let current_pos = pos;
289 pos += 1;
290 Some(current_pos)
291 } else {
292 None
293 };
294
295 tokens.push(MatchedTextToken {
296 value: token,
297 line_num,
298 pos: token_pos,
299 is_text: true,
300 is_matched: false,
301 });
302 }
303 }
304 } else if let Some(punct_match) = capture.name("punct") {
305 tokens.push(MatchedTextToken {
306 value: punct_match.as_str().to_string(),
307 line_num,
308 pos: None,
309 is_text: false,
310 is_matched: false,
311 });
312 }
313 }
314 }
315
316 tokens
317}
318
319fn collect_reportable_tokens(
320 tokens: Vec<MatchedTextToken>,
321 matched_positions: &PositionSet,
322 start_pos: usize,
323 end_pos: usize,
324 start_line: usize,
325 end_line: usize,
326) -> Vec<MatchedTextToken> {
327 let mut reportable = Vec::new();
328 let mut started = false;
329 let mut finished = false;
330 let mut end_real_pos = None;
331 let mut last_real_pos = None;
332
333 for (real_pos, mut token) in tokens.into_iter().enumerate() {
334 if token.line_num < start_line {
335 continue;
336 }
337
338 if token.line_num > end_line {
339 break;
340 }
341
342 let mut is_included = false;
343
344 if token.pos.is_some_and(|pos| matched_positions.contains(pos)) {
345 token.is_matched = true;
346 is_included = true;
347 }
348
349 if !started && token.pos == Some(start_pos) {
350 started = true;
351 is_included = true;
352 }
353
354 if started && !finished {
355 is_included = true;
356 }
357
358 if token.pos == Some(end_pos) {
359 finished = true;
360 started = false;
361 end_real_pos = Some(real_pos);
362 }
363
364 if finished && !started && end_real_pos.is_some() && last_real_pos == end_real_pos {
365 end_real_pos = None;
366 if !token.is_text && !token.value.trim().is_empty() {
367 is_included = true;
368 }
369 }
370
371 last_real_pos = Some(real_pos);
372
373 if is_included {
374 reportable.push(token);
375 }
376 }
377
378 reportable
379}
380
381fn collect_line_endings(text: &str) -> Vec<String> {
382 text.split_inclusive('\n')
383 .map(|line| {
384 if line.ends_with("\r\n") {
385 "\r\n".to_string()
386 } else if line.ends_with('\n') {
387 "\n".to_string()
388 } else {
389 String::new()
390 }
391 })
392 .collect()
393}
394
395fn render_diagnostic_tokens(tokens: &[MatchedTextToken], line_endings: &[String]) -> String {
396 let mut rendered = String::new();
397 let mut previous_line: Option<usize> = None;
398
399 for token in tokens {
400 if let Some(prev_line) = previous_line
401 && token.line_num > prev_line
402 {
403 for line in prev_line..token.line_num {
404 if let Some(line_ending) = line_endings.get(line.saturating_sub(1)) {
405 rendered.push_str(line_ending.as_str());
406 }
407 }
408 }
409
410 let token_value = if token.is_text {
411 token.value.as_str()
412 } else {
413 token
414 .value
415 .strip_suffix("\r\n")
416 .or_else(|| token.value.strip_suffix('\n'))
417 .unwrap_or(token.value.as_str())
418 };
419
420 if token.is_text && !STOPWORDS.contains(token.value.to_lowercase().as_str()) {
421 if token.is_matched {
422 rendered.push_str(token_value);
423 } else {
424 rendered.push('[');
425 rendered.push_str(token_value);
426 rendered.push(']');
427 }
428 } else {
429 rendered.push_str(token_value);
430 }
431
432 previous_line = Some(token.line_num);
433 }
434
435 rendered
436}
437
438impl<'a> Query<'a> {
439 const TEXT_LINE_THRESHOLD: usize = 15;
454 const BINARY_LINE_THRESHOLD: usize = 50;
455 const MAX_TOKEN_PER_LINE: usize = 25;
456
457 fn compute_spdx_offset(
458 tokens: &[QueryToken],
459 dictionary: &crate::license_detection::index::dictionary::TokenDictionary,
460 ) -> Option<usize> {
461 let get_known_id = |i: usize| -> Option<TokenId> {
462 match tokens.get(i)? {
463 QueryToken::Known(known) => Some(known.id),
464 _ => None,
465 }
466 };
467
468 let spdx_id = dictionary.get("spdx")?;
469 let license_id = dictionary.get("license")?;
470 let identifier_id = dictionary.get("identifier")?;
471 let licence_id = dictionary.get("licence");
472
473 let licenses_id = dictionary.get("licenses");
474 let nuget_id = dictionary.get("nuget");
475 let org_id = dictionary.get("org");
476
477 let is_spdx_prefix = |ids: [Option<TokenId>; 3]| -> bool {
478 ids.iter().all(|id| id.is_some())
479 && ids[0] == Some(spdx_id)
480 && (ids[1] == Some(license_id) || ids[1] == licence_id)
481 && ids[2] == Some(identifier_id)
482 };
483
484 let is_nuget_prefix = |ids: [Option<TokenId>; 3]| -> bool {
485 licenses_id.is_some()
486 && nuget_id.is_some()
487 && org_id.is_some()
488 && ids[0] == licenses_id
489 && ids[1] == Some(nuget_id.unwrap())
490 && ids[2] == Some(org_id.unwrap())
491 };
492
493 if tokens.len() >= 3 {
494 let first_three = [get_known_id(0), get_known_id(1), get_known_id(2)];
495 if is_spdx_prefix(first_three) || is_nuget_prefix(first_three) {
496 return Some(0);
497 }
498 }
499
500 if tokens.len() >= 4 {
501 let second_three = [get_known_id(1), get_known_id(2), get_known_id(3)];
502 if is_spdx_prefix(second_three) || is_nuget_prefix(second_three) {
503 return Some(1);
504 }
505 }
506
507 if tokens.len() >= 5 {
508 let third_three = [get_known_id(2), get_known_id(3), get_known_id(4)];
509 if is_spdx_prefix(third_three) || is_nuget_prefix(third_three) {
510 return Some(2);
511 }
512 }
513
514 None
515 }
516
517 pub(crate) fn from_extracted_text(
518 text: &str,
519 index: &'a LicenseIndex,
520 binary_derived: bool,
521 ) -> Result<Self, LicenseDetectionError> {
522 Self::from_extracted_text_with_deadline(text, index, binary_derived, None)
523 }
524
525 pub(crate) fn from_extracted_text_with_deadline(
526 text: &str,
527 index: &'a LicenseIndex,
528 binary_derived: bool,
529 deadline: Option<Instant>,
530 ) -> Result<Self, LicenseDetectionError> {
531 let line_threshold = if binary_derived {
532 Self::BINARY_LINE_THRESHOLD
533 } else {
534 Self::TEXT_LINE_THRESHOLD
535 };
536
537 Self::with_source_options(text, index, line_threshold, Some(binary_derived), deadline)
538 }
539
540 pub fn query_runs(&self) -> Vec<QueryRun<'_>> {
544 self.query_run_ranges
545 .iter()
546 .map(|&(start, end)| QueryRun::new(self, start, end))
547 .collect()
548 }
549
550 fn with_source_options(
551 text: &str,
552 index: &'a LicenseIndex,
553 line_threshold: usize,
554 binary_derived: Option<bool>,
555 deadline: Option<Instant>,
556 ) -> Result<Self, LicenseDetectionError> {
557 crate::license_detection::ensure_within_deadline(deadline)?;
558 let is_binary = match binary_derived {
559 Some(is_binary) => is_binary,
560 None => Self::detect_binary(text),
561 };
562 let has_long_lines = Self::detect_long_lines(text);
563
564 let mut tokens = Vec::new();
565 let mut line_by_pos = Vec::new();
566 let mut unknowns_by_pos: HashMap<Option<usize>, usize> = HashMap::new();
567 let mut stopwords_by_pos: HashMap<Option<usize>, usize> = HashMap::new();
568 let mut shorts_and_digits_pos = PositionSet::new();
569 let mut spdx_lines: Vec<(String, usize, usize)> = Vec::new();
570
571 let mut known_pos: Option<usize> = None;
572 let mut started = false;
573 let mut tokens_by_line: Vec<Vec<Option<KnownToken>>> = Vec::new();
574
575 for (current_line, (line_index, line)) in (1usize..).zip(text.lines().enumerate()) {
576 if line_index.is_multiple_of(128) {
577 crate::license_detection::ensure_within_deadline(deadline)?;
578 }
579
580 let line_trimmed = line.trim();
581 let mut line_tokens: Vec<Option<KnownToken>> = Vec::new();
582
583 let mut line_first_known_pos = None;
584
585 let line_query_tokens = tokenize_as_ids(line_trimmed, &index.dictionary);
586
587 for query_token in &line_query_tokens {
588 match query_token {
589 QueryToken::Known(known_token) => {
590 known_pos = Some(known_pos.map_or(0, |p| p + 1));
591 started = true;
592 tokens.push(known_token.id);
593 line_by_pos.push(current_line);
594 line_tokens.push(Some(*known_token));
595
596 if line_first_known_pos.is_none() {
597 line_first_known_pos = known_pos;
598 }
599
600 if known_token.is_short_or_digit {
601 let _ = shorts_and_digits_pos.insert(known_pos.unwrap());
602 }
603 }
604 QueryToken::Unknown if !started => {
605 *unknowns_by_pos.entry(None).or_insert(0) += 1;
606 line_tokens.push(None);
607 }
608 QueryToken::Unknown => {
609 *unknowns_by_pos.entry(known_pos).or_insert(0) += 1;
610 line_tokens.push(None);
611 }
612 QueryToken::Stopword if !started => {
613 *stopwords_by_pos.entry(None).or_insert(0) += 1;
614 }
615 QueryToken::Stopword => {
616 *stopwords_by_pos.entry(known_pos).or_insert(0) += 1;
617 }
618 }
619 }
620
621 let line_last_known_pos = known_pos;
622
623 let spdx_start_offset =
624 Self::compute_spdx_offset(&line_query_tokens, &index.dictionary);
625
626 if let Some(offset) = spdx_start_offset
627 && let Some(line_first_known_pos) = line_first_known_pos
628 {
629 let (spdx_prefix, spdx_expression) = split_spdx_lid(line);
630 let spdx_text = format!("{}{}", spdx_prefix.unwrap_or_default(), spdx_expression);
631 let spdx_start_known_pos = line_first_known_pos + offset;
632
633 if spdx_start_known_pos <= line_last_known_pos.unwrap() {
634 let spdx_end = line_last_known_pos.unwrap() + 1;
635 spdx_lines.push((spdx_text, spdx_start_known_pos, spdx_end));
636 }
637 }
638 tokens_by_line.push(line_tokens);
639 }
640
641 crate::license_detection::ensure_within_deadline(deadline)?;
642
643 let high_matchables: PositionSet = tokens
644 .iter()
645 .enumerate()
646 .filter(|(_pos, tid)| index.dictionary.token_kind(**tid) == TokenKind::Legalese)
647 .map(|(pos, _tid)| pos)
648 .collect();
649
650 let low_matchables: PositionSet = tokens
651 .iter()
652 .enumerate()
653 .filter(|(_pos, tid)| index.dictionary.token_kind(**tid) == TokenKind::Regular)
654 .map(|(pos, _tid)| pos)
655 .collect();
656
657 let query_runs = Self::compute_query_runs(&tokens_by_line, line_threshold, has_long_lines);
658
659 Ok(Query {
660 text: text.to_string(),
661 tokens,
662 line_by_pos,
663 unknowns_by_pos,
664 stopwords_by_pos,
665 shorts_and_digits_pos,
666 high_matchables,
667 low_matchables,
668 is_binary,
669 query_run_ranges: query_runs,
670 spdx_lines,
671 line_content_ranges: std::sync::OnceLock::new(),
672 index,
673 })
674 }
675
676 fn detect_binary(text: &str) -> bool {
690 let null_byte_count = text.bytes().filter(|&b| b == 0).count();
691
692 if null_byte_count > 0 {
693 return true;
694 }
695
696 let non_printable_ratio = text
697 .chars()
698 .filter(|&c| {
699 !c.is_ascii() && !c.is_ascii_graphic() && c != '\n' && c != '\r' && c != '\t'
700 })
701 .count() as f64
702 / text.len().max(1) as f64;
703
704 non_printable_ratio > 0.3
705 }
706
707 fn detect_long_lines(text: &str) -> bool {
717 text.lines()
718 .any(|line| crate::license_detection::tokenize::count_tokens(line) > 25)
719 }
720
721 fn break_long_lines(lines: &[Vec<Option<KnownToken>>]) -> Vec<Vec<Option<KnownToken>>> {
722 lines
723 .iter()
724 .flat_map(|line| {
725 if line.is_empty() {
726 return Vec::new();
727 }
728
729 if line.len() <= Self::MAX_TOKEN_PER_LINE {
730 vec![line.clone()]
731 } else {
732 line.chunks(Self::MAX_TOKEN_PER_LINE)
733 .map(|chunk| chunk.to_vec())
734 .collect()
735 }
736 })
737 .collect()
738 }
739
740 fn compute_query_runs(
741 tokens_by_line: &[Vec<Option<KnownToken>>],
742 line_threshold: usize,
743 has_long_lines: bool,
744 ) -> Vec<(usize, Option<usize>)> {
745 let processed_lines = if has_long_lines {
746 Self::break_long_lines(tokens_by_line)
747 } else {
748 tokens_by_line.to_vec()
749 };
750
751 let mut query_runs = Vec::new();
752 let mut query_run_start = 0usize;
753 let mut query_run_end = None;
754 let mut empty_lines = 0usize;
755 let mut pos = 0usize;
756 let mut query_run_is_all_digit = true;
757
758 for line_tokens in processed_lines {
759 if query_run_end.is_some() && empty_lines >= line_threshold {
760 if !query_run_is_all_digit {
761 query_runs.push((query_run_start, query_run_end));
762 }
763 query_run_start = pos;
764 query_run_end = None;
765 empty_lines = 0;
766 query_run_is_all_digit = true;
767 }
768
769 if query_run_end.is_none() {
770 query_run_start = pos;
771 }
772
773 if line_tokens.is_empty() {
774 empty_lines += 1;
775 continue;
776 }
777
778 let line_is_all_digit = line_tokens
779 .iter()
780 .all(|token_id| token_id.map(|known| known.is_digit_only).unwrap_or(true));
781 let mut line_has_known_tokens = false;
782 let mut line_has_good_tokens = false;
783
784 for known in line_tokens.into_iter().flatten() {
785 line_has_known_tokens = true;
786 if known.kind == TokenKind::Legalese {
787 line_has_good_tokens = true;
788 }
789 if !known.is_digit_only {
790 query_run_is_all_digit = false;
791 }
792 query_run_end = Some(pos);
793 pos += 1;
794 }
795
796 if line_is_all_digit || !line_has_known_tokens {
797 empty_lines += 1;
798 continue;
799 }
800
801 if line_has_good_tokens {
802 empty_lines = 0;
803 } else {
804 empty_lines += 1;
805 }
806 }
807
808 if let Some(end) = query_run_end
809 && !query_run_is_all_digit
810 {
811 query_runs.push((query_run_start, Some(end)));
812 }
813
814 query_runs
815 }
816
817 #[inline]
827 pub fn line_for_pos(&self, pos: usize) -> Option<usize> {
828 self.line_by_pos.get(pos).copied()
829 }
830
831 #[inline]
833 pub fn is_empty(&self) -> bool {
834 self.tokens.is_empty()
835 }
836
837 pub fn whole_query_run(&self) -> QueryRun<'a> {
841 QueryRun::whole_query_snapshot(self)
842 }
843
844 pub fn subtract(&mut self, span: &PositionSpan) {
853 self.high_matchables.remove_span(span);
854 self.low_matchables.remove_span(span);
855 }
856
857 pub fn matched_text(&self, start_line: usize, end_line: usize) -> String {
871 if start_line == 0 || end_line == 0 || start_line > end_line {
872 return String::new();
873 }
874 let ranges = self.line_content_ranges();
875 let from = start_line - 1;
876 if from >= ranges.len() {
877 return String::new();
878 }
879 let to = end_line.min(ranges.len());
882 ranges[from..to]
883 .iter()
884 .map(|r| &self.text[r.clone()])
885 .collect::<Vec<_>>()
886 .join("\n")
887 }
888
889 pub(crate) fn line_range_has_oversized_line(
893 &self,
894 start_line: usize,
895 end_line: usize,
896 max_line_length: usize,
897 ) -> bool {
898 if start_line == 0 || end_line == 0 || start_line > end_line {
899 return false;
900 }
901 let ranges = self.line_content_ranges();
902 let from = start_line - 1;
903 if from >= ranges.len() {
904 return false;
905 }
906 let to = end_line.min(ranges.len());
907 ranges[from..to].iter().any(|r| r.len() > max_line_length)
908 }
909
910 fn line_content_ranges(&self) -> &[std::ops::Range<usize>] {
916 self.line_content_ranges.get_or_init(|| {
917 let base = self.text.as_ptr() as usize;
918 self.text
919 .lines()
920 .map(|line| {
921 let start = line.as_ptr() as usize - base;
922 start..start + line.len()
923 })
924 .collect()
925 })
926 }
927}
928
929#[derive(Debug, Clone)]
930struct WholeQueryRunSnapshot<'a> {
931 index: &'a LicenseIndex,
932 tokens: Vec<TokenId>,
933 line_by_pos: Vec<usize>,
934 high_matchables: PositionSet,
935 low_matchables: PositionSet,
936}
937
938#[derive(Debug, Clone)]
946pub struct QueryRun<'a> {
947 query: Option<&'a Query<'a>>,
948 whole_query_snapshot: Option<WholeQueryRunSnapshot<'a>>,
949 pub start: usize,
950 pub end: Option<usize>,
951 cached_high_matchables: OnceCell<PositionSet>,
952 cached_low_matchables: OnceCell<PositionSet>,
953 combined_matchables: RefCell<Option<PositionSet>>,
954}
955
956impl<'a> QueryRun<'a> {
957 pub fn new(query: &'a Query<'a>, start: usize, end: Option<usize>) -> Self {
966 Self {
967 query: Some(query),
968 whole_query_snapshot: None,
969 start,
970 end,
971 cached_high_matchables: OnceCell::new(),
972 cached_low_matchables: OnceCell::new(),
973 combined_matchables: RefCell::new(None),
974 }
975 }
976
977 fn whole_query_snapshot(query: &Query<'a>) -> Self {
978 let end = if query.is_empty() {
979 None
980 } else {
981 Some(query.tokens.len() - 1)
982 };
983
984 Self {
985 query: None,
986 whole_query_snapshot: Some(WholeQueryRunSnapshot {
987 index: query.index,
988 tokens: query.tokens.clone(),
989 line_by_pos: query.line_by_pos.clone(),
990 high_matchables: query.high_matchables.clone(),
991 low_matchables: query.low_matchables.clone(),
992 }),
993 start: 0,
994 end,
995 cached_high_matchables: OnceCell::new(),
996 cached_low_matchables: OnceCell::new(),
997 combined_matchables: RefCell::new(None),
998 }
999 }
1000
1001 fn source_tokens(&self) -> &[TokenId] {
1002 if let Some(query) = self.query {
1003 &query.tokens
1004 } else {
1005 &self
1006 .whole_query_snapshot
1007 .as_ref()
1008 .expect("snapshot-backed whole query run should have snapshot data")
1009 .tokens
1010 }
1011 }
1012
1013 fn source_line_by_pos(&self) -> &[usize] {
1014 if let Some(query) = self.query {
1015 &query.line_by_pos
1016 } else {
1017 &self
1018 .whole_query_snapshot
1019 .as_ref()
1020 .expect("snapshot-backed whole query run should have snapshot data")
1021 .line_by_pos
1022 }
1023 }
1024
1025 fn source_high_matchables(&self) -> &PositionSet {
1026 if let Some(query) = self.query {
1027 &query.high_matchables
1028 } else {
1029 &self
1030 .whole_query_snapshot
1031 .as_ref()
1032 .expect("snapshot-backed whole query run should have snapshot data")
1033 .high_matchables
1034 }
1035 }
1036
1037 fn source_low_matchables(&self) -> &PositionSet {
1038 if let Some(query) = self.query {
1039 &query.low_matchables
1040 } else {
1041 &self
1042 .whole_query_snapshot
1043 .as_ref()
1044 .expect("snapshot-backed whole query run should have snapshot data")
1045 .low_matchables
1046 }
1047 }
1048
1049 pub fn get_index(&self) -> &LicenseIndex {
1051 if let Some(query) = self.query {
1052 query.index
1053 } else {
1054 self.whole_query_snapshot
1055 .as_ref()
1056 .expect("snapshot-backed whole query run should have snapshot data")
1057 .index
1058 }
1059 }
1060
1061 pub fn line_for_pos(&self, pos: usize) -> Option<usize> {
1069 self.source_line_by_pos().get(pos).copied()
1070 }
1071
1072 pub fn tokens(&self) -> &[TokenId] {
1078 match self.end {
1079 Some(end) => &self.source_tokens()[self.start..=end],
1080 None => &[],
1081 }
1082 }
1083
1084 pub fn tokens_with_pos(&self) -> impl Iterator<Item = (usize, TokenId)> + '_ {
1088 self.tokens()
1089 .iter()
1090 .copied()
1091 .enumerate()
1092 .map(|(i, tid)| (self.start + i, tid))
1093 }
1094
1095 pub fn is_digits_only(&self) -> bool {
1099 self.tokens()
1100 .iter()
1101 .all(|&tid| self.get_index().dictionary.is_digit_only_token(tid))
1102 }
1103
1104 pub fn is_matchable(&self, include_low: bool, exclude_positions: &[PositionSpan]) -> bool {
1114 if self.is_digits_only() {
1115 return false;
1116 }
1117
1118 let matchables = self.matchables(include_low);
1119
1120 if exclude_positions.is_empty() {
1121 return !matchables.is_empty();
1122 }
1123
1124 let mut matchable_set = matchables;
1125 for span in exclude_positions {
1126 matchable_set.remove_span(span);
1127 }
1128
1129 !matchable_set.is_empty()
1130 }
1131
1132 pub fn matchables(&self, include_low: bool) -> PositionSet {
1133 if include_low {
1134 if let Some(ref cached) = *self.combined_matchables.borrow() {
1135 return cached.clone();
1136 }
1137 let combined = self.low_matchables().union(&self.high_matchables());
1138 *self.combined_matchables.borrow_mut() = Some(combined.clone());
1139 combined
1140 } else {
1141 self.high_matchables()
1142 }
1143 }
1144
1145 pub fn matchable_tokens(&self) -> Vec<Option<TokenId>> {
1146 let high_matchables = self.high_matchables();
1147 if high_matchables.is_empty() {
1148 return Vec::new();
1149 }
1150
1151 let matchables = self.matchables(true);
1152 self.tokens_with_pos()
1153 .map(|(pos, tid)| {
1154 if matchables.contains(pos) {
1155 Some(tid)
1156 } else {
1157 None
1158 }
1159 })
1160 .collect()
1161 }
1162
1163 pub fn high_matchables(&self) -> PositionSet {
1164 self.cached_high_matchables
1165 .get_or_init(|| {
1166 let start = self.start;
1167 let end = self.end.map(|e| e + 1).unwrap_or(usize::MAX);
1168 self.source_high_matchables()
1169 .restricted_to_range(start, end)
1170 })
1171 .clone()
1172 }
1173
1174 pub fn low_matchables(&self) -> PositionSet {
1175 self.cached_low_matchables
1176 .get_or_init(|| {
1177 let start = self.start;
1178 let end = self.end.map(|e| e + 1).unwrap_or(usize::MAX);
1179 self.source_low_matchables().restricted_to_range(start, end)
1180 })
1181 .clone()
1182 }
1183}
1184
1185#[cfg(test)]
1186mod test;