1use crate::license_detection::LicenseDetectionError;
9use crate::license_detection::index::LicenseIndex;
10use crate::license_detection::index::dictionary::{KnownToken, QueryToken, TokenId, TokenKind};
11use crate::license_detection::models::PositionSpan;
12use crate::license_detection::position_set::PositionSet;
13use crate::license_detection::spdx_lid::split_spdx_lid;
14use crate::license_detection::tokenize::STOPWORDS;
15use crate::license_detection::tokenize::tokenize_as_ids;
16use regex::Regex;
17use std::cell::{OnceCell, RefCell};
18use std::collections::HashMap;
19use std::sync::LazyLock;
20use std::time::Instant;
21
22static QUERY_PATTERN: LazyLock<Regex> =
23 LazyLock::new(|| Regex::new(r"[^_\W]+\+?[^_\W]*").expect("valid query regex"));
24static MATCHED_TEXT_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
25 Regex::new(r"(?P<token>[^_\W]+\+?[^_\W]*)|(?P<punct>[_\W\s\+]+[_\W\s]?)")
26 .expect("valid matched text regex")
27});
28
29#[derive(Clone)]
30struct MatchedTextToken {
31 value: String,
32 line_num: usize,
33 pos: Option<usize>,
34 is_text: bool,
35 is_matched: bool,
36}
37
38#[derive(Debug)]
50pub struct Query<'a> {
51 pub text: String,
55
56 pub tokens: Vec<TokenId>,
60
61 pub line_by_pos: Vec<usize>,
68
69 pub unknowns_by_pos: HashMap<Option<usize>, usize>,
77
78 pub stopwords_by_pos: HashMap<Option<usize>, usize>,
84
85 pub shorts_and_digits_pos: PositionSet,
91
92 pub high_matchables: PositionSet,
98
99 pub low_matchables: PositionSet,
105
106 pub is_binary: bool,
110
111 pub(crate) query_run_ranges: Vec<(usize, Option<usize>)>,
117
118 pub spdx_lines: Vec<(String, usize, usize)>,
125
126 pub index: &'a LicenseIndex,
128}
129
130pub fn matched_text_from_text(text: &str, start_line: usize, end_line: usize) -> String {
131 if start_line == 0 || end_line == 0 || start_line > end_line {
132 return String::new();
133 }
134
135 text.lines()
136 .enumerate()
137 .filter_map(|(idx, line)| {
138 let line_num = idx + 1;
139 if line_num >= start_line && line_num <= end_line {
140 Some(line)
141 } else {
142 None
143 }
144 })
145 .collect::<Vec<_>>()
146 .join("\n")
147}
148
149pub fn matched_text_diagnostics_from_text(
150 text: &str,
151 query: &Query<'_>,
152 matched_positions: &PositionSet,
153 start_pos: usize,
154 end_pos: usize,
155 start_line: usize,
156 end_line: usize,
157) -> String {
158 let tokens = tokenize_matched_text(text, query);
159 let reportable_tokens = collect_reportable_tokens(
160 tokens,
161 matched_positions,
162 start_pos,
163 end_pos,
164 start_line,
165 end_line,
166 );
167 let line_endings = collect_line_endings(text);
168
169 render_diagnostic_tokens(&reportable_tokens, &line_endings)
170}
171
172pub fn matched_text_from_tokens(
182 text: &str,
183 query: &Query<'_>,
184 matched_positions: &PositionSet,
185 start_pos: usize,
186 end_pos: usize,
187 start_line: usize,
188 end_line: usize,
189) -> String {
190 let tokens = tokenize_matched_text(text, query);
191 let reportable_tokens = collect_reportable_tokens(
192 tokens,
193 matched_positions,
194 start_pos,
195 end_pos,
196 start_line,
197 end_line,
198 );
199 let line_endings = collect_line_endings(text);
200
201 render_plain_tokens(&reportable_tokens, &line_endings)
202}
203
204fn render_plain_tokens(tokens: &[MatchedTextToken], line_endings: &[String]) -> String {
205 let mut rendered = String::new();
206 let mut previous_line: Option<usize> = None;
207
208 for token in tokens {
209 if let Some(prev_line) = previous_line
210 && token.line_num > prev_line
211 {
212 for line in prev_line..token.line_num {
213 if let Some(line_ending) = line_endings.get(line.saturating_sub(1)) {
214 rendered.push_str(line_ending.as_str());
215 }
216 }
217 }
218
219 let token_value = if token.is_text {
220 token.value.as_str()
221 } else {
222 token
223 .value
224 .strip_suffix("\r\n")
225 .or_else(|| token.value.strip_suffix('\n'))
226 .unwrap_or(token.value.as_str())
227 };
228
229 rendered.push_str(token_value);
230
231 previous_line = Some(token.line_num);
232 }
233
234 rendered
235}
236
237fn tokenize_matched_text(text: &str, query: &Query<'_>) -> Vec<MatchedTextToken> {
238 let mut tokens = Vec::new();
239 let mut pos = 0usize;
240 for (line_num, line) in (1usize..).zip(text.split_inclusive('\n')) {
241 for capture in MATCHED_TEXT_PATTERN.captures_iter(line) {
242 if let Some(token_match) = capture.name("token") {
243 let token_text = token_match.as_str();
244 let retokenized: Vec<String> = QUERY_PATTERN
245 .find_iter(&token_text.to_lowercase())
246 .map(|m| m.as_str().to_string())
247 .filter(|token| !STOPWORDS.contains(token.as_str()))
248 .collect();
249
250 if retokenized.is_empty() {
251 tokens.push(MatchedTextToken {
252 value: token_text.to_string(),
253 line_num,
254 pos: None,
255 is_text: true,
256 is_matched: false,
257 });
258 } else if retokenized.len() == 1 {
259 let token = &retokenized[0];
260 let token_pos = if query.index.dictionary.get(token).is_some() {
261 let current_pos = pos;
262 pos += 1;
263 Some(current_pos)
264 } else {
265 None
266 };
267
268 tokens.push(MatchedTextToken {
269 value: token_text.to_string(),
270 line_num,
271 pos: token_pos,
272 is_text: true,
273 is_matched: false,
274 });
275 } else {
276 for token in retokenized {
277 let token_pos = if query.index.dictionary.get(&token).is_some() {
278 let current_pos = pos;
279 pos += 1;
280 Some(current_pos)
281 } else {
282 None
283 };
284
285 tokens.push(MatchedTextToken {
286 value: token,
287 line_num,
288 pos: token_pos,
289 is_text: true,
290 is_matched: false,
291 });
292 }
293 }
294 } else if let Some(punct_match) = capture.name("punct") {
295 tokens.push(MatchedTextToken {
296 value: punct_match.as_str().to_string(),
297 line_num,
298 pos: None,
299 is_text: false,
300 is_matched: false,
301 });
302 }
303 }
304 }
305
306 tokens
307}
308
309fn collect_reportable_tokens(
310 tokens: Vec<MatchedTextToken>,
311 matched_positions: &PositionSet,
312 start_pos: usize,
313 end_pos: usize,
314 start_line: usize,
315 end_line: usize,
316) -> Vec<MatchedTextToken> {
317 let mut reportable = Vec::new();
318 let mut started = false;
319 let mut finished = false;
320 let mut end_real_pos = None;
321 let mut last_real_pos = None;
322
323 for (real_pos, mut token) in tokens.into_iter().enumerate() {
324 if token.line_num < start_line {
325 continue;
326 }
327
328 if token.line_num > end_line {
329 break;
330 }
331
332 let mut is_included = false;
333
334 if token.pos.is_some_and(|pos| matched_positions.contains(pos)) {
335 token.is_matched = true;
336 is_included = true;
337 }
338
339 if !started && token.pos == Some(start_pos) {
340 started = true;
341 is_included = true;
342 }
343
344 if started && !finished {
345 is_included = true;
346 }
347
348 if token.pos == Some(end_pos) {
349 finished = true;
350 started = false;
351 end_real_pos = Some(real_pos);
352 }
353
354 if finished && !started && end_real_pos.is_some() && last_real_pos == end_real_pos {
355 end_real_pos = None;
356 if !token.is_text && !token.value.trim().is_empty() {
357 is_included = true;
358 }
359 }
360
361 last_real_pos = Some(real_pos);
362
363 if is_included {
364 reportable.push(token);
365 }
366 }
367
368 reportable
369}
370
371fn collect_line_endings(text: &str) -> Vec<String> {
372 text.split_inclusive('\n')
373 .map(|line| {
374 if line.ends_with("\r\n") {
375 "\r\n".to_string()
376 } else if line.ends_with('\n') {
377 "\n".to_string()
378 } else {
379 String::new()
380 }
381 })
382 .collect()
383}
384
385fn render_diagnostic_tokens(tokens: &[MatchedTextToken], line_endings: &[String]) -> String {
386 let mut rendered = String::new();
387 let mut previous_line: Option<usize> = None;
388
389 for token in tokens {
390 if let Some(prev_line) = previous_line
391 && token.line_num > prev_line
392 {
393 for line in prev_line..token.line_num {
394 if let Some(line_ending) = line_endings.get(line.saturating_sub(1)) {
395 rendered.push_str(line_ending.as_str());
396 }
397 }
398 }
399
400 let token_value = if token.is_text {
401 token.value.as_str()
402 } else {
403 token
404 .value
405 .strip_suffix("\r\n")
406 .or_else(|| token.value.strip_suffix('\n'))
407 .unwrap_or(token.value.as_str())
408 };
409
410 if token.is_text && !STOPWORDS.contains(token.value.to_lowercase().as_str()) {
411 if token.is_matched {
412 rendered.push_str(token_value);
413 } else {
414 rendered.push('[');
415 rendered.push_str(token_value);
416 rendered.push(']');
417 }
418 } else {
419 rendered.push_str(token_value);
420 }
421
422 previous_line = Some(token.line_num);
423 }
424
425 rendered
426}
427
428impl<'a> Query<'a> {
429 const TEXT_LINE_THRESHOLD: usize = 15;
444 const BINARY_LINE_THRESHOLD: usize = 50;
445 const MAX_TOKEN_PER_LINE: usize = 25;
446
447 fn compute_spdx_offset(
448 tokens: &[QueryToken],
449 dictionary: &crate::license_detection::index::dictionary::TokenDictionary,
450 ) -> Option<usize> {
451 let get_known_id = |i: usize| -> Option<TokenId> {
452 match tokens.get(i)? {
453 QueryToken::Known(known) => Some(known.id),
454 _ => None,
455 }
456 };
457
458 let spdx_id = dictionary.get("spdx")?;
459 let license_id = dictionary.get("license")?;
460 let identifier_id = dictionary.get("identifier")?;
461 let licence_id = dictionary.get("licence");
462
463 let licenses_id = dictionary.get("licenses");
464 let nuget_id = dictionary.get("nuget");
465 let org_id = dictionary.get("org");
466
467 let is_spdx_prefix = |ids: [Option<TokenId>; 3]| -> bool {
468 ids.iter().all(|id| id.is_some())
469 && ids[0] == Some(spdx_id)
470 && (ids[1] == Some(license_id) || ids[1] == licence_id)
471 && ids[2] == Some(identifier_id)
472 };
473
474 let is_nuget_prefix = |ids: [Option<TokenId>; 3]| -> bool {
475 licenses_id.is_some()
476 && nuget_id.is_some()
477 && org_id.is_some()
478 && ids[0] == licenses_id
479 && ids[1] == Some(nuget_id.unwrap())
480 && ids[2] == Some(org_id.unwrap())
481 };
482
483 if tokens.len() >= 3 {
484 let first_three = [get_known_id(0), get_known_id(1), get_known_id(2)];
485 if is_spdx_prefix(first_three) || is_nuget_prefix(first_three) {
486 return Some(0);
487 }
488 }
489
490 if tokens.len() >= 4 {
491 let second_three = [get_known_id(1), get_known_id(2), get_known_id(3)];
492 if is_spdx_prefix(second_three) || is_nuget_prefix(second_three) {
493 return Some(1);
494 }
495 }
496
497 if tokens.len() >= 5 {
498 let third_three = [get_known_id(2), get_known_id(3), get_known_id(4)];
499 if is_spdx_prefix(third_three) || is_nuget_prefix(third_three) {
500 return Some(2);
501 }
502 }
503
504 None
505 }
506
507 pub(crate) fn from_extracted_text(
508 text: &str,
509 index: &'a LicenseIndex,
510 binary_derived: bool,
511 ) -> Result<Self, LicenseDetectionError> {
512 Self::from_extracted_text_with_deadline(text, index, binary_derived, None)
513 }
514
515 pub(crate) fn from_extracted_text_with_deadline(
516 text: &str,
517 index: &'a LicenseIndex,
518 binary_derived: bool,
519 deadline: Option<Instant>,
520 ) -> Result<Self, LicenseDetectionError> {
521 let line_threshold = if binary_derived {
522 Self::BINARY_LINE_THRESHOLD
523 } else {
524 Self::TEXT_LINE_THRESHOLD
525 };
526
527 Self::with_source_options(text, index, line_threshold, Some(binary_derived), deadline)
528 }
529
530 pub fn query_runs(&self) -> Vec<QueryRun<'_>> {
534 self.query_run_ranges
535 .iter()
536 .map(|&(start, end)| QueryRun::new(self, start, end))
537 .collect()
538 }
539
540 fn with_source_options(
541 text: &str,
542 index: &'a LicenseIndex,
543 line_threshold: usize,
544 binary_derived: Option<bool>,
545 deadline: Option<Instant>,
546 ) -> Result<Self, LicenseDetectionError> {
547 crate::license_detection::ensure_within_deadline(deadline)?;
548 let is_binary = match binary_derived {
549 Some(is_binary) => is_binary,
550 None => Self::detect_binary(text),
551 };
552 let has_long_lines = Self::detect_long_lines(text);
553
554 let mut tokens = Vec::new();
555 let mut line_by_pos = Vec::new();
556 let mut unknowns_by_pos: HashMap<Option<usize>, usize> = HashMap::new();
557 let mut stopwords_by_pos: HashMap<Option<usize>, usize> = HashMap::new();
558 let mut shorts_and_digits_pos = PositionSet::new();
559 let mut spdx_lines: Vec<(String, usize, usize)> = Vec::new();
560
561 let mut known_pos: Option<usize> = None;
562 let mut started = false;
563 let mut tokens_by_line: Vec<Vec<Option<KnownToken>>> = Vec::new();
564
565 for (current_line, (line_index, line)) in (1usize..).zip(text.lines().enumerate()) {
566 if line_index.is_multiple_of(128) {
567 crate::license_detection::ensure_within_deadline(deadline)?;
568 }
569
570 let line_trimmed = line.trim();
571 let mut line_tokens: Vec<Option<KnownToken>> = Vec::new();
572
573 let mut line_first_known_pos = None;
574
575 let line_query_tokens = tokenize_as_ids(line_trimmed, &index.dictionary);
576
577 for query_token in &line_query_tokens {
578 match query_token {
579 QueryToken::Known(known_token) => {
580 known_pos = Some(known_pos.map_or(0, |p| p + 1));
581 started = true;
582 tokens.push(known_token.id);
583 line_by_pos.push(current_line);
584 line_tokens.push(Some(*known_token));
585
586 if line_first_known_pos.is_none() {
587 line_first_known_pos = known_pos;
588 }
589
590 if known_token.is_short_or_digit {
591 let _ = shorts_and_digits_pos.insert(known_pos.unwrap());
592 }
593 }
594 QueryToken::Unknown if !started => {
595 *unknowns_by_pos.entry(None).or_insert(0) += 1;
596 line_tokens.push(None);
597 }
598 QueryToken::Unknown => {
599 *unknowns_by_pos.entry(known_pos).or_insert(0) += 1;
600 line_tokens.push(None);
601 }
602 QueryToken::Stopword if !started => {
603 *stopwords_by_pos.entry(None).or_insert(0) += 1;
604 }
605 QueryToken::Stopword => {
606 *stopwords_by_pos.entry(known_pos).or_insert(0) += 1;
607 }
608 }
609 }
610
611 let line_last_known_pos = known_pos;
612
613 let spdx_start_offset =
614 Self::compute_spdx_offset(&line_query_tokens, &index.dictionary);
615
616 if let Some(offset) = spdx_start_offset
617 && let Some(line_first_known_pos) = line_first_known_pos
618 {
619 let (spdx_prefix, spdx_expression) = split_spdx_lid(line);
620 let spdx_text = format!("{}{}", spdx_prefix.unwrap_or_default(), spdx_expression);
621 let spdx_start_known_pos = line_first_known_pos + offset;
622
623 if spdx_start_known_pos <= line_last_known_pos.unwrap() {
624 let spdx_end = line_last_known_pos.unwrap() + 1;
625 spdx_lines.push((spdx_text, spdx_start_known_pos, spdx_end));
626 }
627 }
628 tokens_by_line.push(line_tokens);
629 }
630
631 crate::license_detection::ensure_within_deadline(deadline)?;
632
633 let high_matchables: PositionSet = tokens
634 .iter()
635 .enumerate()
636 .filter(|(_pos, tid)| index.dictionary.token_kind(**tid) == TokenKind::Legalese)
637 .map(|(pos, _tid)| pos)
638 .collect();
639
640 let low_matchables: PositionSet = tokens
641 .iter()
642 .enumerate()
643 .filter(|(_pos, tid)| index.dictionary.token_kind(**tid) == TokenKind::Regular)
644 .map(|(pos, _tid)| pos)
645 .collect();
646
647 let query_runs = Self::compute_query_runs(&tokens_by_line, line_threshold, has_long_lines);
648
649 Ok(Query {
650 text: text.to_string(),
651 tokens,
652 line_by_pos,
653 unknowns_by_pos,
654 stopwords_by_pos,
655 shorts_and_digits_pos,
656 high_matchables,
657 low_matchables,
658 is_binary,
659 query_run_ranges: query_runs,
660 spdx_lines,
661 index,
662 })
663 }
664
665 fn detect_binary(text: &str) -> bool {
679 let null_byte_count = text.bytes().filter(|&b| b == 0).count();
680
681 if null_byte_count > 0 {
682 return true;
683 }
684
685 let non_printable_ratio = text
686 .chars()
687 .filter(|&c| {
688 !c.is_ascii() && !c.is_ascii_graphic() && c != '\n' && c != '\r' && c != '\t'
689 })
690 .count() as f64
691 / text.len().max(1) as f64;
692
693 non_printable_ratio > 0.3
694 }
695
696 fn detect_long_lines(text: &str) -> bool {
706 text.lines()
707 .any(|line| crate::license_detection::tokenize::count_tokens(line) > 25)
708 }
709
710 fn break_long_lines(lines: &[Vec<Option<KnownToken>>]) -> Vec<Vec<Option<KnownToken>>> {
711 lines
712 .iter()
713 .flat_map(|line| {
714 if line.is_empty() {
715 return Vec::new();
716 }
717
718 if line.len() <= Self::MAX_TOKEN_PER_LINE {
719 vec![line.clone()]
720 } else {
721 line.chunks(Self::MAX_TOKEN_PER_LINE)
722 .map(|chunk| chunk.to_vec())
723 .collect()
724 }
725 })
726 .collect()
727 }
728
729 fn compute_query_runs(
730 tokens_by_line: &[Vec<Option<KnownToken>>],
731 line_threshold: usize,
732 has_long_lines: bool,
733 ) -> Vec<(usize, Option<usize>)> {
734 let processed_lines = if has_long_lines {
735 Self::break_long_lines(tokens_by_line)
736 } else {
737 tokens_by_line.to_vec()
738 };
739
740 let mut query_runs = Vec::new();
741 let mut query_run_start = 0usize;
742 let mut query_run_end = None;
743 let mut empty_lines = 0usize;
744 let mut pos = 0usize;
745 let mut query_run_is_all_digit = true;
746
747 for line_tokens in processed_lines {
748 if query_run_end.is_some() && empty_lines >= line_threshold {
749 if !query_run_is_all_digit {
750 query_runs.push((query_run_start, query_run_end));
751 }
752 query_run_start = pos;
753 query_run_end = None;
754 empty_lines = 0;
755 query_run_is_all_digit = true;
756 }
757
758 if query_run_end.is_none() {
759 query_run_start = pos;
760 }
761
762 if line_tokens.is_empty() {
763 empty_lines += 1;
764 continue;
765 }
766
767 let line_is_all_digit = line_tokens
768 .iter()
769 .all(|token_id| token_id.map(|known| known.is_digit_only).unwrap_or(true));
770 let mut line_has_known_tokens = false;
771 let mut line_has_good_tokens = false;
772
773 for known in line_tokens.into_iter().flatten() {
774 line_has_known_tokens = true;
775 if known.kind == TokenKind::Legalese {
776 line_has_good_tokens = true;
777 }
778 if !known.is_digit_only {
779 query_run_is_all_digit = false;
780 }
781 query_run_end = Some(pos);
782 pos += 1;
783 }
784
785 if line_is_all_digit || !line_has_known_tokens {
786 empty_lines += 1;
787 continue;
788 }
789
790 if line_has_good_tokens {
791 empty_lines = 0;
792 } else {
793 empty_lines += 1;
794 }
795 }
796
797 if let Some(end) = query_run_end
798 && !query_run_is_all_digit
799 {
800 query_runs.push((query_run_start, Some(end)));
801 }
802
803 query_runs
804 }
805
806 #[inline]
816 pub fn line_for_pos(&self, pos: usize) -> Option<usize> {
817 self.line_by_pos.get(pos).copied()
818 }
819
820 #[inline]
822 pub fn is_empty(&self) -> bool {
823 self.tokens.is_empty()
824 }
825
826 pub fn whole_query_run(&self) -> QueryRun<'a> {
830 QueryRun::whole_query_snapshot(self)
831 }
832
833 pub fn subtract(&mut self, span: &PositionSpan) {
842 self.high_matchables.remove_span(span);
843 self.low_matchables.remove_span(span);
844 }
845
846 pub fn matched_text(&self, start_line: usize, end_line: usize) -> String {
860 matched_text_from_text(&self.text, start_line, end_line)
861 }
862}
863
864#[derive(Debug, Clone)]
865struct WholeQueryRunSnapshot<'a> {
866 index: &'a LicenseIndex,
867 tokens: Vec<TokenId>,
868 line_by_pos: Vec<usize>,
869 high_matchables: PositionSet,
870 low_matchables: PositionSet,
871}
872
873#[derive(Debug, Clone)]
881pub struct QueryRun<'a> {
882 query: Option<&'a Query<'a>>,
883 whole_query_snapshot: Option<WholeQueryRunSnapshot<'a>>,
884 pub start: usize,
885 pub end: Option<usize>,
886 cached_high_matchables: OnceCell<PositionSet>,
887 cached_low_matchables: OnceCell<PositionSet>,
888 combined_matchables: RefCell<Option<PositionSet>>,
889}
890
891impl<'a> QueryRun<'a> {
892 pub fn new(query: &'a Query<'a>, start: usize, end: Option<usize>) -> Self {
901 Self {
902 query: Some(query),
903 whole_query_snapshot: None,
904 start,
905 end,
906 cached_high_matchables: OnceCell::new(),
907 cached_low_matchables: OnceCell::new(),
908 combined_matchables: RefCell::new(None),
909 }
910 }
911
912 fn whole_query_snapshot(query: &Query<'a>) -> Self {
913 let end = if query.is_empty() {
914 None
915 } else {
916 Some(query.tokens.len() - 1)
917 };
918
919 Self {
920 query: None,
921 whole_query_snapshot: Some(WholeQueryRunSnapshot {
922 index: query.index,
923 tokens: query.tokens.clone(),
924 line_by_pos: query.line_by_pos.clone(),
925 high_matchables: query.high_matchables.clone(),
926 low_matchables: query.low_matchables.clone(),
927 }),
928 start: 0,
929 end,
930 cached_high_matchables: OnceCell::new(),
931 cached_low_matchables: OnceCell::new(),
932 combined_matchables: RefCell::new(None),
933 }
934 }
935
936 fn source_tokens(&self) -> &[TokenId] {
937 if let Some(query) = self.query {
938 &query.tokens
939 } else {
940 &self
941 .whole_query_snapshot
942 .as_ref()
943 .expect("snapshot-backed whole query run should have snapshot data")
944 .tokens
945 }
946 }
947
948 fn source_line_by_pos(&self) -> &[usize] {
949 if let Some(query) = self.query {
950 &query.line_by_pos
951 } else {
952 &self
953 .whole_query_snapshot
954 .as_ref()
955 .expect("snapshot-backed whole query run should have snapshot data")
956 .line_by_pos
957 }
958 }
959
960 fn source_high_matchables(&self) -> &PositionSet {
961 if let Some(query) = self.query {
962 &query.high_matchables
963 } else {
964 &self
965 .whole_query_snapshot
966 .as_ref()
967 .expect("snapshot-backed whole query run should have snapshot data")
968 .high_matchables
969 }
970 }
971
972 fn source_low_matchables(&self) -> &PositionSet {
973 if let Some(query) = self.query {
974 &query.low_matchables
975 } else {
976 &self
977 .whole_query_snapshot
978 .as_ref()
979 .expect("snapshot-backed whole query run should have snapshot data")
980 .low_matchables
981 }
982 }
983
984 pub fn get_index(&self) -> &LicenseIndex {
986 if let Some(query) = self.query {
987 query.index
988 } else {
989 self.whole_query_snapshot
990 .as_ref()
991 .expect("snapshot-backed whole query run should have snapshot data")
992 .index
993 }
994 }
995
996 pub fn line_for_pos(&self, pos: usize) -> Option<usize> {
1004 self.source_line_by_pos().get(pos).copied()
1005 }
1006
1007 pub fn tokens(&self) -> &[TokenId] {
1013 match self.end {
1014 Some(end) => &self.source_tokens()[self.start..=end],
1015 None => &[],
1016 }
1017 }
1018
1019 pub fn tokens_with_pos(&self) -> impl Iterator<Item = (usize, TokenId)> + '_ {
1023 self.tokens()
1024 .iter()
1025 .copied()
1026 .enumerate()
1027 .map(|(i, tid)| (self.start + i, tid))
1028 }
1029
1030 pub fn is_digits_only(&self) -> bool {
1034 self.tokens()
1035 .iter()
1036 .all(|&tid| self.get_index().dictionary.is_digit_only_token(tid))
1037 }
1038
1039 pub fn is_matchable(&self, include_low: bool, exclude_positions: &[PositionSpan]) -> bool {
1049 if self.is_digits_only() {
1050 return false;
1051 }
1052
1053 let matchables = self.matchables(include_low);
1054
1055 if exclude_positions.is_empty() {
1056 return !matchables.is_empty();
1057 }
1058
1059 let mut matchable_set = matchables;
1060 for span in exclude_positions {
1061 matchable_set.remove_span(span);
1062 }
1063
1064 !matchable_set.is_empty()
1065 }
1066
1067 pub fn matchables(&self, include_low: bool) -> PositionSet {
1068 if include_low {
1069 if let Some(ref cached) = *self.combined_matchables.borrow() {
1070 return cached.clone();
1071 }
1072 let combined = self.low_matchables().union(&self.high_matchables());
1073 *self.combined_matchables.borrow_mut() = Some(combined.clone());
1074 combined
1075 } else {
1076 self.high_matchables()
1077 }
1078 }
1079
1080 pub fn matchable_tokens(&self) -> Vec<Option<TokenId>> {
1081 let high_matchables = self.high_matchables();
1082 if high_matchables.is_empty() {
1083 return Vec::new();
1084 }
1085
1086 let matchables = self.matchables(true);
1087 self.tokens_with_pos()
1088 .map(|(pos, tid)| {
1089 if matchables.contains(pos) {
1090 Some(tid)
1091 } else {
1092 None
1093 }
1094 })
1095 .collect()
1096 }
1097
1098 pub fn high_matchables(&self) -> PositionSet {
1099 self.cached_high_matchables
1100 .get_or_init(|| {
1101 let start = self.start;
1102 let end = self.end.map(|e| e + 1).unwrap_or(usize::MAX);
1103 let source = self.source_high_matchables();
1104 let live_span = PositionSpan::new(start, end);
1105 source
1106 .iter()
1107 .filter(|&pos| live_span.contains(pos))
1108 .collect()
1109 })
1110 .clone()
1111 }
1112
1113 pub fn low_matchables(&self) -> PositionSet {
1114 self.cached_low_matchables
1115 .get_or_init(|| {
1116 let start = self.start;
1117 let end = self.end.map(|e| e + 1).unwrap_or(usize::MAX);
1118 let source = self.source_low_matchables();
1119 let live_span = PositionSpan::new(start, end);
1120 source
1121 .iter()
1122 .filter(|&pos| live_span.contains(pos))
1123 .collect()
1124 })
1125 .clone()
1126 }
1127}
1128
1129#[cfg(test)]
1130mod test;