1use std::path::PathBuf;
4
5use crate::classify::{
6 Classification, ClassificationFlags, ContentFamily, DetectionConfidence, DetectionSource,
7 FileTypeId,
8};
9use crate::query::Rejection;
10use crate::{Attrs, EntryId, Fingerprint};
11
12#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
14pub struct AnalyzerId(pub &'static str);
15
16#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
18pub struct AnalyzerVersion(pub u16);
19
20#[derive(Clone, Copy, PartialEq, Eq, Debug)]
22pub struct MetricDef {
23 pub name: &'static str,
25 pub owner: AnalysisSet,
27 pub analyzer: AnalyzerId,
29 pub doc: &'static str,
31}
32
33#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
35pub struct OptionsFingerprint(pub u64);
36
37pub const CONTENT_BASIC: AnalyzerId = AnalyzerId("content-basic-v1");
39pub const CODE_SLOC: AnalyzerId = AnalyzerId("code-sloc-v1");
41pub const TEXT_LOGICAL: AnalyzerId = AnalyzerId("text-logical-v1");
43pub const MARKDOWN_PROSE: AnalyzerId = AnalyzerId("markdown-prose-v1");
45
46pub const METRICS: &[MetricDef] = &[
48 MetricDef {
49 name: "physical_lines",
50 owner: AnalysisSet::LINES_ONLY,
51 analyzer: CONTENT_BASIC,
52 doc: "Logical physical lines across admitted text files.",
53 },
54 MetricDef {
55 name: "blank_lines",
56 owner: AnalysisSet::LINES_ONLY,
57 analyzer: CONTENT_BASIC,
58 doc: "Whitespace-only physical lines.",
59 },
60 MetricDef {
61 name: "nonblank_lines",
62 owner: AnalysisSet::LINES_ONLY,
63 analyzer: CONTENT_BASIC,
64 doc: "Physical lines containing non-whitespace text.",
65 },
66 MetricDef {
67 name: "raw_words",
68 owner: AnalysisSet::LINES_ONLY,
69 analyzer: CONTENT_BASIC,
70 doc: "Whitespace-delimited words before document projection.",
71 },
72 MetricDef {
73 name: "code_lines",
74 owner: AnalysisSet::CODE_ONLY,
75 analyzer: CODE_SLOC,
76 doc: "Code-bearing lines in supported source languages.",
77 },
78 MetricDef {
79 name: "comment_lines",
80 owner: AnalysisSet::CODE_ONLY,
81 analyzer: CODE_SLOC,
82 doc: "Comment-only lines in supported source languages.",
83 },
84 MetricDef {
85 name: "code_blank_lines",
86 owner: AnalysisSet::CODE_ONLY,
87 analyzer: CODE_SLOC,
88 doc: "Blank lines under the code analyzer's syntax.",
89 },
90 MetricDef {
91 name: "logical_words",
92 owner: AnalysisSet::WORDS_ONLY,
93 analyzer: TEXT_LOGICAL,
94 doc: "Normalized logical word volume.",
95 },
96 MetricDef {
97 name: "paragraphs",
98 owner: AnalysisSet::WORDS_ONLY,
99 analyzer: TEXT_LOGICAL,
100 doc: "Plain-text runs or reader-visible Markdown paragraphs.",
101 },
102 MetricDef {
103 name: "visible_words",
104 owner: AnalysisSet::WORDS_ONLY,
105 analyzer: MARKDOWN_PROSE,
106 doc: "Reader-visible Markdown words.",
107 },
108 MetricDef {
109 name: "visible_logical_words",
110 owner: AnalysisSet::WORDS_ONLY,
111 analyzer: MARKDOWN_PROSE,
112 doc: "Normalized reader-visible Markdown words.",
113 },
114 MetricDef {
115 name: "document_words",
116 owner: AnalysisSet::WORDS_ONLY,
117 analyzer: TEXT_LOGICAL,
118 doc: "Logical words after the document-type projection.",
119 },
120];
121
122#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
135pub struct AnalysisSet(u8);
136
137impl AnalysisSet {
138 const LINES: u8 = 1 << 0;
139 const CODE: u8 = 1 << 1;
140 const WORDS: u8 = 1 << 2;
141 const KNOWN: u8 = Self::LINES | Self::CODE | Self::WORDS;
142
143 pub const NONE: Self = Self(0);
145 pub const LINES_ONLY: Self = Self(Self::LINES);
147 pub const CODE_ONLY: Self = Self(Self::LINES | Self::CODE);
149 pub const WORDS_ONLY: Self = Self(Self::LINES | Self::WORDS);
151 pub const ALL: Self = Self(Self::KNOWN);
153
154 #[must_use]
156 pub const fn with_lines(self) -> Self {
157 Self(self.0 | Self::LINES)
158 }
159
160 #[must_use]
162 pub const fn with_code(self) -> Self {
163 Self(self.0 | Self::LINES | Self::CODE)
164 }
165
166 #[must_use]
168 pub const fn with_words(self) -> Self {
169 Self(self.0 | Self::LINES | Self::WORDS)
170 }
171
172 pub const fn is_enabled(self) -> bool {
174 self.0 != 0
175 }
176
177 pub const fn includes_code(self) -> bool {
179 self.0 & Self::CODE != 0
180 }
181
182 pub const fn includes_words(self) -> bool {
184 self.0 & Self::WORDS != 0
185 }
186
187 pub const fn contains(self, other: Self) -> bool {
189 self.0 & other.0 == other.0
190 }
191
192 #[must_use]
194 pub const fn union(self, other: Self) -> Self {
195 Self(self.0 | other.0)
196 }
197
198 pub fn named(self) -> Vec<&'static str> {
204 let labels = self.labels();
205 if labels.len() > 1 {
206 labels.into_iter().filter(|label| *label != "lines").collect()
207 } else {
208 labels
209 }
210 }
211
212 pub fn request_label(self) -> String {
215 if !self.is_enabled() {
216 Self::NONE_LABEL.to_owned()
217 } else if self == Self::ALL {
218 "all".to_owned()
219 } else {
220 self.named().join(",")
221 }
222 }
223
224 pub const fn bits(self) -> u8 {
226 self.0
227 }
228
229 pub const NONE_LABEL: &'static str = "none";
237
238 pub const fn from_bits(bits: u8) -> Option<Self> {
241 if bits & !Self::KNOWN == 0 { Some(Self(bits)) } else { None }
242 }
243
244 pub fn parse(value: &str) -> Result<Self, String> {
255 Self::parse_labeled(value, "analyze")
256 }
257
258 pub fn parse_labeled(value: &str, label: &str) -> Result<Self, String> {
264 Self::parse_rejecting(value).map_err(|rejection| rejection.labeled(label))
265 }
266
267 pub(crate) fn parse_rejecting(value: &str) -> Result<Self, Rejection> {
270 let mut set = Self::NONE;
271 let mut seen: Vec<String> = Vec::new();
272 let mut total: Option<&'static str> = None;
273 for raw in value.split(',') {
274 let token = raw.trim().to_ascii_lowercase();
275 if token.is_empty() {
276 return Err(Rejection::new(value, "empty entry in the list"));
277 }
278 if seen.contains(&token) {
279 return Err(Rejection::new(value, format!("{token:?} appears more than once")));
280 }
281 seen.push(token.clone());
282 match token.as_str() {
283 "none" => total = Some(Self::NONE_LABEL),
284 "all" => {
285 total = Some("all");
286 set = Self::ALL;
287 }
288 "lines" => set = set.with_lines(),
289 "code" => set = set.with_code(),
290 "words" => set = set.with_words(),
291 other => {
292 return Err(Rejection::new(
293 other,
294 "expected one of none, lines, code, words, all",
295 ));
296 }
297 }
298 }
299 if let Some(total) = total {
300 if seen.len() > 1 {
301 return Err(Rejection::new(
302 value,
303 format!("{total:?} names the whole axis and cannot be combined"),
304 ));
305 }
306 if total == Self::NONE_LABEL {
307 return Ok(Self::NONE);
308 }
309 }
310 Ok(set)
311 }
312
313 pub fn labels(self) -> Vec<&'static str> {
315 let mut labels = Vec::new();
316 if self.0 & Self::LINES != 0 {
317 labels.push("lines");
318 }
319 if self.includes_code() {
320 labels.push("code");
321 }
322 if self.includes_words() {
323 labels.push("words");
324 }
325 labels
326 }
327}
328
329#[derive(Clone, PartialEq, Eq, Debug)]
331pub struct ContentDetection {
332 pub file_type: FileTypeId,
334 pub family: ContentFamily,
336 pub source: DetectionSource,
338 pub confidence: DetectionConfidence,
340 pub flags: ClassificationFlags,
342}
343
344impl From<Classification> for ContentDetection {
345 fn from(value: Classification) -> Self {
346 Self {
347 file_type: value.file_type,
348 family: value.family,
349 source: value.source,
350 confidence: value.confidence,
351 flags: value.flags,
352 }
353 }
354}
355
356impl From<ContentDetection> for Classification {
357 fn from(value: ContentDetection) -> Self {
358 Self {
359 file_type: value.file_type,
360 family: value.family,
361 source: value.source,
362 confidence: value.confidence,
363 flags: value.flags,
364 }
365 }
366}
367
368#[derive(Clone, Copy, PartialEq, Eq, Debug)]
370pub struct AnalysisRequest {
371 pub profile: AnalysisSet,
373 pub workers: usize,
375}
376
377impl Default for AnalysisRequest {
378 fn default() -> Self {
379 Self { profile: AnalysisSet::NONE, workers: 0 }
380 }
381}
382
383impl AnalysisRequest {
384 pub fn options_fingerprint(self) -> OptionsFingerprint {
386 const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
387 const PRIME: u64 = 0x0000_0100_0000_01b3;
388 let hash = [self.profile.bits()]
389 .into_iter()
390 .fold(OFFSET, |hash, byte| (hash ^ u64::from(byte)).wrapping_mul(PRIME));
391 OptionsFingerprint(hash)
392 }
393}
394
395#[derive(Clone, PartialEq, Eq, Debug)]
397pub struct ContentProvenance {
398 pub type_rules_fingerprint: u64,
400 pub options_fingerprint: OptionsFingerprint,
402 pub analyzers: Vec<(AnalyzerId, AnalyzerVersion)>,
404}
405
406impl ContentProvenance {
407 pub fn for_request(request: AnalysisRequest, type_rules_fingerprint: u64) -> Self {
413 const VERSION_ONE: AnalyzerVersion = AnalyzerVersion(1);
414 let mut analyzers = Vec::new();
415 if request.profile.is_enabled() {
416 analyzers.push((CONTENT_BASIC, VERSION_ONE));
417 }
418 if request.profile.includes_code() {
419 analyzers.push((CODE_SLOC, AnalyzerVersion(3)));
420 }
421 if request.profile.includes_words() {
422 analyzers.push((TEXT_LOGICAL, VERSION_ONE));
423 analyzers.push((MARKDOWN_PROSE, AnalyzerVersion(2)));
426 }
427 Self {
428 type_rules_fingerprint,
429 options_fingerprint: request.options_fingerprint(),
430 analyzers,
431 }
432 }
433}
434
435#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
437pub struct LogicalWordStats {
438 pub wide_chars: u64,
440 pub nonwide_tokens: u64,
442 pub nonwide_chars: u64,
444}
445
446#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
448pub struct BasicMetrics {
449 pub physical_lines: u64,
451 pub blank_lines: u64,
453 pub nonblank_lines: u64,
455 pub raw_words: u64,
457}
458
459#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
461pub struct CodeMetrics {
462 pub code_lines: u64,
464 pub comment_lines: u64,
466 pub code_blank_lines: u64,
468}
469
470#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
472pub struct WordMetrics {
473 pub paragraphs: u64,
475 pub visible_words: u64,
477 pub logical_word_stats: LogicalWordStats,
479 pub visible_logical_word_stats: LogicalWordStats,
481}
482
483#[derive(Clone, Copy, PartialEq, Eq, Debug)]
485pub struct AnalyzerOutcome<T> {
486 coverage: CoverageReason,
488 value: Option<T>,
490}
491
492impl<T> AnalyzerOutcome<T> {
493 pub(crate) const fn analyzed(value: T) -> Self {
495 Self { coverage: CoverageReason::Analyzed, value: Some(value) }
496 }
497
498 pub(crate) fn unavailable(coverage: CoverageReason) -> Self {
500 assert!(
501 !matches!(coverage, CoverageReason::Analyzed | CoverageReason::TextOnly),
502 "an outcome with a value must carry one"
503 );
504 Self { coverage, value: None }
505 }
506
507 pub(crate) const fn text_only(value: T) -> Self {
509 Self { coverage: CoverageReason::TextOnly, value: Some(value) }
510 }
511
512 pub const fn coverage(&self) -> CoverageReason {
514 self.coverage
515 }
516
517 pub const fn value(self) -> Option<T>
520 where
521 T: Copy,
522 {
523 self.value
524 }
525
526 pub(crate) fn from_parts(coverage: CoverageReason, value: Option<T>) -> Option<Self> {
527 if matches!(coverage, CoverageReason::Analyzed | CoverageReason::TextOnly)
528 == value.is_some()
529 {
530 Some(Self { coverage, value })
531 } else {
532 None
533 }
534 }
535
536 pub const fn is_reusable(&self) -> bool {
538 !matches!(self.coverage, CoverageReason::IoError | CoverageReason::ChangedDuringRead)
539 }
540}
541
542impl LogicalWordStats {
543 pub(crate) fn add_assign(&mut self, other: Self) {
544 self.wide_chars = self.wide_chars.saturating_add(other.wide_chars);
545 self.nonwide_tokens = self.nonwide_tokens.saturating_add(other.nonwide_tokens);
546 self.nonwide_chars = self.nonwide_chars.saturating_add(other.nonwide_chars);
547 }
548
549 pub fn logical_words(self) -> u64 {
551 let chars = u128::from(self.nonwide_chars);
552 let tokens = u128::from(self.nonwide_tokens);
553 let wide = u128::from(self.wide_chars);
554 let (numerator, denominator) = if tokens.saturating_mul(6) < chars {
555 (chars.saturating_add(wide.saturating_mul(3)), 6)
556 } else if tokens.saturating_mul(3) > chars {
557 (chars.saturating_mul(2).saturating_add(wide.saturating_mul(3)), 6)
558 } else {
559 (tokens.saturating_mul(2).saturating_add(wide), 2)
560 };
561 let rounded = numerator.saturating_add(denominator / 2) / denominator;
562 u64::try_from(rounded).unwrap_or(u64::MAX)
563 }
564}
565
566#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
568pub struct MetricValues {
569 pub physical_lines: u64,
571 pub blank_lines: u64,
573 pub nonblank_lines: u64,
575 pub raw_words: u64,
577 pub code_lines: u64,
579 pub comment_lines: u64,
581 pub code_blank_lines: u64,
583 pub paragraphs: u64,
585 pub visible_words: u64,
587 pub logical_word_stats: LogicalWordStats,
589 pub visible_logical_word_stats: LogicalWordStats,
591}
592
593#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
595pub enum CoverageReason {
596 Analyzed,
598 Binary,
600 InvalidUtf8,
602 UnsupportedEncoding,
604 Unsupported,
606 IoError,
608 ChangedDuringRead,
610 TextOnly,
614}
615
616#[derive(Clone, PartialEq, Eq, Debug)]
618pub struct FileAnalysis {
619 pub fingerprint: Fingerprint,
621 pub bytes: u64,
623 pub detection: ContentDetection,
625 pub lines: AnalyzerOutcome<BasicMetrics>,
627 pub code: Option<AnalyzerOutcome<CodeMetrics>>,
629 pub words: Option<AnalyzerOutcome<WordMetrics>>,
631 pub error: Option<String>,
633}
634
635impl FileAnalysis {
636 pub const fn matches_profile(&self, profile: AnalysisSet) -> bool {
638 self.code.is_some() == profile.includes_code()
639 && self.words.is_some() == profile.includes_words()
640 }
641
642 pub const fn operational_failure(&self) -> Option<CoverageReason> {
644 match self.lines.coverage() {
645 CoverageReason::IoError => Some(CoverageReason::IoError),
646 CoverageReason::ChangedDuringRead => Some(CoverageReason::ChangedDuringRead),
647 CoverageReason::Analyzed
648 | CoverageReason::TextOnly
649 | CoverageReason::Binary
650 | CoverageReason::InvalidUtf8
651 | CoverageReason::UnsupportedEncoding
652 | CoverageReason::Unsupported => None,
653 }
654 }
655
656 pub const fn is_reusable(&self) -> bool {
658 self.operational_failure().is_none()
659 && self.lines.is_reusable()
660 && match self.code {
661 Some(outcome) => outcome.is_reusable(),
662 None => true,
663 }
664 && match self.words {
665 Some(outcome) => outcome.is_reusable(),
666 None => true,
667 }
668 }
669}
670
671#[derive(Clone, Debug)]
681pub(crate) struct AnalysisCandidate {
682 pub entry_id: EntryId,
684 pub revision: u64,
686 pub relative_path: PathBuf,
688 pub absolute_path: PathBuf,
690 pub attrs: Attrs,
692 pub classification: Classification,
694}
695
696#[derive(Clone, Debug)]
702pub(crate) struct RestoreCandidate {
703 pub entry_id: EntryId,
705 pub revision: u64,
707 pub relative_path: PathBuf,
709 pub attrs: Attrs,
711}
712
713#[derive(Clone, Debug)]
715pub(crate) struct AnalysisObservation {
716 pub candidate: AnalysisCandidate,
718 pub profile: AnalysisSet,
720 pub provenance: ContentProvenance,
722 pub analysis: FileAnalysis,
724}
725
726#[derive(Clone, Copy, PartialEq, Eq, Debug)]
728pub(crate) enum AnalysisApplyOutcome {
729 Applied,
731 Stale,
735}
736
737#[cfg(test)]
738mod tests {
739 use std::collections::HashSet;
740
741 use super::{
742 AnalysisRequest, AnalysisSet, AnalyzerOutcome, AnalyzerVersion, CODE_SLOC,
743 ContentProvenance, CoverageReason, LogicalWordStats, METRICS,
744 };
745
746 #[test]
747 #[should_panic(expected = "an outcome with a value must carry one")]
748 fn unavailable_outcome_cannot_claim_success() {
749 let _: AnalyzerOutcome<()> = AnalyzerOutcome::unavailable(CoverageReason::Analyzed);
750 }
751
752 #[test]
753 fn metric_registry_has_unique_names_and_one_requestable_owner_each() {
754 let mut names = HashSet::new();
755 for metric in METRICS {
756 assert!(names.insert(metric.name), "duplicate metric name {}", metric.name);
757 assert!(
758 matches!(
759 metric.owner,
760 AnalysisSet::LINES_ONLY | AnalysisSet::CODE_ONLY | AnalysisSet::WORDS_ONLY
761 ),
762 "{} has a non-unit owner {:?}",
763 metric.name,
764 metric.owner
765 );
766 }
767 assert_eq!(names.len(), 12);
768 }
769
770 #[test]
771 fn logical_words_derive_only_after_additive_stats_are_combined() {
772 let first = LogicalWordStats { wide_chars: 3, nonwide_tokens: 1, nonwide_chars: 12 };
773 let second = LogicalWordStats { wide_chars: 1, nonwide_tokens: 9, nonwide_chars: 6 };
774 let combined = LogicalWordStats {
775 wide_chars: first.wide_chars + second.wide_chars,
776 nonwide_tokens: first.nonwide_tokens + second.nonwide_tokens,
777 nonwide_chars: first.nonwide_chars + second.nonwide_chars,
778 };
779 assert_eq!(combined.logical_words(), 8);
780 }
781
782 #[test]
783 fn logical_words_match_the_pinned_rational_clamp_and_half_up_rounding() {
784 let logical = |wide_chars, nonwide_tokens, nonwide_chars| {
785 LogicalWordStats { wide_chars, nonwide_tokens, nonwide_chars }.logical_words()
786 };
787 assert_eq!(logical(0, 0, 0), 0);
788 assert_eq!(logical(0, 2, 9), 2, "ordinary prose passes through");
789 assert_eq!(logical(0, 1, 12), 2, "long tokens use the six-character floor");
790 assert_eq!(logical(0, 4, 4), 1, "short tokens use the three-character ceiling");
791 assert_eq!(logical(3, 0, 0), 2, "wide halves round up once");
792 assert_eq!(logical(1, 1, 1), 1, "mixed fractions combine before rounding");
793 }
794
795 #[test]
796 fn the_analyzer_vocabulary_parses_every_accepted_spelling() {
797 let cases = [
798 ("none", AnalysisSet::NONE),
799 ("lines", AnalysisSet::NONE.with_lines()),
800 ("code", AnalysisSet::NONE.with_code()),
801 ("words", AnalysisSet::NONE.with_words()),
802 ("all", AnalysisSet::ALL),
803 ("code,words", AnalysisSet::ALL),
805 ("words,code", AnalysisSet::ALL),
806 ("lines,code", AnalysisSet::NONE.with_code()),
808 (" CODE , Words ", AnalysisSet::ALL),
810 ];
811 for (input, expected) in cases {
812 assert_eq!(AnalysisSet::parse(input), Ok(expected), "parsing {input:?}");
813 }
814 }
815
816 #[test]
817 fn the_analyzer_vocabulary_rejects_every_incoherent_request() {
818 let cases = [
821 ("", "empty entry"),
822 ("code,,words", "empty entry"),
823 ("code,code", "more than once"),
824 ("basic", "expected one of"),
825 ("documents", "expected one of"),
826 ("full", "expected one of"),
827 ("none,code", "cannot be combined"),
828 ("all,code", "cannot be combined"),
829 ("none,all", "cannot be combined"),
830 ];
831 for (input, needle) in cases {
832 let error = AnalysisSet::parse(input).expect_err(&format!("{input:?} must fail"));
833 assert!(error.contains(needle), "parsing {input:?} said {error:?}, wanted {needle:?}");
834 }
835 }
836
837 #[test]
841 fn a_label_never_rewrites_the_value_it_is_reporting() {
842 for value in ["analyzer", "reanalyze", "analyze-all"] {
843 let error = AnalysisSet::parse_labeled(value, "--analyze")
844 .expect_err("must reject an unknown analyzer");
845 assert!(
846 error.contains(&format!("{value:?}")),
847 "{error} must quote {value:?} exactly as typed"
848 );
849 assert!(error.starts_with("invalid --analyze "), "{error} must carry the label");
850 }
851 }
852
853 #[test]
854 fn the_on_disk_encoding_round_trips_and_refuses_unknown_analyzers() {
855 for set in [
856 AnalysisSet::NONE,
857 AnalysisSet::NONE.with_lines(),
858 AnalysisSet::NONE.with_code(),
859 AnalysisSet::NONE.with_words(),
860 AnalysisSet::ALL,
861 ] {
862 assert_eq!(AnalysisSet::from_bits(set.bits()), Some(set));
863 }
864 assert_eq!(AnalysisSet::from_bits(0b1000_0000), None);
867 }
868
869 #[test]
870 fn code_metrics_use_the_updated_analyzer_version() {
871 let request = AnalysisRequest { profile: AnalysisSet::NONE.with_code(), workers: 1 };
872 let provenance = ContentProvenance::for_request(request, 42);
873 assert!(provenance.analyzers.contains(&(CODE_SLOC, AnalyzerVersion(3))));
874 }
875
876 #[test]
877 fn labels_are_the_vocabulary_parse_accepts() {
878 for set in [
879 AnalysisSet::NONE.with_lines(),
880 AnalysisSet::NONE.with_code(),
881 AnalysisSet::NONE.with_words(),
882 AnalysisSet::ALL,
883 ] {
884 let spelled = set.labels().join(",");
885 assert_eq!(AnalysisSet::parse(&spelled), Ok(set), "round trip through {spelled:?}");
886 }
887 assert!(AnalysisSet::NONE.labels().is_empty());
888 }
889
890 #[test]
892 fn a_request_label_is_the_shortest_spelling_that_parses_back() {
893 for (set, named, label) in [
894 (AnalysisSet::NONE, &[][..], "none"),
895 (AnalysisSet::LINES_ONLY, &["lines"][..], "lines"),
896 (AnalysisSet::CODE_ONLY, &["code"][..], "code"),
897 (AnalysisSet::WORDS_ONLY, &["words"][..], "words"),
898 (AnalysisSet::ALL, &["code", "words"][..], "all"),
899 ] {
900 assert_eq!(set.named(), named, "{set:?}");
901 assert_eq!(set.request_label(), label, "{set:?}");
902 assert_eq!(AnalysisSet::parse(label), Ok(set), "round trip through {label:?}");
903 }
904 assert_eq!(AnalysisSet::CODE_ONLY.union(AnalysisSet::WORDS_ONLY), AnalysisSet::ALL);
905 assert_eq!(AnalysisSet::NONE.union(AnalysisSet::LINES_ONLY), AnalysisSet::LINES_ONLY);
906 }
907}