1pub mod style;
5pub use style::{IndentStyle, StyleAnalysis, StyleGuideScore, StyleSignal};
6
7use std::collections::{BTreeMap, BTreeSet, HashSet};
8use std::path::Path;
9
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum Language {
15 C,
16 Cpp,
17 CSharp,
18 Go,
19 Java,
20 JavaScript,
21 Python,
22 Rust,
23 Shell,
24 PowerShell,
25 TypeScript,
26 Assembly,
28 Clojure,
29 Css,
30 Dart,
31 Dockerfile,
32 Elixir,
33 Erlang,
34 FSharp,
35 Groovy,
36 Haskell,
37 Html,
38 Julia,
39 Kotlin,
40 Lua,
41 Makefile,
42 Nim,
43 ObjectiveC,
44 Ocaml,
45 Perl,
46 Php,
47 R,
48 Ruby,
49 Scala,
50 Scss,
51 Sql,
52 Svelte,
53 Swift,
54 Vue,
55 Xml,
56 Zig,
57 Solidity,
59 Protobuf,
60 Hcl,
61 GraphQl,
62 Ada,
64 Vhdl,
65 Verilog,
66 Tcl,
67 Pascal,
68 VisualBasic,
69 Lisp,
70 Fortran,
72 Nix,
73 Crystal,
74 D,
75 Glsl,
76 Cmake,
77 Elm,
78 Awk,
79}
80
81impl Language {
82 #[must_use]
83 pub const fn display_name(&self) -> &'static str {
84 match self {
85 Self::C => "C",
86 Self::Cpp => "C++",
87 Self::CSharp => "C#",
88 Self::Go => "Go",
89 Self::Java => "Java",
90 Self::JavaScript => "JavaScript",
91 Self::Python => "Python",
92 Self::Rust => "Rust",
93 Self::Shell => "Shell",
94 Self::PowerShell => "PowerShell",
95 Self::TypeScript => "TypeScript",
96 Self::Assembly => "Assembly",
97 Self::Clojure => "Clojure",
98 Self::Css => "CSS",
99 Self::Dart => "Dart",
100 Self::Dockerfile => "Dockerfile",
101 Self::Elixir => "Elixir",
102 Self::Erlang => "Erlang",
103 Self::FSharp => "F#",
104 Self::Groovy => "Groovy",
105 Self::Haskell => "Haskell",
106 Self::Html => "HTML",
107 Self::Julia => "Julia",
108 Self::Kotlin => "Kotlin",
109 Self::Lua => "Lua",
110 Self::Makefile => "Makefile",
111 Self::Nim => "Nim",
112 Self::ObjectiveC => "Objective-C",
113 Self::Ocaml => "OCaml",
114 Self::Perl => "Perl",
115 Self::Php => "PHP",
116 Self::R => "R",
117 Self::Ruby => "Ruby",
118 Self::Scala => "Scala",
119 Self::Scss => "SCSS",
120 Self::Sql => "SQL",
121 Self::Svelte => "Svelte",
122 Self::Swift => "Swift",
123 Self::Vue => "Vue",
124 Self::Xml => "XML",
125 Self::Zig => "Zig",
126 Self::Solidity => "Solidity",
127 Self::Protobuf => "Protocol Buffers",
128 Self::Hcl => "HCL/Terraform",
129 Self::GraphQl => "GraphQL",
130 Self::Ada => "Ada",
131 Self::Vhdl => "VHDL",
132 Self::Verilog => "Verilog/SystemVerilog",
133 Self::Tcl => "Tcl",
134 Self::Pascal => "Pascal/Delphi",
135 Self::VisualBasic => "Visual Basic",
136 Self::Lisp => "Lisp/Scheme",
137 Self::Fortran => "Fortran",
138 Self::Nix => "Nix",
139 Self::Crystal => "Crystal",
140 Self::D => "D",
141 Self::Glsl => "GLSL/HLSL",
142 Self::Cmake => "CMake",
143 Self::Elm => "Elm",
144 Self::Awk => "Awk",
145 }
146 }
147
148 #[must_use]
149 pub const fn as_slug(&self) -> &'static str {
150 match self {
151 Self::C => "c",
152 Self::Cpp => "cpp",
153 Self::CSharp => "csharp",
154 Self::Go => "go",
155 Self::Java => "java",
156 Self::JavaScript => "javascript",
157 Self::Python => "python",
158 Self::Rust => "rust",
159 Self::Shell => "shell",
160 Self::PowerShell => "powershell",
161 Self::TypeScript => "typescript",
162 Self::Assembly => "assembly",
163 Self::Clojure => "clojure",
164 Self::Css => "css",
165 Self::Dart => "dart",
166 Self::Dockerfile => "dockerfile",
167 Self::Elixir => "elixir",
168 Self::Erlang => "erlang",
169 Self::FSharp => "fsharp",
170 Self::Groovy => "groovy",
171 Self::Haskell => "haskell",
172 Self::Html => "html",
173 Self::Julia => "julia",
174 Self::Kotlin => "kotlin",
175 Self::Lua => "lua",
176 Self::Makefile => "makefile",
177 Self::Nim => "nim",
178 Self::ObjectiveC => "objectivec",
179 Self::Ocaml => "ocaml",
180 Self::Perl => "perl",
181 Self::Php => "php",
182 Self::R => "r",
183 Self::Ruby => "ruby",
184 Self::Scala => "scala",
185 Self::Scss => "scss",
186 Self::Sql => "sql",
187 Self::Svelte => "svelte",
188 Self::Swift => "swift",
189 Self::Vue => "vue",
190 Self::Xml => "xml",
191 Self::Zig => "zig",
192 Self::Solidity => "solidity",
193 Self::Protobuf => "protobuf",
194 Self::Hcl => "hcl",
195 Self::GraphQl => "graphql",
196 Self::Ada => "ada",
197 Self::Vhdl => "vhdl",
198 Self::Verilog => "verilog",
199 Self::Tcl => "tcl",
200 Self::Pascal => "pascal",
201 Self::VisualBasic => "visualbasic",
202 Self::Lisp => "lisp",
203 Self::Fortran => "fortran",
204 Self::Nix => "nix",
205 Self::Crystal => "crystal",
206 Self::D => "d",
207 Self::Glsl => "glsl",
208 Self::Cmake => "cmake",
209 Self::Elm => "elm",
210 Self::Awk => "awk",
211 }
212 }
213
214 #[must_use]
215 pub fn from_name(name: &str) -> Option<Self> {
216 match name.trim().to_ascii_lowercase().as_str() {
217 "c" => Some(Self::C),
218 "cpp" | "c++" | "cplusplus" => Some(Self::Cpp),
219 "csharp" | "c#" | "cs" => Some(Self::CSharp),
220 "go" | "golang" => Some(Self::Go),
221 "java" => Some(Self::Java),
222 "javascript" | "js" => Some(Self::JavaScript),
223 "python" | "py" => Some(Self::Python),
224 "rust" | "rs" => Some(Self::Rust),
225 "shell" | "sh" | "bash" => Some(Self::Shell),
226 "powershell" | "pwsh" | "ps" => Some(Self::PowerShell),
227 "typescript" | "ts" => Some(Self::TypeScript),
228 "assembly" | "asm" => Some(Self::Assembly),
229 "clojure" | "clj" => Some(Self::Clojure),
230 "css" => Some(Self::Css),
231 "dart" => Some(Self::Dart),
232 "dockerfile" | "docker" => Some(Self::Dockerfile),
233 "elixir" | "ex" => Some(Self::Elixir),
234 "erlang" | "erl" => Some(Self::Erlang),
235 "fsharp" | "f#" | "fs" => Some(Self::FSharp),
236 "groovy" => Some(Self::Groovy),
237 "haskell" | "hs" => Some(Self::Haskell),
238 "html" | "htm" => Some(Self::Html),
239 "julia" | "jl" => Some(Self::Julia),
240 "kotlin" | "kt" => Some(Self::Kotlin),
241 "lua" => Some(Self::Lua),
242 "makefile" | "make" | "mk" => Some(Self::Makefile),
243 "nim" => Some(Self::Nim),
244 "objectivec" | "objc" | "objective-c" => Some(Self::ObjectiveC),
245 "ocaml" | "ml" => Some(Self::Ocaml),
246 "perl" | "pl" => Some(Self::Perl),
247 "php" => Some(Self::Php),
248 "r" => Some(Self::R),
249 "ruby" | "rb" => Some(Self::Ruby),
250 "scala" => Some(Self::Scala),
251 "scss" | "sass" => Some(Self::Scss),
252 "sql" => Some(Self::Sql),
253 "svelte" => Some(Self::Svelte),
254 "swift" => Some(Self::Swift),
255 "vue" => Some(Self::Vue),
256 "xml" => Some(Self::Xml),
257 "zig" => Some(Self::Zig),
258 "solidity" | "sol" => Some(Self::Solidity),
259 "protobuf" | "proto" | "protocolbuffers" => Some(Self::Protobuf),
260 "hcl" | "terraform" | "tf" => Some(Self::Hcl),
261 "graphql" | "gql" => Some(Self::GraphQl),
262 "ada" => Some(Self::Ada),
263 "vhdl" => Some(Self::Vhdl),
264 "verilog" | "systemverilog" | "sv" => Some(Self::Verilog),
265 "tcl" => Some(Self::Tcl),
266 "pascal" | "delphi" | "pas" => Some(Self::Pascal),
267 "visualbasic" | "vb" | "vbnet" | "vb.net" => Some(Self::VisualBasic),
268 "lisp" | "scheme" | "racket" | "clisp" | "elisp" => Some(Self::Lisp),
269 "fortran" | "f90" | "f95" => Some(Self::Fortran),
270 "nix" => Some(Self::Nix),
271 "crystal" | "cr" => Some(Self::Crystal),
272 "d" | "dlang" => Some(Self::D),
273 "glsl" | "hlsl" | "shader" | "wgsl" => Some(Self::Glsl),
274 "cmake" => Some(Self::Cmake),
275 "elm" => Some(Self::Elm),
276 "awk" => Some(Self::Awk),
277 _ => None,
278 }
279 }
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize, Default)]
283pub struct RawLineCounts {
284 pub total_physical_lines: u64,
285 pub blank_only_lines: u64,
286 pub code_only_lines: u64,
287 pub single_comment_only_lines: u64,
288 pub multi_comment_only_lines: u64,
289 pub mixed_code_single_comment_lines: u64,
290 pub mixed_code_multi_comment_lines: u64,
291 pub docstring_comment_lines: u64,
292 pub skipped_unknown_lines: u64,
293 #[serde(default)]
295 pub functions: u64,
296 #[serde(default)]
298 pub classes: u64,
299 #[serde(default)]
303 pub variables: u64,
304 #[serde(default)]
306 pub variables_member: u64,
307 #[serde(default)]
309 pub variables_local: u64,
310 #[serde(default)]
312 pub variables_global: u64,
313 #[serde(default)]
316 pub macro_definitions: u64,
317 #[serde(default)]
319 pub imports: u64,
320 #[serde(default)]
324 pub compiler_directive_lines: u64,
325 #[serde(default)]
328 pub test_count: u64,
329 #[serde(default)]
332 pub test_assertion_count: u64,
333 #[serde(default)]
336 pub test_suite_count: u64,
337 #[serde(default)]
340 pub cyclomatic_complexity: u32,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
344 pub lsloc: Option<u32>,
345 #[serde(skip)]
348 pub code_line_hashes: Vec<u64>,
349}
350
351#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
352#[serde(rename_all = "snake_case")]
353pub enum ParseMode {
354 Lexical,
355 LexicalBestEffort,
356 TreeSitter,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct RawFileAnalysis {
361 pub raw: RawLineCounts,
362 pub parse_mode: ParseMode,
363 pub warnings: Vec<String>,
364 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub style_analysis: Option<StyleAnalysis>,
367}
368
369#[derive(Debug, Clone, Copy)]
374pub struct AnalysisOptions {
375 pub blank_in_block_comment_as_comment: bool,
378 pub collapse_continuation_lines: bool,
381 pub enable_style: bool,
384 pub style_lang_scope: StyleLangScope,
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq)]
391pub enum StyleLangScope {
392 All,
393 CFamilyOnly,
394}
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
398pub enum LslocStrategy {
399 Semicolons,
401 NonContinuationNewlines,
404 Unsupported,
407}
408
409impl Default for AnalysisOptions {
410 fn default() -> Self {
411 Self {
412 blank_in_block_comment_as_comment: true,
413 collapse_continuation_lines: false,
414 enable_style: true,
415 style_lang_scope: StyleLangScope::All,
416 }
417 }
418}
419
420#[must_use]
421pub fn supported_languages() -> BTreeSet<Language> {
422 [
423 Language::Assembly,
424 Language::C,
425 Language::Clojure,
426 Language::Cpp,
427 Language::CSharp,
428 Language::Css,
429 Language::Dart,
430 Language::Dockerfile,
431 Language::Elixir,
432 Language::Erlang,
433 Language::FSharp,
434 Language::Go,
435 Language::Groovy,
436 Language::Haskell,
437 Language::Html,
438 Language::Java,
439 Language::JavaScript,
440 Language::Julia,
441 Language::Kotlin,
442 Language::Lua,
443 Language::Makefile,
444 Language::Nim,
445 Language::ObjectiveC,
446 Language::Ocaml,
447 Language::Perl,
448 Language::Php,
449 Language::PowerShell,
450 Language::Python,
451 Language::R,
452 Language::Ruby,
453 Language::Rust,
454 Language::Scala,
455 Language::Scss,
456 Language::Shell,
457 Language::Sql,
458 Language::Svelte,
459 Language::Swift,
460 Language::TypeScript,
461 Language::Vue,
462 Language::Xml,
463 Language::Zig,
464 Language::Solidity,
465 Language::Protobuf,
466 Language::Hcl,
467 Language::GraphQl,
468 Language::Ada,
469 Language::Vhdl,
470 Language::Verilog,
471 Language::Tcl,
472 Language::Pascal,
473 Language::VisualBasic,
474 Language::Lisp,
475 Language::Fortran,
476 Language::Nix,
477 Language::Crystal,
478 Language::D,
479 Language::Glsl,
480 Language::Cmake,
481 Language::Elm,
482 Language::Awk,
483 ]
484 .into_iter()
485 .collect()
486}
487
488fn detect_by_shebang(line: &str) -> Option<Language> {
490 let lower = line.to_ascii_lowercase();
491 if !lower.starts_with("#!") {
492 return None;
493 }
494 if lower.contains("python") {
495 return Some(Language::Python);
496 }
497 if lower.contains("pwsh") || lower.contains("powershell") {
498 return Some(Language::PowerShell);
499 }
500 if lower.contains("bash")
501 || lower.contains("/sh")
502 || lower.contains("zsh")
503 || lower.contains("ksh")
504 {
505 return Some(Language::Shell);
506 }
507 if lower.contains("ruby") {
508 return Some(Language::Ruby);
509 }
510 if lower.contains("perl") {
511 return Some(Language::Perl);
512 }
513 if lower.contains("php") {
514 return Some(Language::Php);
515 }
516 if lower.contains("node") || lower.contains("nodejs") {
517 return Some(Language::JavaScript);
518 }
519 None
520}
521
522#[allow(clippy::too_many_lines)]
524fn detect_by_extension(ext: &str) -> Option<Language> {
525 static EXT_MAP: &[(&str, Language)] = &[
527 ("c", Language::C),
528 ("h", Language::C),
529 ("cc", Language::Cpp),
530 ("cp", Language::Cpp),
531 ("cpp", Language::Cpp),
532 ("cxx", Language::Cpp),
533 ("hh", Language::Cpp),
534 ("hpp", Language::Cpp),
535 ("hxx", Language::Cpp),
536 ("cs", Language::CSharp),
537 ("go", Language::Go),
538 ("java", Language::Java),
539 ("js", Language::JavaScript),
540 ("mjs", Language::JavaScript),
541 ("cjs", Language::JavaScript),
542 ("py", Language::Python),
543 ("rs", Language::Rust),
544 ("sh", Language::Shell),
545 ("bash", Language::Shell),
546 ("zsh", Language::Shell),
547 ("ksh", Language::Shell),
548 ("ps1", Language::PowerShell),
549 ("psm1", Language::PowerShell),
550 ("psd1", Language::PowerShell),
551 ("ts", Language::TypeScript),
552 ("mts", Language::TypeScript),
553 ("cts", Language::TypeScript),
554 ("tsx", Language::TypeScript),
555 ("jsx", Language::JavaScript),
556 ("asm", Language::Assembly),
557 ("s", Language::Assembly),
558 ("clj", Language::Clojure),
559 ("cljs", Language::Clojure),
560 ("cljc", Language::Clojure),
561 ("edn", Language::Clojure),
562 ("css", Language::Css),
563 ("dart", Language::Dart),
564 ("ex", Language::Elixir),
565 ("exs", Language::Elixir),
566 ("erl", Language::Erlang),
567 ("hrl", Language::Erlang),
568 ("fs", Language::FSharp),
569 ("fsi", Language::FSharp),
570 ("fsx", Language::FSharp),
571 ("groovy", Language::Groovy),
572 ("gradle", Language::Groovy),
573 ("hs", Language::Haskell),
574 ("lhs", Language::Haskell),
575 ("html", Language::Html),
576 ("htm", Language::Html),
577 ("xhtml", Language::Html),
578 ("jl", Language::Julia),
579 ("kt", Language::Kotlin),
580 ("kts", Language::Kotlin),
581 ("lua", Language::Lua),
582 ("mk", Language::Makefile),
583 ("nim", Language::Nim),
584 ("nims", Language::Nim),
585 ("m", Language::ObjectiveC),
586 ("mm", Language::ObjectiveC),
587 ("ml", Language::Ocaml),
588 ("mli", Language::Ocaml),
589 ("pl", Language::Perl),
590 ("pm", Language::Perl),
591 ("t", Language::Perl),
592 ("php", Language::Php),
593 ("php3", Language::Php),
594 ("php4", Language::Php),
595 ("php5", Language::Php),
596 ("php7", Language::Php),
597 ("phtml", Language::Php),
598 ("r", Language::R),
599 ("rb", Language::Ruby),
600 ("rake", Language::Ruby),
601 ("scala", Language::Scala),
602 ("sc", Language::Scala),
603 ("scss", Language::Scss),
604 ("sass", Language::Scss),
605 ("sql", Language::Sql),
606 ("svelte", Language::Svelte),
607 ("swift", Language::Swift),
608 ("vue", Language::Vue),
609 ("xml", Language::Xml),
610 ("xsd", Language::Xml),
611 ("xsl", Language::Xml),
612 ("xslt", Language::Xml),
613 ("svg", Language::Xml),
614 ("zig", Language::Zig),
615 ("sol", Language::Solidity),
616 ("proto", Language::Protobuf),
617 ("tf", Language::Hcl),
618 ("tfvars", Language::Hcl),
619 ("hcl", Language::Hcl),
620 ("graphql", Language::GraphQl),
621 ("gql", Language::GraphQl),
622 ("adb", Language::Ada),
623 ("ads", Language::Ada),
624 ("ada", Language::Ada),
625 ("vhd", Language::Vhdl),
626 ("vhdl", Language::Vhdl),
627 ("v", Language::Verilog),
628 ("sv", Language::Verilog),
629 ("svh", Language::Verilog),
630 ("vh", Language::Verilog),
631 ("tcl", Language::Tcl),
632 ("pas", Language::Pascal),
633 ("dpr", Language::Pascal),
634 ("vb", Language::VisualBasic),
635 ("bas", Language::VisualBasic),
636 ("lisp", Language::Lisp),
637 ("lsp", Language::Lisp),
638 ("el", Language::Lisp),
639 ("scm", Language::Lisp),
640 ("ss", Language::Lisp),
641 ("rkt", Language::Lisp),
642 ("f90", Language::Fortran),
643 ("f95", Language::Fortran),
644 ("f03", Language::Fortran),
645 ("f08", Language::Fortran),
646 ("f", Language::Fortran),
647 ("for", Language::Fortran),
648 ("nix", Language::Nix),
649 ("cr", Language::Crystal),
650 ("d", Language::D),
651 ("glsl", Language::Glsl),
652 ("vert", Language::Glsl),
653 ("frag", Language::Glsl),
654 ("comp", Language::Glsl),
655 ("geom", Language::Glsl),
656 ("tesc", Language::Glsl),
657 ("tese", Language::Glsl),
658 ("hlsl", Language::Glsl),
659 ("wgsl", Language::Glsl),
660 ("cmake", Language::Cmake),
661 ("elm", Language::Elm),
662 ("awk", Language::Awk),
663 ];
664 EXT_MAP.iter().find_map(|&(e, l)| (e == ext).then_some(l))
665}
666
667fn detect_by_filename(filename: &str, filename_lower: &str) -> Option<Language> {
669 if filename == "Dockerfile"
671 || filename.starts_with("Dockerfile.")
672 || filename_lower == "dockerfile"
673 {
674 return Some(Language::Dockerfile);
675 }
676 if matches!(
678 filename,
679 "Makefile" | "GNUmakefile" | "makefile" | "BSDmakefile"
680 ) {
681 return Some(Language::Makefile);
682 }
683 if matches!(
685 filename,
686 "Rakefile" | "Gemfile" | "Guardfile" | "Vagrantfile" | "Fastfile" | "Podfile"
687 ) {
688 return Some(Language::Ruby);
689 }
690 if filename == "CMakeLists.txt" || filename_lower == "cmakelists.txt" {
693 return Some(Language::Cmake);
694 }
695 None
696}
697
698#[must_use]
699#[allow(clippy::too_many_lines)]
700pub fn detect_language(
701 path: &Path,
702 first_line: Option<&str>,
703 extension_overrides: &BTreeMap<String, String>,
704 shebang_detection: bool,
705) -> Option<Language> {
706 let extension = path
707 .extension()
708 .and_then(|ext| ext.to_str())
709 .map(str::to_ascii_lowercase);
710
711 if let Some(ext) = extension.as_ref()
713 && let Some(override_name) = extension_overrides.get(ext.as_str())
714 && let Some(lang) = Language::from_name(override_name)
715 {
716 return Some(lang);
717 }
718
719 let filename = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
721 let filename_lower = filename.to_ascii_lowercase();
722
723 if let Some(lang) = detect_by_filename(filename, &filename_lower) {
724 return Some(lang);
725 }
726
727 if let Some(lang) = extension.as_deref().and_then(detect_by_extension) {
729 return Some(lang);
730 }
731
732 if shebang_detection
734 && let Some(line) = first_line
735 && let Some(lang) = detect_by_shebang(line)
736 {
737 return Some(lang);
738 }
739
740 None
741}
742
743#[must_use]
751pub fn looks_like_cpp(text: &str) -> bool {
752 const MARKERS: &[&str] = &[
753 "namespace ",
754 "template<",
755 "template <",
756 "class ",
757 "public:",
758 "private:",
759 "protected:",
760 "std::",
761 "::",
762 "nullptr",
763 "constexpr ",
764 "noexcept",
765 ];
766 MARKERS.iter().any(|m| text.contains(m))
767}
768
769#[cfg(feature = "tree-sitter")]
775fn tree_sitter_fast_path(
776 language: Language,
777 text: &str,
778 options: AnalysisOptions,
779) -> Option<RawFileAnalysis> {
780 if options.blank_in_block_comment_as_comment || options.collapse_continuation_lines {
781 return None;
782 }
783 match language {
784 Language::C | Language::Cpp => {
785 let mut result = ts::analyze_c(text)?;
786 if options.enable_style && should_style_analyse(language, options.style_lang_scope) {
787 result.style_analysis = style::analyze_style(language, text);
788 }
789 Some(result)
790 }
791 Language::Python => ts::analyze_python(text),
792 _ => None,
793 }
794}
795
796#[must_use]
797pub fn analyze_text(language: Language, text: &str, options: AnalysisOptions) -> RawFileAnalysis {
798 #[cfg(feature = "tree-sitter")]
800 if let Some(result) = tree_sitter_fast_path(language, text, options) {
801 return result;
802 }
803
804 let (mut config, has_preprocessor) = language_scan_config(language);
805
806 if language == Language::Python {
808 config.skip_lines = detect_python_docstring_lines(text);
809 }
810
811 let flags = IeeeFlags {
814 has_preprocessor_directives: has_preprocessor,
815 blank_in_block_comment_as_comment: options.blank_in_block_comment_as_comment,
816 collapse_continuation_lines: options.collapse_continuation_lines,
817 };
818 let mut result = analyze_generic(text, config, flags);
819 if options.enable_style && should_style_analyse(language, options.style_lang_scope) {
820 result.style_analysis = style::analyze_style(language, text);
821 }
822 result
823}
824
825const fn should_style_analyse(language: Language, scope: StyleLangScope) -> bool {
827 match scope {
828 StyleLangScope::CFamilyOnly => {
829 matches!(language, Language::C | Language::Cpp | Language::ObjectiveC)
830 }
831 StyleLangScope::All => true,
832 }
833}
834
835fn language_scan_config(language: Language) -> (ScanConfig, bool) {
843 let cfg = LANG_SCAN_TABLE
844 .iter()
845 .find_map(|&(l, c)| (l == language).then_some(c))
846 .unwrap_or_else(|| panic!("language_scan_config: no entry for {language:?}"));
847 let (branch_keywords, lsloc_strategy) = language_complexity_config(language);
848 (
849 ScanConfig {
850 line_comments: cfg.line_comments,
851 block_comment: cfg.block_comment,
852 allow_single_quote_strings: cfg.allow_single_quote_strings,
853 allow_double_quote_strings: cfg.allow_double_quote_strings,
854 allow_triple_quote_strings: cfg.allow_triple_quote_strings,
855 allow_csharp_verbatim_strings: cfg.allow_csharp_verbatim_strings,
856 allow_raw_strings: cfg.allow_raw_strings,
857 skip_lines: HashSet::new(),
858 symbol_patterns: cfg.symbol_patterns,
859 branch_keywords,
860 lsloc_strategy,
861 },
862 cfg.has_preprocessor,
863 )
864}
865
866const BRANCH_C_FAMILY: &[&str] = &[
871 "if", "else", "for", "while", "switch", "case", "catch", "||", "&&",
872];
873const BRANCH_C_TERNARY: &[&str] = &[
874 "if", "else", "for", "while", "switch", "case", "catch", "||", "&&", "?",
875];
876const BRANCH_GO: &[&str] = &["if", "else", "for", "switch", "case", "select", "||", "&&"];
877const BRANCH_RUST: &[&str] = &["if", "else", "for", "while", "match", "||", "&&"];
878const BRANCH_ZIG: &[&str] = &["if", "else", "for", "while", "switch", "catch", "||", "&&"];
879const BRANCH_FSHARP: &[&str] = &["if", "then", "else", "elif", "match", "when", "||", "&&"];
880const BRANCH_LUA: &[&str] = &[
881 "if", "elseif", "else", "for", "while", "repeat", "and", "or",
882];
883const BRANCH_HASKELL: &[&str] = &["if", "then", "else", "case", "otherwise"];
884const BRANCH_SQL: &[&str] = &["CASE", "WHEN", "IF", "ELSE", "case", "when", "if", "else"];
885const BRANCH_OCAML: &[&str] = &["if", "then", "else", "match", "when", "||", "&&"];
886const BRANCH_CLOJURE: &[&str] = &["if", "when", "cond", "case", "and", "or"];
887const BRANCH_PHP: &[&str] = &[
888 "if", "elseif", "else", "for", "while", "switch", "case", "catch", "match", "||", "&&", "?",
889];
890const BRANCH_JULIA: &[&str] = &["if", "elseif", "else", "for", "while", "catch", "||", "&&"];
891const BRANCH_PYTHON: &[&str] = &["if", "elif", "else", "for", "while", "except", "or", "and"];
892const BRANCH_RUBY: &[&str] = &[
893 "if", "elsif", "else", "unless", "until", "while", "case", "when", "rescue", "||", "&&",
894];
895const BRANCH_SHELL: &[&str] = &["if", "elif", "else", "while", "until", "case", "||", "&&"];
896const BRANCH_ELIXIR: &[&str] = &[
897 "if", "else", "cond", "case", "when", "rescue", "||", "&&", "and", "or",
898];
899const BRANCH_POWERSHELL: &[&str] = &[
900 "if", "elseif", "else", "for", "while", "switch", "foreach", "||", "&&",
901];
902const BRANCH_NIM: &[&str] = &[
903 "if", "elif", "else", "for", "while", "case", "of", "except", "and", "or",
904];
905const BRANCH_PERL: &[&str] = &[
906 "if", "elsif", "else", "unless", "until", "for", "while", "foreach", "||", "&&",
907];
908const BRANCH_R: &[&str] = &["if", "else", "for", "while", "repeat", "||", "&&"];
909const BRANCH_ADA: &[&str] = &[
911 "if", "elsif", "else", "case", "when", "loop", "while", "for", "and", "or",
912];
913const BRANCH_VHDL: &[&str] = &[
914 "if", "elsif", "else", "case", "when", "loop", "while", "for", "and", "or", "nand", "nor",
915 "xor",
916];
917const BRANCH_VERILOG: &[&str] = &[
918 "if", "else", "case", "casex", "casez", "for", "while", "&&", "||",
919];
920const BRANCH_TCL: &[&str] = &["if", "elseif", "else", "switch", "while", "for", "foreach"];
921const BRANCH_PASCAL: &[&str] = &[
922 "if", "then", "else", "case", "while", "for", "repeat", "until", "and", "or",
923];
924const BRANCH_VB: &[&str] = &[
925 "If", "Then", "ElseIf", "Else", "Select", "Case", "While", "For", "Do", "And", "Or",
926];
927const BRANCH_LISP: &[&str] = &["if", "when", "unless", "cond", "case", "and", "or"];
928const BRANCH_FORTRAN: &[&str] = &[
930 "if", "then", "else", "elseif", "case", "do", "while", "where",
931];
932const BRANCH_NIX: &[&str] = &["if", "then", "else"];
933const BRANCH_CMAKE: &[&str] = &["if(", "elseif(", "else(", "while(", "foreach("];
934const BRANCH_ELM: &[&str] = &["if", "then", "else", "case", "of"];
935const BRANCH_AWK: &[&str] = &["if", "else", "while", "for", "do"];
936
937const fn language_complexity_config(
940 language: Language,
941) -> (&'static [&'static str], LslocStrategy) {
942 match language {
943 Language::C
945 | Language::Cpp
946 | Language::ObjectiveC
947 | Language::CSharp
948 | Language::JavaScript
949 | Language::TypeScript
950 | Language::Svelte
951 | Language::Vue
952 | Language::Dart
953 | Language::Groovy
954 | Language::Swift
955 | Language::Solidity => (BRANCH_C_TERNARY, LslocStrategy::Semicolons),
956 Language::Java | Language::Kotlin | Language::Scala | Language::D | Language::Glsl => {
958 (BRANCH_C_FAMILY, LslocStrategy::Semicolons)
959 }
960 Language::Go => (BRANCH_GO, LslocStrategy::Semicolons),
961 Language::Rust => (BRANCH_RUST, LslocStrategy::Semicolons),
962 Language::Zig => (BRANCH_ZIG, LslocStrategy::Semicolons),
963 Language::FSharp => (BRANCH_FSHARP, LslocStrategy::Unsupported),
964 Language::Shell => (BRANCH_SHELL, LslocStrategy::NonContinuationNewlines),
966 Language::Elixir => (BRANCH_ELIXIR, LslocStrategy::NonContinuationNewlines),
967 Language::Perl => (BRANCH_PERL, LslocStrategy::Semicolons),
968 Language::R => (BRANCH_R, LslocStrategy::NonContinuationNewlines),
969 Language::Ruby | Language::Crystal => (BRANCH_RUBY, LslocStrategy::NonContinuationNewlines),
970 Language::Python => (BRANCH_PYTHON, LslocStrategy::NonContinuationNewlines),
971 Language::PowerShell => (BRANCH_POWERSHELL, LslocStrategy::Unsupported),
972 Language::Nim => (BRANCH_NIM, LslocStrategy::NonContinuationNewlines),
973 Language::Lua => (BRANCH_LUA, LslocStrategy::Unsupported),
975 Language::Haskell => (BRANCH_HASKELL, LslocStrategy::Unsupported),
976 Language::Sql => (BRANCH_SQL, LslocStrategy::Semicolons),
977 Language::Ocaml => (BRANCH_OCAML, LslocStrategy::Semicolons),
978 Language::Clojure => (BRANCH_CLOJURE, LslocStrategy::Unsupported),
979 Language::Php => (BRANCH_PHP, LslocStrategy::Semicolons),
980 Language::Julia => (BRANCH_JULIA, LslocStrategy::NonContinuationNewlines),
981 Language::Protobuf => (&[], LslocStrategy::Semicolons),
982 Language::Hcl => (&[], LslocStrategy::NonContinuationNewlines),
983 Language::Ada => (BRANCH_ADA, LslocStrategy::Semicolons),
985 Language::Vhdl => (BRANCH_VHDL, LslocStrategy::Semicolons),
986 Language::Verilog => (BRANCH_VERILOG, LslocStrategy::Semicolons),
987 Language::Tcl => (BRANCH_TCL, LslocStrategy::NonContinuationNewlines),
988 Language::Pascal => (BRANCH_PASCAL, LslocStrategy::Semicolons),
989 Language::VisualBasic => (BRANCH_VB, LslocStrategy::NonContinuationNewlines),
990 Language::Lisp => (BRANCH_LISP, LslocStrategy::Unsupported),
991 Language::Fortran => (BRANCH_FORTRAN, LslocStrategy::NonContinuationNewlines),
993 Language::Nix => (BRANCH_NIX, LslocStrategy::Unsupported),
994 Language::Cmake => (BRANCH_CMAKE, LslocStrategy::Unsupported),
995 Language::Elm => (BRANCH_ELM, LslocStrategy::Unsupported),
996 Language::Awk => (BRANCH_AWK, LslocStrategy::NonContinuationNewlines),
997 Language::Makefile
999 | Language::Dockerfile
1000 | Language::Css
1001 | Language::Html
1002 | Language::Xml
1003 | Language::Assembly
1004 | Language::Erlang
1005 | Language::GraphQl
1006 | Language::Scss => (&[], LslocStrategy::Unsupported),
1007 }
1008}
1009
1010#[derive(Debug, Clone, Copy)]
1014struct SymbolPatterns {
1015 functions: &'static [&'static str],
1016 functions_prefix_paren: &'static [&'static str],
1022 classes: &'static [&'static str],
1023 variables: &'static [&'static str],
1024 imports: &'static [&'static str],
1025 tests: &'static [&'static str],
1028 assertions: &'static [&'static str],
1031 test_suites: &'static [&'static str],
1034 variables_prefix_no_paren: &'static [&'static str],
1040}
1041
1042impl SymbolPatterns {
1043 const fn none() -> Self {
1044 Self {
1045 functions: &[],
1046 functions_prefix_paren: &[],
1047 classes: &[],
1048 variables: &[],
1049 imports: &[],
1050 tests: &[],
1051 assertions: &[],
1052 test_suites: &[],
1053 variables_prefix_no_paren: &[],
1054 }
1055 }
1056}
1057
1058const SP_NONE: SymbolPatterns = SymbolPatterns::none(); const SP_SOLIDITY: SymbolPatterns = SymbolPatterns {
1063 functions: &[
1064 "function ",
1065 "modifier ",
1066 "constructor",
1067 "receive ",
1068 "fallback ",
1069 ],
1070 functions_prefix_paren: &[],
1071 classes: &["contract ", "interface ", "library ", "struct ", "enum "],
1072 variables: &[],
1073 imports: &["import "],
1074 tests: &["function test", "function testFuzz", "function invariant"],
1077 assertions: &[
1078 "assertEq(",
1079 "assertEq0(",
1080 "assertTrue(",
1081 "assertFalse(",
1082 "assertGt(",
1083 "assertLt(",
1084 "assertGe(",
1085 "assertLe(",
1086 "assertApproxEq",
1087 "vm.expectRevert(",
1088 "vm.expectEmit(",
1089 ],
1090 test_suites: &[],
1091 variables_prefix_no_paren: &[],
1092};
1093
1094const SP_PROTOBUF: SymbolPatterns = SymbolPatterns {
1097 functions: &["rpc "],
1098 functions_prefix_paren: &[],
1099 classes: &["message ", "service ", "enum "],
1100 variables: &[],
1101 imports: &["import "],
1102 tests: &[],
1103 assertions: &[],
1104 test_suites: &[],
1105 variables_prefix_no_paren: &[],
1106};
1107
1108const SP_ADA: SymbolPatterns = SymbolPatterns {
1110 functions: &["procedure ", "function "],
1111 functions_prefix_paren: &[],
1112 classes: &["package ", "type ", "task ", "protected "],
1113 variables: &[],
1114 imports: &["with ", "use "],
1115 tests: &[],
1116 assertions: &[],
1117 test_suites: &[],
1118 variables_prefix_no_paren: &[],
1119};
1120
1121const SP_VHDL: SymbolPatterns = SymbolPatterns {
1122 functions: &["function ", "procedure ", "process "],
1123 functions_prefix_paren: &[],
1124 classes: &["entity ", "architecture ", "package ", "component "],
1125 variables: &[],
1126 imports: &["library ", "use "],
1127 tests: &[],
1128 assertions: &[],
1129 test_suites: &[],
1130 variables_prefix_no_paren: &[],
1131};
1132
1133const SP_VERILOG: SymbolPatterns = SymbolPatterns {
1134 functions: &["function ", "task "],
1135 functions_prefix_paren: &[],
1136 classes: &["module ", "interface ", "class ", "package "],
1137 variables: &[],
1138 imports: &["import ", "`include"],
1139 tests: &[],
1140 assertions: &[],
1141 test_suites: &[],
1142 variables_prefix_no_paren: &[],
1143};
1144
1145const SP_TCL: SymbolPatterns = SymbolPatterns {
1146 functions: &["proc "],
1147 functions_prefix_paren: &[],
1148 classes: &[],
1149 variables: &[],
1150 imports: &["source ", "package require "],
1151 tests: &["test "],
1153 assertions: &[],
1154 test_suites: &[],
1155 variables_prefix_no_paren: &[],
1156};
1157
1158const SP_PASCAL: SymbolPatterns = SymbolPatterns {
1159 functions: &["procedure ", "function "],
1160 functions_prefix_paren: &[],
1161 classes: &["type ", "class ", "record "],
1162 variables: &[],
1163 imports: &["uses "],
1164 tests: &["procedure Test"],
1166 assertions: &[
1167 "Check(",
1168 "CheckEquals(",
1169 "CheckTrue(",
1170 "CheckFalse(",
1171 "CheckNotNull(",
1172 ],
1173 test_suites: &[],
1174 variables_prefix_no_paren: &[],
1175};
1176
1177const SP_VB: SymbolPatterns = SymbolPatterns {
1178 functions: &[
1179 "Sub ",
1180 "Function ",
1181 "Private Sub ",
1182 "Public Sub ",
1183 "Private Function ",
1184 "Public Function ",
1185 ],
1186 functions_prefix_paren: &[],
1187 classes: &["Class ", "Module ", "Structure "],
1188 variables: &[],
1189 imports: &["Imports "],
1190 tests: &["<TestMethod>", "<TestMethod("],
1192 assertions: &["Assert.", "CollectionAssert.", "StringAssert."],
1193 test_suites: &["<TestClass>", "<TestClass("],
1194 variables_prefix_no_paren: &[],
1195};
1196
1197const SP_LISP: SymbolPatterns = SymbolPatterns {
1198 functions: &["(defun ", "(defmacro ", "(define ", "(defmethod ", "(defn "],
1199 functions_prefix_paren: &[],
1200 classes: &["(defclass ", "(defstruct "],
1201 variables: &[],
1202 imports: &["(require ", "(import ", "(use-package "],
1203 tests: &["(test ", "(deftest "],
1205 assertions: &["(is ", "(is-true ", "(is-false ", "(signals "],
1206 test_suites: &[],
1207 variables_prefix_no_paren: &[],
1208};
1209
1210const SP_FORTRAN: SymbolPatterns = SymbolPatterns {
1212 functions: &["subroutine ", "function "],
1213 functions_prefix_paren: &[],
1214 classes: &["module ", "program ", "type "],
1215 variables: &[],
1216 imports: &["use ", "include "],
1217 tests: &[],
1218 assertions: &[],
1219 test_suites: &[],
1220 variables_prefix_no_paren: &[],
1221};
1222
1223const SP_CRYSTAL: SymbolPatterns = SymbolPatterns {
1224 functions: &["def "],
1225 functions_prefix_paren: &[],
1226 classes: &["class ", "module ", "struct ", "enum "],
1227 variables: &[],
1228 imports: &["require "],
1229 tests: &["it ", "it(", "describe ", "context ", "pending "],
1231 assertions: &[],
1232 test_suites: &[],
1233 variables_prefix_no_paren: &[],
1234};
1235
1236const SP_D: SymbolPatterns = SymbolPatterns {
1237 functions: &[],
1238 functions_prefix_paren: &[],
1239 classes: &["class ", "struct ", "interface ", "enum ", "template "],
1240 variables: &[],
1241 imports: &["import "],
1242 tests: &["unittest"],
1244 assertions: &["assert(", "assertThrown", "assertNotThrown"],
1245 test_suites: &[],
1246 variables_prefix_no_paren: &[],
1247};
1248
1249const SP_CMAKE: SymbolPatterns = SymbolPatterns {
1250 functions: &["function(", "macro("],
1251 functions_prefix_paren: &[],
1252 classes: &[],
1253 variables: &[],
1254 imports: &["include(", "add_subdirectory("],
1255 tests: &[],
1256 assertions: &[],
1257 test_suites: &[],
1258 variables_prefix_no_paren: &[],
1259};
1260
1261const SP_ELM: SymbolPatterns = SymbolPatterns {
1262 functions: &[],
1263 functions_prefix_paren: &[],
1264 classes: &["type "],
1265 variables: &[],
1266 imports: &["import "],
1267 tests: &["test ", "describe ", "fuzz "],
1269 assertions: &["Expect."],
1270 test_suites: &[],
1271 variables_prefix_no_paren: &[],
1272};
1273
1274const SP_AWK: SymbolPatterns = SymbolPatterns {
1275 functions: &["function "],
1276 functions_prefix_paren: &[],
1277 classes: &[],
1278 variables: &[],
1279 imports: &[],
1280 tests: &[],
1281 assertions: &[],
1282 test_suites: &[],
1283 variables_prefix_no_paren: &[],
1284};
1285
1286const SP_RUST: SymbolPatterns = SymbolPatterns {
1287 functions: &[
1288 "fn ",
1289 "pub fn ",
1290 "pub(crate) fn ",
1291 "pub(super) fn ",
1292 "async fn ",
1293 "pub async fn ",
1294 "pub(crate) async fn ",
1295 "unsafe fn ",
1296 "pub unsafe fn ",
1297 "pub(crate) unsafe fn ",
1298 "const fn ",
1299 "pub const fn ",
1300 "pub(crate) const fn ",
1301 "extern fn ",
1302 "pub extern fn ",
1303 ],
1304 functions_prefix_paren: &[],
1305 classes: &[
1306 "struct ",
1307 "pub struct ",
1308 "pub(crate) struct ",
1309 "enum ",
1310 "pub enum ",
1311 "pub(crate) enum ",
1312 "trait ",
1313 "pub trait ",
1314 "pub(crate) trait ",
1315 "impl ",
1316 "impl<",
1317 "type ",
1318 "pub type ",
1319 "pub(crate) type ",
1320 ],
1321 variables: &["let ", "let mut "],
1322 imports: &["use ", "pub use ", "pub(crate) use ", "extern crate "],
1323 tests: &[
1325 "#[test]",
1326 "#[tokio::test]",
1327 "#[actix_web::test]",
1328 "#[rstest]",
1329 "#[test_case",
1330 ],
1331 assertions: &[
1332 "assert_eq!(",
1333 "assert_ne!(",
1334 "assert!(",
1335 "assert_matches!(",
1336 "assert_err!(",
1337 "assert_ok!(",
1338 ],
1339 test_suites: &[],
1340 variables_prefix_no_paren: &[],
1341};
1342
1343const SP_PYTHON: SymbolPatterns = SymbolPatterns {
1344 functions: &["def ", "async def "],
1345 functions_prefix_paren: &[],
1346 classes: &["class "],
1347 variables: &[],
1348 imports: &["import ", "from "],
1349 tests: &["def test_", "async def test_", "class Test"],
1351 assertions: &[
1352 "self.assertEqual(",
1353 "self.assertNotEqual(",
1354 "self.assertTrue(",
1355 "self.assertFalse(",
1356 "self.assertIsNone(",
1357 "self.assertIsNotNone(",
1358 "self.assertIn(",
1359 "self.assertNotIn(",
1360 "self.assertRaises(",
1361 "self.assertAlmostEqual(",
1362 ],
1363 test_suites: &[],
1364 variables_prefix_no_paren: &[],
1365};
1366
1367const SP_JS: SymbolPatterns = SymbolPatterns {
1368 functions: &[
1369 "function ",
1370 "async function ",
1371 "export function ",
1372 "export async function ",
1373 "export default function ",
1374 ],
1375 functions_prefix_paren: &[],
1376 classes: &["class ", "export class ", "export default class "],
1377 variables: &[
1378 "var ",
1379 "let ",
1380 "const ",
1381 "export var ",
1382 "export let ",
1383 "export const ",
1384 ],
1385 imports: &["import "],
1386 tests: &[
1388 "describe(",
1389 "it(",
1390 "test(",
1391 "it.each(",
1392 "test.each(",
1393 "describe.each(",
1394 ],
1395 assertions: &["expect("],
1396 test_suites: &[],
1397 variables_prefix_no_paren: &[],
1398};
1399
1400const SP_TS: SymbolPatterns = SymbolPatterns {
1401 functions: &[
1402 "function ",
1403 "async function ",
1404 "export function ",
1405 "export async function ",
1406 "export default function ",
1407 ],
1408 functions_prefix_paren: &[],
1409 classes: &[
1410 "class ",
1411 "export class ",
1412 "export default class ",
1413 "abstract class ",
1414 "export abstract class ",
1415 "interface ",
1416 "export interface ",
1417 "declare class ",
1418 "declare interface ",
1419 ],
1420 variables: &[
1421 "var ",
1422 "let ",
1423 "const ",
1424 "export var ",
1425 "export let ",
1426 "export const ",
1427 ],
1428 imports: &["import "],
1429 tests: &[
1431 "describe(",
1432 "it(",
1433 "test(",
1434 "it.each(",
1435 "test.each(",
1436 "describe.each(",
1437 ],
1438 assertions: &["expect("],
1439 test_suites: &[],
1440 variables_prefix_no_paren: &[],
1441};
1442
1443const SP_GO: SymbolPatterns = SymbolPatterns {
1444 functions: &["func "],
1445 functions_prefix_paren: &[],
1446 classes: &["type "],
1447 variables: &["var "],
1448 imports: &["import "],
1449 tests: &["func Test", "func Benchmark", "func Fuzz"],
1451 assertions: &[],
1452 test_suites: &[],
1453 variables_prefix_no_paren: &[],
1454};
1455
1456const SP_JAVA: SymbolPatterns = SymbolPatterns {
1457 functions: &[],
1458 functions_prefix_paren: &[],
1459 classes: &[
1460 "class ",
1461 "public class ",
1462 "private class ",
1463 "protected class ",
1464 "abstract class ",
1465 "final class ",
1466 "public abstract class ",
1467 "public final class ",
1468 "interface ",
1469 "public interface ",
1470 "enum ",
1471 "public enum ",
1472 "record ",
1473 "public record ",
1474 "@interface ",
1475 ],
1476 variables: &[],
1477 imports: &["import "],
1478 tests: &[
1480 "@Test",
1481 "@ParameterizedTest",
1482 "@RepeatedTest",
1483 "@TestFactory",
1484 "@TestTemplate",
1485 ],
1486 assertions: &[
1487 "assertEquals(",
1488 "assertNotEquals(",
1489 "assertTrue(",
1490 "assertFalse(",
1491 "assertNull(",
1492 "assertNotNull(",
1493 "assertThat(",
1494 "assertThrows(",
1495 "assertAll(",
1496 "assertArrayEquals(",
1497 "assertIterableEquals(",
1498 "assertLinesMatch(",
1499 ],
1500 test_suites: &[],
1501 variables_prefix_no_paren: &[],
1502};
1503
1504const SP_CSHARP: SymbolPatterns = SymbolPatterns {
1505 functions: &[],
1506 functions_prefix_paren: &[],
1507 classes: &[
1508 "class ",
1509 "public class ",
1510 "private class ",
1511 "protected class ",
1512 "internal class ",
1513 "abstract class ",
1514 "sealed class ",
1515 "static class ",
1516 "partial class ",
1517 "public abstract class ",
1518 "public sealed class ",
1519 "public static class ",
1520 "interface ",
1521 "public interface ",
1522 "internal interface ",
1523 "enum ",
1524 "public enum ",
1525 "struct ",
1526 "public struct ",
1527 "record ",
1528 "public record ",
1529 ],
1530 variables: &["var "],
1531 imports: &["using "],
1532 tests: &[
1534 "[TestMethod]",
1535 "[Test]",
1536 "[Fact]",
1537 "[Theory]",
1538 "[TestCase(",
1539 "[DataRow(",
1540 "[InlineData(",
1541 "[MemberData(",
1542 ],
1543 assertions: &[
1544 "Assert.AreEqual(",
1545 "Assert.AreNotEqual(",
1546 "Assert.IsTrue(",
1547 "Assert.IsFalse(",
1548 "Assert.IsNull(",
1549 "Assert.IsNotNull(",
1550 "Assert.Equal(",
1551 "Assert.NotEqual(",
1552 "Assert.True(",
1553 "Assert.False(",
1554 "Assert.That(",
1555 "Assert.Contains(",
1556 "Assert.Throws(",
1557 "Assert.ThrowsAsync(",
1558 "Assert.IsInstanceOfType(",
1559 ],
1560 test_suites: &["[TestClass]", "[TestFixture]", "[SetUpFixture]"],
1561 variables_prefix_no_paren: &[],
1562};
1563
1564const TEST_PATTERNS_C_CPP: &[&str] = &[
1566 "TEST(",
1568 "TEST_F(",
1569 "TEST_P(",
1570 "TYPED_TEST(",
1571 "TYPED_TEST_P(",
1572 "INSTANTIATE_TEST_SUITE_P(",
1573 "INSTANTIATE_TYPED_TEST_SUITE_P(",
1574 "TEST_CASE(",
1576 "SECTION(",
1577 "SCENARIO(",
1578 "SCENARIO_METHOD(",
1579 "TEST_CASE_METHOD(",
1580 "BOOST_AUTO_TEST_CASE(",
1582 "BOOST_FIXTURE_TEST_CASE(",
1583 "BOOST_AUTO_TEST_SUITE(",
1584 "BOOST_PARAM_TEST_CASE(",
1585 "CPPUNIT_TEST(",
1587 "CPPUNIT_TEST_SUITE(",
1588 "RUN_TEST(",
1590 "TEST_IGNORE(",
1591 "TEST_FAIL(",
1592 "START_TEST(",
1594 "tcase_add_test(",
1595 "suite_create(",
1596 "cmocka_unit_test(",
1598 "cmocka_run_group_tests(",
1599 "IGNORE_TEST(",
1601 "TEST_GROUP(",
1602 "TEST_GROUP_BASE(",
1603];
1604
1605const ASSERT_PATTERNS_C_CPP: &[&str] = &[
1607 "ASSERT_EQ(",
1609 "ASSERT_NE(",
1610 "ASSERT_LT(",
1611 "ASSERT_LE(",
1612 "ASSERT_GT(",
1613 "ASSERT_GE(",
1614 "ASSERT_TRUE(",
1615 "ASSERT_FALSE(",
1616 "ASSERT_STREQ(",
1617 "ASSERT_STRNE(",
1618 "ASSERT_FLOAT_EQ(",
1619 "ASSERT_DOUBLE_EQ(",
1620 "ASSERT_NEAR(",
1621 "ASSERT_THROW(",
1622 "ASSERT_NO_THROW(",
1623 "ASSERT_ANY_THROW(",
1624 "EXPECT_EQ(",
1626 "EXPECT_NE(",
1627 "EXPECT_LT(",
1628 "EXPECT_LE(",
1629 "EXPECT_GT(",
1630 "EXPECT_GE(",
1631 "EXPECT_TRUE(",
1632 "EXPECT_FALSE(",
1633 "EXPECT_STREQ(",
1634 "EXPECT_STRNE(",
1635 "EXPECT_FLOAT_EQ(",
1636 "EXPECT_DOUBLE_EQ(",
1637 "EXPECT_NEAR(",
1638 "EXPECT_THROW(",
1639 "EXPECT_NO_THROW(",
1640 "EXPECT_ANY_THROW(",
1641 "REQUIRE(",
1643 "CHECK(",
1644 "REQUIRE_FALSE(",
1645 "CHECK_FALSE(",
1646 "REQUIRE_NOTHROW(",
1647 "CHECK_NOTHROW(",
1648 "REQUIRE_THROWS(",
1649 "CHECK_THROWS(",
1650 "REQUIRE_THAT(",
1651 "CHECK_THAT(",
1652 "TEST_ASSERT_EQUAL(",
1654 "TEST_ASSERT_EQUAL_INT(",
1655 "TEST_ASSERT_EQUAL_STRING(",
1656 "TEST_ASSERT_EQUAL_FLOAT(",
1657 "TEST_ASSERT_EQUAL_DOUBLE(",
1658 "TEST_ASSERT_EQUAL_PTR(",
1659 "TEST_ASSERT_TRUE(",
1660 "TEST_ASSERT_FALSE(",
1661 "TEST_ASSERT_NULL(",
1662 "TEST_ASSERT_NOT_NULL(",
1663 "TEST_ASSERT_BITS_HIGH(",
1664 "TEST_ASSERT_BITS_LOW(",
1665 "assert_int_equal(",
1667 "assert_int_not_equal(",
1668 "assert_string_equal(",
1669 "assert_string_not_equal(",
1670 "assert_true(",
1671 "assert_false(",
1672 "assert_null(",
1673 "assert_non_null(",
1674 "assert_ptr_equal(",
1675 "assert_memory_equal(",
1676 "assert_return_code(",
1677];
1678
1679const SUITE_PATTERNS_C_CPP: &[&str] = &[
1681 "TEST_GROUP(",
1682 "TEST_GROUP_BASE(",
1683 "BOOST_AUTO_TEST_SUITE(",
1684 "CPPUNIT_TEST_SUITE(",
1685 "CPPUNIT_TEST_SUITE_END(",
1686];
1687
1688const SP_C: SymbolPatterns = SymbolPatterns {
1689 functions: &[],
1691 functions_prefix_paren: &[
1692 "void ",
1693 "int ",
1694 "char ",
1695 "float ",
1696 "double ",
1697 "long ",
1698 "unsigned ",
1699 "size_t ",
1700 "static ",
1701 "inline ",
1702 "const ",
1703 "extern ",
1704 ],
1705 classes: &[
1706 "struct ",
1707 "typedef struct ",
1708 "union ",
1709 "typedef union ",
1710 "typedef enum ",
1711 ],
1712 variables: &[],
1713 imports: &["#include "],
1714 tests: TEST_PATTERNS_C_CPP,
1715 assertions: ASSERT_PATTERNS_C_CPP,
1716 test_suites: SUITE_PATTERNS_C_CPP,
1717 variables_prefix_no_paren: &[
1720 "void ",
1721 "int ",
1722 "char ",
1723 "float ",
1724 "double ",
1725 "long ",
1726 "unsigned ",
1727 "size_t ",
1728 "static ",
1729 "inline ",
1730 "const ",
1731 "extern ",
1732 ],
1733};
1734
1735const SP_CPP: SymbolPatterns = SymbolPatterns {
1736 functions: &[
1738 "virtual ", "explicit ", "~", "operator", ],
1743 functions_prefix_paren: &[
1744 "void ",
1745 "bool ",
1746 "int ",
1747 "char ",
1748 "float ",
1749 "double ",
1750 "long ",
1751 "unsigned ",
1752 "size_t ",
1753 "auto ",
1754 "static ",
1755 "inline ",
1756 "constexpr ",
1757 "const ",
1758 "extern ",
1759 ],
1760 classes: &["class ", "struct ", "namespace ", "template ", "template<"],
1762 variables: &[],
1763 imports: &["#include "],
1764 tests: TEST_PATTERNS_C_CPP,
1765 assertions: ASSERT_PATTERNS_C_CPP,
1766 test_suites: SUITE_PATTERNS_C_CPP,
1767 variables_prefix_no_paren: &[
1769 "void ",
1770 "bool ",
1771 "int ",
1772 "char ",
1773 "float ",
1774 "double ",
1775 "long ",
1776 "unsigned ",
1777 "size_t ",
1778 "auto ",
1779 "static ",
1780 "inline ",
1781 "constexpr ",
1782 "const ",
1783 "extern ",
1784 ],
1785};
1786
1787const SP_SHELL: SymbolPatterns = SymbolPatterns {
1788 functions: &["function "],
1789 functions_prefix_paren: &[],
1790 classes: &[],
1791 variables: &["declare ", "local ", "export "],
1792 imports: &["source ", ". "],
1793 tests: &["@test "],
1795 assertions: &[],
1796 test_suites: &[],
1797 variables_prefix_no_paren: &[],
1798};
1799
1800const SP_POWERSHELL: SymbolPatterns = SymbolPatterns {
1801 functions: &["function ", "Function "],
1802 functions_prefix_paren: &[],
1803 classes: &["class "],
1804 variables: &[],
1805 imports: &["Import-Module ", "using "],
1806 tests: &["Describe ", "It ", "Context "],
1808 assertions: &[],
1809 test_suites: &[],
1810 variables_prefix_no_paren: &[],
1811};
1812
1813const SP_KOTLIN: SymbolPatterns = SymbolPatterns {
1814 functions: &[
1815 "fun ",
1816 "private fun ",
1817 "public fun ",
1818 "protected fun ",
1819 "internal fun ",
1820 "override fun ",
1821 "suspend fun ",
1822 "abstract fun ",
1823 "open fun ",
1824 "private suspend fun ",
1825 "public suspend fun ",
1826 ],
1827 functions_prefix_paren: &[],
1828 classes: &[
1829 "class ",
1830 "data class ",
1831 "sealed class ",
1832 "abstract class ",
1833 "open class ",
1834 "object ",
1835 "companion object",
1836 "interface ",
1837 "enum class ",
1838 "annotation class ",
1839 ],
1840 variables: &["val ", "var ", "private val ", "private var ", "const val "],
1841 imports: &["import "],
1842 tests: &[
1844 "@Test",
1845 "@ParameterizedTest",
1846 "@RepeatedTest",
1847 "\"should ",
1848 "\"it ",
1849 ],
1850 assertions: &[
1851 "assertEquals(",
1852 "assertNotEquals(",
1853 "assertTrue(",
1854 "assertFalse(",
1855 "assertNull(",
1856 "assertNotNull(",
1857 "assertThat(",
1858 "assertThrows(",
1859 "shouldBe(",
1860 "shouldNotBe(",
1861 "shouldThrow(",
1862 ],
1863 test_suites: &[],
1864 variables_prefix_no_paren: &[],
1865};
1866
1867const SP_SWIFT: SymbolPatterns = SymbolPatterns {
1868 functions: &[
1869 "func ",
1870 "private func ",
1871 "public func ",
1872 "internal func ",
1873 "override func ",
1874 "open func ",
1875 "static func ",
1876 "class func ",
1877 "mutating func ",
1878 "private static func ",
1879 "public static func ",
1880 ],
1881 functions_prefix_paren: &[],
1882 classes: &[
1883 "class ",
1884 "struct ",
1885 "protocol ",
1886 "enum ",
1887 "extension ",
1888 "actor ",
1889 "public class ",
1890 "private class ",
1891 "open class ",
1892 "final class ",
1893 "public struct ",
1894 "private struct ",
1895 "public protocol ",
1896 ],
1897 variables: &[
1898 "var ",
1899 "let ",
1900 "private var ",
1901 "private let ",
1902 "static var ",
1903 "static let ",
1904 ],
1905 imports: &["import "],
1906 tests: &["func test", "func Test", "@Test"],
1908 assertions: &[
1909 "XCTAssertEqual(",
1910 "XCTAssertNotEqual(",
1911 "XCTAssertTrue(",
1912 "XCTAssertFalse(",
1913 "XCTAssertNil(",
1914 "XCTAssertNotNil(",
1915 "XCTAssertGreaterThan(",
1916 "XCTAssertLessThan(",
1917 "XCTAssertThrowsError(",
1918 "XCTAssertNoThrow(",
1919 "#expect(",
1920 ],
1921 test_suites: &[],
1922 variables_prefix_no_paren: &[],
1923};
1924
1925const SP_RUBY: SymbolPatterns = SymbolPatterns {
1926 functions: &["def ", "private def ", "protected def "],
1927 functions_prefix_paren: &[],
1928 classes: &["class ", "module "],
1929 variables: &[],
1930 imports: &["require ", "require_relative "],
1931 tests: &["it ", "it(", "describe ", "context ", "test "],
1933 assertions: &[],
1934 test_suites: &[],
1935 variables_prefix_no_paren: &[],
1936};
1937
1938const SP_SCALA: SymbolPatterns = SymbolPatterns {
1939 functions: &["def ", "private def ", "protected def ", "override def "],
1940 functions_prefix_paren: &[],
1941 classes: &[
1942 "class ",
1943 "case class ",
1944 "abstract class ",
1945 "sealed class ",
1946 "object ",
1947 "trait ",
1948 ],
1949 variables: &["val ", "var ", "lazy val "],
1950 imports: &["import "],
1951 tests: &["test(", "it(", "describe("],
1953 assertions: &[],
1954 test_suites: &[],
1955 variables_prefix_no_paren: &[],
1956};
1957
1958const SP_PHP: SymbolPatterns = SymbolPatterns {
1959 functions: &[
1960 "function ",
1961 "public function ",
1962 "private function ",
1963 "protected function ",
1964 "static function ",
1965 "abstract function ",
1966 "final function ",
1967 "public static function ",
1968 "private static function ",
1969 "protected static function ",
1970 ],
1971 functions_prefix_paren: &[],
1972 classes: &[
1973 "class ",
1974 "abstract class ",
1975 "final class ",
1976 "interface ",
1977 "trait ",
1978 "enum ",
1979 ],
1980 variables: &[],
1981 imports: &[
1982 "use ",
1983 "require ",
1984 "require_once ",
1985 "include ",
1986 "include_once ",
1987 ],
1988 tests: &[
1990 "public function test",
1991 "function test",
1992 "#[Test]",
1993 "#[DataProvider(",
1994 ],
1995 assertions: &[],
1996 test_suites: &[],
1997 variables_prefix_no_paren: &[],
1998};
1999
2000const SP_ELIXIR: SymbolPatterns = SymbolPatterns {
2001 functions: &[
2002 "def ",
2003 "defp ",
2004 "defmacro ",
2005 "defmacrop ",
2006 "defguard ",
2007 "defguardp ",
2008 ],
2009 functions_prefix_paren: &[],
2010 classes: &["defmodule ", "defprotocol ", "defimpl "],
2011 variables: &[],
2012 imports: &["import ", "alias ", "use ", "require "],
2013 tests: &["test ", "describe "],
2015 assertions: &[],
2016 test_suites: &[],
2017 variables_prefix_no_paren: &[],
2018};
2019
2020const SP_ERLANG: SymbolPatterns = SymbolPatterns {
2021 functions: &[],
2022 functions_prefix_paren: &[],
2023 classes: &["-module("],
2024 variables: &[],
2025 imports: &["-import(", "-include(", "-include_lib("],
2026 tests: &[],
2029 assertions: &[
2030 "?assert(",
2031 "?assertEqual(",
2032 "?assertNotEqual(",
2033 "?assertMatch(",
2034 "?assertError(",
2035 "?assertThrow(",
2036 "?assertException(",
2037 ],
2038 test_suites: &[],
2039 variables_prefix_no_paren: &[],
2040};
2041
2042const SP_FSHARP: SymbolPatterns = SymbolPatterns {
2043 functions: &[
2044 "let ",
2045 "let rec ",
2046 "member ",
2047 "override ",
2048 "abstract member ",
2049 ],
2050 functions_prefix_paren: &[],
2051 classes: &["type "],
2052 variables: &["let mutable "],
2053 imports: &["open "],
2054 tests: &["[<Test>]", "[<Fact>]", "[<Theory>]", "[<TestCase("],
2056 assertions: &[],
2057 test_suites: &[],
2058 variables_prefix_no_paren: &[],
2059};
2060
2061const SP_GROOVY: SymbolPatterns = SymbolPatterns {
2062 functions: &["def ", "private def ", "public def ", "protected def "],
2063 functions_prefix_paren: &[],
2064 classes: &["class ", "abstract class ", "interface ", "enum ", "trait "],
2065 variables: &[],
2066 imports: &["import "],
2067 tests: &["def \"", "@Test", "given:", "when:", "then:", "expect:"],
2069 assertions: &[],
2070 test_suites: &[],
2071 variables_prefix_no_paren: &[],
2072};
2073
2074const SP_HASKELL: SymbolPatterns = SymbolPatterns {
2075 functions: &[],
2076 functions_prefix_paren: &[],
2077 classes: &["class ", "data ", "newtype ", "type "],
2078 variables: &[],
2079 imports: &["import "],
2080 tests: &["describe ", "it ", "prop_"],
2083 assertions: &[],
2084 test_suites: &[],
2085 variables_prefix_no_paren: &[],
2086};
2087
2088const SP_LUA: SymbolPatterns = SymbolPatterns {
2089 functions: &["function ", "local function "],
2090 functions_prefix_paren: &[],
2091 classes: &[],
2092 variables: &["local "],
2093 imports: &[],
2094 tests: &["it(", "describe(", "pending("],
2096 assertions: &[],
2097 test_suites: &[],
2098 variables_prefix_no_paren: &[],
2099};
2100
2101const SP_NIM: SymbolPatterns = SymbolPatterns {
2102 functions: &[
2103 "proc ",
2104 "func ",
2105 "method ",
2106 "iterator ",
2107 "converter ",
2108 "template ",
2109 "macro ",
2110 ],
2111 functions_prefix_paren: &[],
2112 classes: &["type "],
2113 variables: &["var ", "let ", "const "],
2114 imports: &["import ", "from "],
2115 tests: &["test "],
2117 assertions: &[],
2118 test_suites: &[],
2119 variables_prefix_no_paren: &[],
2120};
2121
2122const SP_OBJECTIVEC: SymbolPatterns = SymbolPatterns {
2123 functions: &["- (", "+ ("],
2124 functions_prefix_paren: &[],
2125 classes: &["@interface ", "@implementation ", "@protocol "],
2126 variables: &[],
2127 imports: &["#import ", "#include "],
2128 tests: &["- (void)test"],
2130 assertions: &[
2131 "XCTAssertEqual(",
2132 "XCTAssertNotEqual(",
2133 "XCTAssertTrue(",
2134 "XCTAssertFalse(",
2135 "XCTAssertNil(",
2136 "XCTAssertNotNil(",
2137 "XCTAssertGreaterThan(",
2138 "XCTAssertLessThan(",
2139 "XCTAssertThrowsError(",
2140 "XCTAssertNoThrow(",
2141 ],
2142 test_suites: &[],
2143 variables_prefix_no_paren: &[],
2144};
2145
2146const SP_OCAML: SymbolPatterns = SymbolPatterns {
2147 functions: &["let ", "let rec "],
2148 functions_prefix_paren: &[],
2149 classes: &["type ", "module ", "class "],
2150 variables: &[],
2151 imports: &["open "],
2152 tests: &["let test_", "test_case "],
2154 assertions: &[
2155 "assert_equal ",
2156 "assert_bool ",
2157 "assert_raises ",
2158 "assert_failure ",
2159 "OUnit.assert",
2160 ],
2161 test_suites: &[],
2162 variables_prefix_no_paren: &[],
2163};
2164
2165const SP_PERL: SymbolPatterns = SymbolPatterns {
2166 functions: &["sub "],
2167 functions_prefix_paren: &[],
2168 classes: &["package "],
2169 variables: &["my ", "our ", "local "],
2170 imports: &["use ", "require "],
2171 tests: &["subtest "],
2173 assertions: &[
2174 "ok(",
2175 "is(",
2176 "isnt(",
2177 "like(",
2178 "unlike(",
2179 "cmp_ok(",
2180 "is_deeply(",
2181 "isa_ok(",
2182 "can_ok(",
2183 ],
2184 test_suites: &[],
2185 variables_prefix_no_paren: &[],
2186};
2187
2188const SP_CLOJURE: SymbolPatterns = SymbolPatterns {
2189 functions: &["(defn ", "(defn- ", "(defmacro ", "(defmulti "],
2190 functions_prefix_paren: &[],
2191 classes: &[
2192 "(defrecord ",
2193 "(defprotocol ",
2194 "(deftype ",
2195 "(definterface ",
2196 ],
2197 variables: &["(def ", "(defonce "],
2198 imports: &["(ns ", "(require "],
2199 tests: &["(deftest ", "(testing "],
2201 assertions: &[],
2202 test_suites: &[],
2203 variables_prefix_no_paren: &[],
2204};
2205
2206const SP_JULIA: SymbolPatterns = SymbolPatterns {
2207 functions: &["function ", "macro "],
2208 functions_prefix_paren: &[],
2209 classes: &[
2210 "struct ",
2211 "mutable struct ",
2212 "abstract type ",
2213 "primitive type ",
2214 ],
2215 variables: &["const "],
2216 imports: &["import ", "using "],
2217 tests: &["@test ", "@testset "],
2219 assertions: &[],
2220 test_suites: &[],
2221 variables_prefix_no_paren: &[],
2222};
2223
2224const SP_DART: SymbolPatterns = SymbolPatterns {
2225 functions: &[],
2226 functions_prefix_paren: &[],
2227 classes: &["class ", "abstract class ", "mixin ", "extension ", "enum "],
2228 variables: &["var ", "final ", "const ", "late "],
2229 imports: &["import "],
2230 tests: &["test(", "testWidgets(", "group("],
2232 assertions: &[],
2233 test_suites: &[],
2234 variables_prefix_no_paren: &[],
2235};
2236
2237const SP_R: SymbolPatterns = SymbolPatterns {
2238 functions: &[],
2239 functions_prefix_paren: &[],
2240 classes: &[],
2241 variables: &[],
2242 imports: &["library(", "source("],
2243 tests: &["test_that(", "it(", "describe(", "expect_"],
2245 assertions: &[],
2246 test_suites: &[],
2247 variables_prefix_no_paren: &[],
2248};
2249
2250const SP_SQL: SymbolPatterns = SymbolPatterns {
2251 functions: &[
2252 "create function ",
2253 "create or replace function ",
2254 "create procedure ",
2255 "create or replace procedure ",
2256 "CREATE FUNCTION ",
2257 "CREATE OR REPLACE FUNCTION ",
2258 "CREATE PROCEDURE ",
2259 "CREATE OR REPLACE PROCEDURE ",
2260 ],
2261 functions_prefix_paren: &[],
2262 classes: &[
2263 "create table ",
2264 "create view ",
2265 "create schema ",
2266 "CREATE TABLE ",
2267 "CREATE VIEW ",
2268 "CREATE SCHEMA ",
2269 ],
2270 variables: &["declare ", "DECLARE "],
2271 imports: &[],
2272 tests: &[],
2273 assertions: &[],
2274 test_suites: &[],
2275 variables_prefix_no_paren: &[],
2276};
2277
2278const SP_ASSEMBLY: SymbolPatterns = SymbolPatterns {
2279 functions: &["proc ", "PROC "],
2280 functions_prefix_paren: &[],
2281 classes: &[],
2282 variables: &[],
2283 imports: &["include ", "INCLUDE ", "%include "],
2284 tests: &[],
2285 assertions: &[],
2286 test_suites: &[],
2287 variables_prefix_no_paren: &[],
2288};
2289
2290const SP_ZIG: SymbolPatterns = SymbolPatterns {
2291 functions: &[
2292 "fn ",
2293 "pub fn ",
2294 "export fn ",
2295 "inline fn ",
2296 "pub inline fn ",
2297 ],
2298 functions_prefix_paren: &[],
2299 classes: &[],
2300 variables: &["var ", "pub var "],
2301 imports: &[],
2302 tests: &["test \"", "test{"],
2304 assertions: &[],
2305 test_suites: &[],
2306 variables_prefix_no_paren: &[],
2307};
2308
2309#[allow(clippy::struct_excessive_bools)]
2313#[derive(Clone, Copy)]
2314struct StaticLangConfig {
2315 line_comments: &'static [&'static str],
2316 block_comment: Option<(&'static str, &'static str)>,
2317 allow_single_quote_strings: bool,
2318 allow_double_quote_strings: bool,
2319 allow_triple_quote_strings: bool,
2320 allow_csharp_verbatim_strings: bool,
2321 allow_raw_strings: bool,
2324 symbol_patterns: SymbolPatterns,
2325 has_preprocessor: bool,
2327}
2328
2329#[allow(clippy::struct_excessive_bools)]
2330#[derive(Debug, Clone)]
2331struct ScanConfig {
2332 line_comments: &'static [&'static str],
2333 block_comment: Option<(&'static str, &'static str)>,
2334 allow_single_quote_strings: bool,
2335 allow_double_quote_strings: bool,
2336 allow_triple_quote_strings: bool,
2337 allow_csharp_verbatim_strings: bool,
2338 allow_raw_strings: bool,
2339 skip_lines: HashSet<usize>,
2340 symbol_patterns: SymbolPatterns,
2341 branch_keywords: &'static [&'static str],
2343 lsloc_strategy: LslocStrategy,
2345}
2346
2347const C_SLASH_BASE: StaticLangConfig = StaticLangConfig {
2357 line_comments: &["//"],
2358 block_comment: Some(("/*", "*/")),
2359 allow_single_quote_strings: true,
2360 allow_double_quote_strings: true,
2361 allow_triple_quote_strings: false,
2362 allow_csharp_verbatim_strings: false,
2363 allow_raw_strings: false,
2364 symbol_patterns: SP_NONE,
2365 has_preprocessor: false,
2366};
2367
2368const HASH_BASE: StaticLangConfig = StaticLangConfig {
2372 line_comments: &["#"],
2373 block_comment: None,
2374 allow_single_quote_strings: true,
2375 allow_double_quote_strings: true,
2376 allow_triple_quote_strings: false,
2377 allow_csharp_verbatim_strings: false,
2378 allow_raw_strings: false,
2379 symbol_patterns: SP_NONE,
2380 has_preprocessor: false,
2381};
2382
2383static LANG_SCAN_TABLE: &[(Language, StaticLangConfig)] = &[
2387 (
2389 Language::C,
2390 StaticLangConfig {
2391 symbol_patterns: SP_C,
2392 has_preprocessor: true,
2393 ..C_SLASH_BASE
2394 },
2395 ),
2396 (
2397 Language::Cpp,
2398 StaticLangConfig {
2399 symbol_patterns: SP_CPP,
2400 has_preprocessor: true,
2401 ..C_SLASH_BASE
2402 },
2403 ),
2404 (
2405 Language::ObjectiveC,
2406 StaticLangConfig {
2407 symbol_patterns: SP_OBJECTIVEC,
2408 has_preprocessor: true,
2409 ..C_SLASH_BASE
2410 },
2411 ),
2412 (
2414 Language::CSharp,
2415 StaticLangConfig {
2416 symbol_patterns: SP_CSHARP,
2417 allow_csharp_verbatim_strings: true,
2418 ..C_SLASH_BASE
2419 },
2420 ),
2421 (
2422 Language::Go,
2423 StaticLangConfig {
2424 symbol_patterns: SP_GO,
2425 ..C_SLASH_BASE
2426 },
2427 ),
2428 (
2429 Language::Java,
2430 StaticLangConfig {
2431 symbol_patterns: SP_JAVA,
2432 ..C_SLASH_BASE
2433 },
2434 ),
2435 (
2436 Language::JavaScript,
2437 StaticLangConfig {
2438 symbol_patterns: SP_JS,
2439 ..C_SLASH_BASE
2440 },
2441 ),
2442 (
2443 Language::TypeScript,
2444 StaticLangConfig {
2445 symbol_patterns: SP_TS,
2446 ..C_SLASH_BASE
2447 },
2448 ),
2449 (
2450 Language::Svelte,
2451 StaticLangConfig {
2452 symbol_patterns: SP_JS,
2453 ..C_SLASH_BASE
2454 },
2455 ),
2456 (
2457 Language::Vue,
2458 StaticLangConfig {
2459 symbol_patterns: SP_JS,
2460 ..C_SLASH_BASE
2461 },
2462 ),
2463 (
2464 Language::Dart,
2465 StaticLangConfig {
2466 symbol_patterns: SP_DART,
2467 ..C_SLASH_BASE
2468 },
2469 ),
2470 (
2471 Language::Groovy,
2472 StaticLangConfig {
2473 symbol_patterns: SP_GROOVY,
2474 ..C_SLASH_BASE
2475 },
2476 ),
2477 (
2478 Language::Kotlin,
2479 StaticLangConfig {
2480 symbol_patterns: SP_KOTLIN,
2481 ..C_SLASH_BASE
2482 },
2483 ),
2484 (
2485 Language::Scala,
2486 StaticLangConfig {
2487 symbol_patterns: SP_SCALA,
2488 ..C_SLASH_BASE
2489 },
2490 ),
2491 (
2492 Language::Scss,
2493 StaticLangConfig {
2494 symbol_patterns: SP_NONE,
2495 ..C_SLASH_BASE
2496 },
2497 ),
2498 (
2500 Language::Rust,
2501 StaticLangConfig {
2502 symbol_patterns: SP_RUST,
2503 allow_single_quote_strings: false,
2504 allow_raw_strings: true,
2505 ..C_SLASH_BASE
2506 },
2507 ),
2508 (
2510 Language::Swift,
2511 StaticLangConfig {
2512 symbol_patterns: SP_SWIFT,
2513 allow_single_quote_strings: false,
2514 ..C_SLASH_BASE
2515 },
2516 ),
2517 (
2519 Language::Zig,
2520 StaticLangConfig {
2521 symbol_patterns: SP_ZIG,
2522 block_comment: None,
2523 ..C_SLASH_BASE
2524 },
2525 ),
2526 (
2528 Language::FSharp,
2529 StaticLangConfig {
2530 line_comments: &["//"],
2531 block_comment: Some(("(*", "*)")),
2532 allow_single_quote_strings: false,
2533 allow_double_quote_strings: true,
2534 symbol_patterns: SP_FSHARP,
2535 ..C_SLASH_BASE
2536 },
2537 ),
2538 (
2540 Language::Shell,
2541 StaticLangConfig {
2542 symbol_patterns: SP_SHELL,
2543 ..HASH_BASE
2544 },
2545 ),
2546 (
2547 Language::Elixir,
2548 StaticLangConfig {
2549 symbol_patterns: SP_ELIXIR,
2550 ..HASH_BASE
2551 },
2552 ),
2553 (
2554 Language::Perl,
2555 StaticLangConfig {
2556 symbol_patterns: SP_PERL,
2557 ..HASH_BASE
2558 },
2559 ),
2560 (
2561 Language::R,
2562 StaticLangConfig {
2563 symbol_patterns: SP_R,
2564 ..HASH_BASE
2565 },
2566 ),
2567 (
2568 Language::Ruby,
2569 StaticLangConfig {
2570 symbol_patterns: SP_RUBY,
2571 ..HASH_BASE
2572 },
2573 ),
2574 (
2576 Language::Python,
2577 StaticLangConfig {
2578 symbol_patterns: SP_PYTHON,
2579 allow_triple_quote_strings: true,
2580 ..HASH_BASE
2581 },
2582 ),
2583 (
2585 Language::PowerShell,
2586 StaticLangConfig {
2587 symbol_patterns: SP_POWERSHELL,
2588 block_comment: Some(("<#", "#>")),
2589 ..HASH_BASE
2590 },
2591 ),
2592 (
2594 Language::Nim,
2595 StaticLangConfig {
2596 symbol_patterns: SP_NIM,
2597 block_comment: Some(("#[", "]#")),
2598 ..HASH_BASE
2599 },
2600 ),
2601 (
2603 Language::Makefile,
2604 StaticLangConfig {
2605 symbol_patterns: SP_NONE,
2606 allow_single_quote_strings: false,
2607 allow_double_quote_strings: false,
2608 ..HASH_BASE
2609 },
2610 ),
2611 (
2612 Language::Dockerfile,
2613 StaticLangConfig {
2614 symbol_patterns: SP_NONE,
2615 allow_single_quote_strings: false,
2616 allow_double_quote_strings: false,
2617 ..HASH_BASE
2618 },
2619 ),
2620 (
2623 Language::Css,
2624 StaticLangConfig {
2625 line_comments: &[],
2626 block_comment: Some(("/*", "*/")),
2627 symbol_patterns: SP_NONE,
2628 ..C_SLASH_BASE
2629 },
2630 ),
2631 (
2633 Language::Html,
2634 StaticLangConfig {
2635 line_comments: &[],
2636 block_comment: Some(("<!--", "-->")),
2637 allow_single_quote_strings: false,
2638 allow_double_quote_strings: false,
2639 symbol_patterns: SP_NONE,
2640 ..C_SLASH_BASE
2641 },
2642 ),
2643 (
2644 Language::Xml,
2645 StaticLangConfig {
2646 line_comments: &[],
2647 block_comment: Some(("<!--", "-->")),
2648 allow_single_quote_strings: false,
2649 allow_double_quote_strings: false,
2650 symbol_patterns: SP_NONE,
2651 ..C_SLASH_BASE
2652 },
2653 ),
2654 (
2656 Language::Lua,
2657 StaticLangConfig {
2658 line_comments: &["--"],
2659 block_comment: Some(("--[[", "]]")),
2660 symbol_patterns: SP_LUA,
2661 ..C_SLASH_BASE
2662 },
2663 ),
2664 (
2666 Language::Haskell,
2667 StaticLangConfig {
2668 line_comments: &["--"],
2669 block_comment: Some(("{-", "-}")),
2670 symbol_patterns: SP_HASKELL,
2671 ..C_SLASH_BASE
2672 },
2673 ),
2674 (
2676 Language::Sql,
2677 StaticLangConfig {
2678 line_comments: &["--"],
2679 block_comment: Some(("/*", "*/")),
2680 allow_single_quote_strings: true,
2681 allow_double_quote_strings: false,
2682 symbol_patterns: SP_SQL,
2683 ..C_SLASH_BASE
2684 },
2685 ),
2686 (
2688 Language::Ocaml,
2689 StaticLangConfig {
2690 line_comments: &[],
2691 block_comment: Some(("(*", "*)")),
2692 allow_single_quote_strings: false,
2693 symbol_patterns: SP_OCAML,
2694 ..C_SLASH_BASE
2695 },
2696 ),
2697 (
2703 Language::Assembly,
2704 StaticLangConfig {
2705 line_comments: &[";"],
2706 block_comment: Some(("/*", "*/")),
2707 allow_single_quote_strings: false,
2708 allow_double_quote_strings: true,
2709 symbol_patterns: SP_ASSEMBLY,
2710 ..C_SLASH_BASE
2711 },
2712 ),
2713 (
2714 Language::Clojure,
2715 StaticLangConfig {
2716 line_comments: &[";"],
2717 block_comment: None,
2718 allow_single_quote_strings: false,
2719 symbol_patterns: SP_CLOJURE,
2720 ..C_SLASH_BASE
2721 },
2722 ),
2723 (
2725 Language::Erlang,
2726 StaticLangConfig {
2727 line_comments: &["%"],
2728 block_comment: None,
2729 allow_single_quote_strings: false,
2730 symbol_patterns: SP_ERLANG,
2731 ..C_SLASH_BASE
2732 },
2733 ),
2734 (
2736 Language::Php,
2737 StaticLangConfig {
2738 line_comments: &["//", "#"],
2739 block_comment: Some(("/*", "*/")),
2740 symbol_patterns: SP_PHP,
2741 ..C_SLASH_BASE
2742 },
2743 ),
2744 (
2746 Language::Julia,
2747 StaticLangConfig {
2748 line_comments: &["#"],
2749 block_comment: Some(("#=", "=#")),
2750 allow_single_quote_strings: false,
2751 allow_triple_quote_strings: true,
2752 symbol_patterns: SP_JULIA,
2753 ..C_SLASH_BASE
2754 },
2755 ),
2756 (
2759 Language::Solidity,
2760 StaticLangConfig {
2761 symbol_patterns: SP_SOLIDITY,
2762 ..C_SLASH_BASE
2763 },
2764 ),
2765 (
2767 Language::Protobuf,
2768 StaticLangConfig {
2769 symbol_patterns: SP_PROTOBUF,
2770 ..C_SLASH_BASE
2771 },
2772 ),
2773 (
2775 Language::Hcl,
2776 StaticLangConfig {
2777 line_comments: &["#", "//"],
2778 allow_single_quote_strings: false,
2779 symbol_patterns: SP_NONE,
2780 ..C_SLASH_BASE
2781 },
2782 ),
2783 (
2785 Language::GraphQl,
2786 StaticLangConfig {
2787 allow_single_quote_strings: false,
2788 allow_triple_quote_strings: true,
2789 symbol_patterns: SP_NONE,
2790 ..HASH_BASE
2791 },
2792 ),
2793 (
2796 Language::Ada,
2797 StaticLangConfig {
2798 line_comments: &["--"],
2799 block_comment: None,
2800 allow_single_quote_strings: false,
2801 symbol_patterns: SP_ADA,
2802 ..C_SLASH_BASE
2803 },
2804 ),
2805 (
2807 Language::Vhdl,
2808 StaticLangConfig {
2809 line_comments: &["--"],
2810 block_comment: None,
2811 allow_single_quote_strings: false,
2812 symbol_patterns: SP_VHDL,
2813 ..C_SLASH_BASE
2814 },
2815 ),
2816 (
2818 Language::Verilog,
2819 StaticLangConfig {
2820 allow_single_quote_strings: false,
2821 symbol_patterns: SP_VERILOG,
2822 ..C_SLASH_BASE
2823 },
2824 ),
2825 (
2827 Language::Tcl,
2828 StaticLangConfig {
2829 allow_single_quote_strings: false,
2830 symbol_patterns: SP_TCL,
2831 ..HASH_BASE
2832 },
2833 ),
2834 (
2836 Language::Pascal,
2837 StaticLangConfig {
2838 line_comments: &["//"],
2839 block_comment: Some(("{", "}")),
2840 allow_single_quote_strings: true,
2841 allow_double_quote_strings: false,
2842 symbol_patterns: SP_PASCAL,
2843 ..C_SLASH_BASE
2844 },
2845 ),
2846 (
2848 Language::VisualBasic,
2849 StaticLangConfig {
2850 line_comments: &["'"],
2851 block_comment: None,
2852 allow_single_quote_strings: false,
2853 allow_double_quote_strings: true,
2854 symbol_patterns: SP_VB,
2855 ..C_SLASH_BASE
2856 },
2857 ),
2858 (
2860 Language::Lisp,
2861 StaticLangConfig {
2862 line_comments: &[";"],
2863 block_comment: Some(("#|", "|#")),
2864 allow_single_quote_strings: false,
2865 symbol_patterns: SP_LISP,
2866 ..C_SLASH_BASE
2867 },
2868 ),
2869 (
2872 Language::Fortran,
2873 StaticLangConfig {
2874 line_comments: &["!"],
2875 block_comment: None,
2876 symbol_patterns: SP_FORTRAN,
2877 ..C_SLASH_BASE
2878 },
2879 ),
2880 (
2882 Language::Nix,
2883 StaticLangConfig {
2884 block_comment: Some(("/*", "*/")),
2885 allow_single_quote_strings: false,
2886 symbol_patterns: SP_NONE,
2887 ..HASH_BASE
2888 },
2889 ),
2890 (
2892 Language::Crystal,
2893 StaticLangConfig {
2894 symbol_patterns: SP_CRYSTAL,
2895 ..HASH_BASE
2896 },
2897 ),
2898 (
2900 Language::D,
2901 StaticLangConfig {
2902 symbol_patterns: SP_D,
2903 ..C_SLASH_BASE
2904 },
2905 ),
2906 (
2908 Language::Glsl,
2909 StaticLangConfig {
2910 allow_single_quote_strings: false,
2911 symbol_patterns: SP_NONE,
2912 ..C_SLASH_BASE
2913 },
2914 ),
2915 (
2917 Language::Cmake,
2918 StaticLangConfig {
2919 block_comment: Some(("#[[", "]]")),
2920 allow_single_quote_strings: false,
2921 symbol_patterns: SP_CMAKE,
2922 ..HASH_BASE
2923 },
2924 ),
2925 (
2927 Language::Elm,
2928 StaticLangConfig {
2929 line_comments: &["--"],
2930 block_comment: Some(("{-", "-}")),
2931 allow_single_quote_strings: false,
2932 symbol_patterns: SP_ELM,
2933 ..C_SLASH_BASE
2934 },
2935 ),
2936 (
2938 Language::Awk,
2939 StaticLangConfig {
2940 allow_single_quote_strings: false,
2941 symbol_patterns: SP_AWK,
2942 ..HASH_BASE
2943 },
2944 ),
2945];
2946
2947#[derive(Debug, Clone, Copy)]
2950struct IeeeFlags {
2951 has_preprocessor_directives: bool,
2953 blank_in_block_comment_as_comment: bool,
2955 collapse_continuation_lines: bool,
2957}
2958
2959#[derive(Debug, Clone, Copy)]
2960enum StringState {
2961 Single(char),
2962 Triple(&'static str),
2963 VerbatimDouble,
2964 RawHash(usize),
2967}
2968
2969#[allow(clippy::struct_excessive_bools)]
2970#[derive(Debug, Default)]
2971struct LineFacts {
2972 has_code: bool,
2973 has_single_comment: bool,
2974 has_multi_comment: bool,
2975 has_docstring: bool,
2976}
2977
2978fn process_string_char(
2982 state: StringState,
2983 chars: &[char],
2984 i: usize,
2985) -> (Option<StringState>, usize) {
2986 match state {
2987 StringState::Single(delim) => step_single(state, delim, chars, i),
2988 StringState::Triple(delim) => step_triple(state, delim, chars, i),
2989 StringState::VerbatimDouble => step_verbatim(state, chars, i),
2990 StringState::RawHash(hashes) => step_raw_hash(state, hashes, chars, i),
2991 }
2992}
2993
2994fn step_single(
2996 state: StringState,
2997 delim: char,
2998 chars: &[char],
2999 i: usize,
3000) -> (Option<StringState>, usize) {
3001 if chars[i] == '\\' {
3002 return (Some(state), 2); }
3004 if chars[i] == delim {
3005 (None, 1)
3006 } else {
3007 (Some(state), 1)
3008 }
3009}
3010
3011fn step_triple(
3013 state: StringState,
3014 delim: &'static str,
3015 chars: &[char],
3016 i: usize,
3017) -> (Option<StringState>, usize) {
3018 if starts_with(chars, i, delim) {
3019 (None, delim.len())
3020 } else {
3021 (Some(state), 1)
3022 }
3023}
3024
3025fn step_verbatim(state: StringState, chars: &[char], i: usize) -> (Option<StringState>, usize) {
3027 if starts_with(chars, i, "\"\"") {
3028 return (Some(state), 2); }
3030 if chars[i] == '"' {
3031 (None, 1)
3032 } else {
3033 (Some(state), 1)
3034 }
3035}
3036
3037fn step_raw_hash(
3039 state: StringState,
3040 hashes: usize,
3041 chars: &[char],
3042 i: usize,
3043) -> (Option<StringState>, usize) {
3044 if chars[i] == '"' && count_leading_hashes(chars, i + 1) >= hashes {
3045 (None, 1 + hashes)
3046 } else {
3047 (Some(state), 1)
3048 }
3049}
3050
3051fn count_leading_hashes(chars: &[char], index: usize) -> usize {
3053 let mut n = 0;
3054 while chars.get(index + n) == Some(&'#') {
3055 n += 1;
3056 }
3057 n
3058}
3059
3060fn try_open_raw_string(chars: &[char], i: usize) -> Option<(usize, usize)> {
3065 let word_before = i
3066 .checked_sub(1)
3067 .and_then(|p| chars.get(p))
3068 .is_some_and(|c| c.is_alphanumeric() || *c == '_');
3069 if word_before {
3070 return None;
3071 }
3072 let mut j = i;
3073 if chars.get(j) == Some(&'b') {
3074 j += 1; }
3076 if chars.get(j) != Some(&'r') {
3077 return None;
3078 }
3079 j += 1;
3080 let hashes = count_leading_hashes(chars, j);
3081 j += hashes;
3082 if chars.get(j) != Some(&'"') {
3083 return None;
3084 }
3085 Some((hashes, j + 1 - i))
3086}
3087
3088fn process_block_comment_char(chars: &[char], i: usize, close: &str) -> (bool, usize) {
3092 if starts_with(chars, i, close) {
3093 (false, close.len())
3094 } else {
3095 (true, 1)
3096 }
3097}
3098
3099fn try_open_string(chars: &[char], i: usize, config: &ScanConfig) -> Option<(StringState, usize)> {
3103 if config.allow_raw_strings
3104 && let Some((hashes, advance)) = try_open_raw_string(chars, i)
3105 {
3106 return Some((StringState::RawHash(hashes), advance));
3107 }
3108 if config.allow_csharp_verbatim_strings && starts_with(chars, i, "@\"") {
3109 return Some((StringState::VerbatimDouble, 2));
3110 }
3111 if config.allow_triple_quote_strings {
3112 if starts_with(chars, i, "\"\"\"") {
3113 return Some((StringState::Triple("\"\"\""), 3));
3114 }
3115 if starts_with(chars, i, "'''") {
3116 return Some((StringState::Triple("'''"), 3));
3117 }
3118 }
3119 if config.allow_single_quote_strings && chars[i] == '\'' {
3120 return Some((StringState::Single('\''), 1));
3121 }
3122 if config.allow_double_quote_strings && chars[i] == '"' {
3123 return Some((StringState::Single('"'), 1));
3124 }
3125 None
3126}
3127
3128fn step_through_block_comment(
3134 chars: &[char],
3135 i: usize,
3136 block_comment: Option<(&'static str, &'static str)>,
3137 in_block_comment: &mut bool,
3138) -> usize {
3139 if let Some((_, close)) = block_comment {
3140 let (still_in, advance) = process_block_comment_char(chars, i, close);
3141 *in_block_comment = still_in;
3142 return advance;
3143 }
3144 0
3145}
3146
3147fn try_open_block_comment(
3150 chars: &[char],
3151 i: usize,
3152 block_comment: Option<(&'static str, &'static str)>,
3153) -> Option<usize> {
3154 let (open, _) = block_comment?;
3155 starts_with(chars, i, open).then_some(open.len())
3156}
3157
3158fn advance_inside_span(
3162 chars: &[char],
3163 i: usize,
3164 config: &ScanConfig,
3165 facts: &mut LineFacts,
3166 in_block_comment: &mut bool,
3167 string_state: &mut Option<StringState>,
3168 code: &mut Vec<u8>,
3169) -> Option<usize> {
3170 if let Some(state) = *string_state {
3172 facts.has_code = true;
3173 let (new_state, advance) = process_string_char(state, chars, i);
3174 *string_state = new_state;
3175 blank_mask(code, advance);
3176 return Some(advance);
3177 }
3178
3179 if *in_block_comment {
3181 facts.has_multi_comment = true;
3182 let advance = step_through_block_comment(chars, i, config.block_comment, in_block_comment);
3183 blank_mask(code, advance);
3184 return Some(advance);
3185 }
3186
3187 None
3188}
3189
3190fn scan_line(
3194 chars: &[char],
3195 config: &ScanConfig,
3196 facts: &mut LineFacts,
3197 in_block_comment: &mut bool,
3198 string_state: &mut Option<StringState>,
3199 code: &mut Vec<u8>,
3200) {
3201 let mut i = 0usize;
3202 while i < chars.len() {
3203 if let Some(advance) = advance_inside_span(
3205 chars,
3206 i,
3207 config,
3208 facts,
3209 in_block_comment,
3210 string_state,
3211 code,
3212 ) {
3213 i += advance;
3214 continue;
3215 }
3216
3217 if chars[i].is_whitespace() {
3219 code.push(b' ');
3220 i += 1;
3221 continue;
3222 }
3223
3224 if let Some((new_state, advance)) = try_open_string(chars, i, config) {
3226 facts.has_code = true;
3227 *string_state = Some(new_state);
3228 blank_mask(code, advance);
3229 i += advance;
3230 continue;
3231 }
3232
3233 if let Some(advance) = try_open_block_comment(chars, i, config.block_comment) {
3235 facts.has_multi_comment = true;
3236 *in_block_comment = true;
3237 blank_mask(code, advance);
3238 i += advance;
3239 continue;
3240 }
3241
3242 if config
3244 .line_comments
3245 .iter()
3246 .any(|prefix| starts_with(chars, i, prefix))
3247 {
3248 facts.has_single_comment = true;
3249 break;
3250 }
3251
3252 facts.has_code = true;
3255 let ch = chars[i];
3256 code.push(if ch.is_ascii() { ch as u8 } else { b' ' });
3257 i += 1;
3258 }
3259}
3260
3261fn blank_mask(code: &mut Vec<u8>, n: usize) {
3264 code.resize(code.len() + n, b' ');
3265}
3266
3267fn finalize_line_facts(
3272 facts: LineFacts,
3273 trimmed: &str,
3274 raw: &mut RawLineCounts,
3275 ieee: IeeeFlags,
3276 in_block_comment: bool,
3277 string_state: Option<StringState>,
3278 pending_continuation: &mut Option<LineFacts>,
3279) -> Option<LineFacts> {
3280 if ieee.has_preprocessor_directives
3284 && facts.has_code
3285 && !facts.has_single_comment
3286 && !facts.has_multi_comment
3287 && trimmed.starts_with('#')
3288 {
3289 raw.compiler_directive_lines += 1;
3290 }
3291
3292 let is_continuation = ieee.collapse_continuation_lines
3295 && !in_block_comment
3296 && string_state.is_none()
3297 && trimmed.ends_with('\\');
3298
3299 if is_continuation {
3300 let pending = pending_continuation.get_or_insert_with(LineFacts::default);
3301 pending.has_code |= facts.has_code;
3302 pending.has_single_comment |= facts.has_single_comment;
3303 pending.has_multi_comment |= facts.has_multi_comment;
3304 pending.has_docstring |= facts.has_docstring;
3305 return None; }
3307
3308 let emit = if let Some(pending) = pending_continuation.take() {
3310 LineFacts {
3311 has_code: pending.has_code | facts.has_code,
3312 has_single_comment: pending.has_single_comment | facts.has_single_comment,
3313 has_multi_comment: pending.has_multi_comment | facts.has_multi_comment,
3314 has_docstring: pending.has_docstring | facts.has_docstring,
3315 }
3316 } else {
3317 facts
3318 };
3319 Some(emit)
3320}
3321
3322#[allow(clippy::needless_pass_by_value)]
3327#[allow(clippy::too_many_arguments)]
3328#[allow(clippy::many_single_char_names)] fn process_physical_line(
3330 line: &str,
3331 line_idx: usize,
3332 config: &ScanConfig,
3333 raw: &mut RawLineCounts,
3334 in_block_comment: &mut bool,
3335 string_state: &mut Option<StringState>,
3336 pending_continuation: &mut Option<LineFacts>,
3337 ieee: IeeeFlags,
3338 scope: &mut CScopeState,
3339) {
3340 raw.total_physical_lines += 1;
3341
3342 if config.skip_lines.contains(&line_idx) {
3343 raw.docstring_comment_lines += 1;
3344 return;
3345 }
3346
3347 let trimmed = line.trim();
3348 let mut facts = LineFacts::default();
3349
3350 if *in_block_comment && (ieee.blank_in_block_comment_as_comment || !trimmed.is_empty()) {
3354 facts.has_multi_comment = true;
3355 }
3356
3357 let chars: Vec<char> = line.chars().collect();
3358 let mut code_mask: Vec<u8> = Vec::with_capacity(chars.len());
3363 scan_line(
3364 &chars,
3365 config,
3366 &mut facts,
3367 in_block_comment,
3368 string_state,
3369 &mut code_mask,
3370 );
3371
3372 let Some(emit) = finalize_line_facts(
3373 facts,
3374 trimmed,
3375 raw,
3376 ieee,
3377 *in_block_comment,
3378 *string_state,
3379 pending_continuation,
3380 ) else {
3381 return;
3382 };
3383
3384 classify_line(raw, &emit, trimmed);
3385
3386 if emit.has_code {
3387 accumulate_code_line(raw, config, trimmed, scope, &code_mask);
3388 }
3389}
3390
3391#[allow(clippy::many_single_char_names)] fn accumulate_code_line(
3395 raw: &mut RawLineCounts,
3396 config: &ScanConfig,
3397 trimmed: &str,
3398 scope: &mut CScopeState,
3399 code_mask: &[u8],
3400) {
3401 use std::hash::{DefaultHasher, Hash, Hasher};
3402 let (f, c, v, i, t, a, s) = count_symbols(&config.symbol_patterns, trimmed);
3403 raw.functions += f;
3404 raw.classes += c;
3405 raw.variables += v;
3406 raw.imports += i;
3407 raw.test_count += t;
3408 raw.test_assertion_count += a;
3409 raw.test_suite_count += s;
3410
3411 if !config.symbol_patterns.functions_prefix_paren.is_empty() {
3415 accumulate_c_family(raw, v, trimmed, scope);
3416 }
3417
3418 raw.cyclomatic_complexity += count_branch_in_line(code_mask, config.branch_keywords);
3421
3422 accumulate_lsloc(raw, trimmed, config.lsloc_strategy);
3424
3425 let mut h = DefaultHasher::new();
3427 trimmed.hash(&mut h);
3428 raw.code_line_hashes.push(h.finish());
3429}
3430
3431fn accumulate_c_family(
3434 raw: &mut RawLineCounts,
3435 var_count: u64,
3436 trimmed: &str,
3437 scope: &mut CScopeState,
3438) {
3439 if var_count == 1 {
3440 match scope.current_var_kind() {
3441 VarKind::Member => raw.variables_member += 1,
3442 VarKind::Local => raw.variables_local += 1,
3443 VarKind::Global => raw.variables_global += 1,
3444 }
3445 }
3446 if is_object_like_macro(trimmed) {
3447 raw.macro_definitions += 1;
3448 }
3449 scope.update(trimmed);
3450}
3451
3452fn accumulate_lsloc(raw: &mut RawLineCounts, trimmed: &str, strategy: LslocStrategy) {
3454 match strategy {
3455 LslocStrategy::Semicolons => {
3456 let semi =
3457 u32::try_from(trimmed.bytes().filter(|&b| b == b';').count()).unwrap_or(u32::MAX);
3458 *raw.lsloc.get_or_insert(0) += semi;
3459 }
3460 LslocStrategy::NonContinuationNewlines => {
3461 let cont = trimmed.ends_with('\\')
3462 || trimmed.ends_with(',')
3463 || trimmed.ends_with('(')
3464 || trimmed.ends_with('[')
3465 || trimmed.ends_with('{');
3466 if !cont {
3467 *raw.lsloc.get_or_insert(0) += 1;
3468 }
3469 }
3470 LslocStrategy::Unsupported => {}
3471 }
3472}
3473
3474#[allow(clippy::needless_pass_by_value)]
3475fn analyze_generic(text: &str, config: ScanConfig, ieee: IeeeFlags) -> RawFileAnalysis {
3476 let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
3477 let lines: Vec<&str> = normalized.split_terminator('\n').collect();
3478
3479 let mut raw = RawLineCounts::default();
3480 let mut warnings = Vec::new();
3481
3482 let mut in_block_comment = false;
3483 let mut string_state: Option<StringState> = None;
3484 let mut pending_continuation: Option<LineFacts> = None;
3486 let mut scope = CScopeState::default();
3488
3489 for (line_idx, line) in lines.iter().enumerate() {
3490 process_physical_line(
3491 line,
3492 line_idx,
3493 &config,
3494 &mut raw,
3495 &mut in_block_comment,
3496 &mut string_state,
3497 &mut pending_continuation,
3498 ieee,
3499 &mut scope,
3500 );
3501 }
3502
3503 if let Some(pending) = pending_continuation.take() {
3505 classify_line(&mut raw, &pending, "");
3506 }
3507
3508 if in_block_comment {
3509 warnings.push("unclosed block comment detected; result is best effort".into());
3510 }
3511 if string_state.is_some() {
3512 warnings.push("unclosed string literal detected; result is best effort".into());
3513 }
3514
3515 RawFileAnalysis {
3516 raw,
3517 parse_mode: if warnings.is_empty() {
3518 ParseMode::Lexical
3519 } else {
3520 ParseMode::LexicalBestEffort
3521 },
3522 warnings,
3523 style_analysis: None,
3524 }
3525}
3526
3527const fn classify_line(raw: &mut RawLineCounts, facts: &LineFacts, trimmed: &str) {
3528 if facts.has_docstring {
3529 raw.docstring_comment_lines += 1;
3530 } else if !facts.has_code
3531 && !facts.has_single_comment
3532 && !facts.has_multi_comment
3533 && trimmed.is_empty()
3534 {
3535 raw.blank_only_lines += 1;
3536 } else if facts.has_code && facts.has_single_comment {
3537 raw.mixed_code_single_comment_lines += 1;
3538 } else if facts.has_code && facts.has_multi_comment {
3539 raw.mixed_code_multi_comment_lines += 1;
3540 } else if facts.has_code {
3541 raw.code_only_lines += 1;
3542 } else if facts.has_single_comment {
3543 raw.single_comment_only_lines += 1;
3544 } else if facts.has_multi_comment {
3545 raw.multi_comment_only_lines += 1;
3546 } else if trimmed.is_empty() {
3547 raw.blank_only_lines += 1;
3548 } else {
3549 raw.skipped_unknown_lines += 1;
3550 }
3551}
3552
3553fn prefix_hit(pats: &[&str], trimmed: &str) -> u64 {
3555 u64::from(pats.iter().any(|p| trimmed.starts_with(p)))
3556}
3557
3558fn fn_prefix_paren_hit(patterns: &SymbolPatterns, trimmed: &str) -> u64 {
3561 if patterns.functions_prefix_paren.is_empty() {
3562 return 0;
3563 }
3564 let Some(paren_pos) = trimmed.find('(') else {
3565 return 0;
3566 };
3567 if trimmed[..paren_pos].contains('=') {
3568 0
3569 } else {
3570 prefix_hit(patterns.functions_prefix_paren, trimmed)
3571 }
3572}
3573
3574fn var_prefix_no_paren_hit(patterns: &SymbolPatterns, trimmed: &str) -> u64 {
3577 if patterns.variables_prefix_no_paren.is_empty()
3578 || prefix_hit(patterns.variables_prefix_no_paren, trimmed) == 0
3579 {
3580 return 0;
3581 }
3582 trimmed
3583 .find('(')
3584 .map_or(1, |pp| u64::from(trimmed[..pp].contains('=')))
3585}
3586
3587const C_STMT_KEYWORDS: &[&str] = &[
3590 "if",
3591 "for",
3592 "while",
3593 "switch",
3594 "return",
3595 "catch",
3596 "sizeof",
3597 "do",
3598 "else",
3599 "case",
3600 "throw",
3601 "goto",
3602 "using",
3603 "namespace",
3604 "typedef",
3605 "friend",
3606 "decltype",
3607 "alignof",
3608 "new",
3609 "delete",
3610 "static_assert",
3611 "template",
3612 "co_await",
3613 "co_return",
3614 "co_yield",
3615 "assert",
3616 "default",
3617 "class",
3618 "struct",
3619 "union",
3620 "enum",
3621 "public",
3622 "private",
3623 "protected",
3624 "try",
3625];
3626
3627const fn c_type_char_ok(c: char) -> bool {
3630 c.is_ascii_alphanumeric()
3631 || matches!(
3632 c,
3633 '_' | ':' | '<' | '>' | '*' | '&' | '~' | ' ' | '\t' | ','
3634 )
3635}
3636
3637fn c_name_is_identifier(name: &str) -> bool {
3640 let core = name.trim_start_matches(['*', '&', '~']);
3641 let seg = core.rsplit("::").next().unwrap_or(core);
3642 let mut chars = seg.chars();
3643 match chars.next() {
3644 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
3645 _ => return false,
3646 }
3647 seg.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
3648}
3649
3650fn looks_like_c_function(trimmed: &str) -> u64 {
3658 let Some(paren) = trimmed.find('(') else {
3659 return 0;
3660 };
3661 let pre = trimmed[..paren].trim();
3662 if pre.is_empty() || pre.contains("->") || !pre.chars().all(c_type_char_ok) {
3666 return 0;
3667 }
3668 let mut toks = pre.split_whitespace();
3670 let Some(first) = toks.next() else {
3671 return 0;
3672 };
3673 if C_STMT_KEYWORDS.contains(&first) || toks.next().is_none() {
3674 return 0;
3675 }
3676 let Some(name) = pre.split_whitespace().next_back() else {
3677 return 0;
3678 };
3679 if !c_name_is_identifier(name) {
3680 return 0;
3681 }
3682 if trimmed.ends_with(';') {
3687 let args = trimmed[paren + 1..]
3688 .rsplit_once(')')
3689 .map_or("", |(a, _)| a)
3690 .trim();
3691 let looks_like_params = args.is_empty()
3692 || args.contains(',')
3693 || args.contains('&')
3694 || args.contains('*')
3695 || args.contains("::")
3696 || args.split_whitespace().count() >= 2;
3697 if !looks_like_params {
3698 return 0;
3699 }
3700 }
3701 1
3702}
3703
3704fn looks_like_c_variable(trimmed: &str) -> u64 {
3710 let Some(delim_pos) = trimmed.find(['=', ';', '{', '(']) else {
3715 return 0;
3716 };
3717 let head = trimmed[..delim_pos].trim();
3718 if head.is_empty() || head.contains("->") || !head.chars().all(c_type_char_ok) {
3719 return 0;
3720 }
3721 let mut toks = head.split_whitespace();
3722 let Some(first) = toks.next() else {
3723 return 0;
3724 };
3725 if C_STMT_KEYWORDS.contains(&first) || toks.next().is_none() {
3726 return 0;
3727 }
3728 let Some(name) = head.split_whitespace().next_back() else {
3729 return 0;
3730 };
3731 u64::from(c_name_is_identifier(name))
3732}
3733
3734#[derive(Clone, Copy, PartialEq, Eq)]
3737enum CScope {
3738 Aggregate,
3740 Function,
3742 Namespace,
3744 Block,
3746}
3747
3748#[derive(Clone, Copy)]
3750enum VarKind {
3751 Member,
3752 Local,
3753 Global,
3754}
3755
3756#[derive(Default)]
3758struct CScopeState {
3759 stack: Vec<CScope>,
3760 pending: Option<CScope>,
3763}
3764
3765impl CScopeState {
3766 fn current_var_kind(&self) -> VarKind {
3768 match self.stack.last() {
3769 Some(CScope::Aggregate) => VarKind::Member,
3770 Some(CScope::Function | CScope::Block) => VarKind::Local,
3771 _ => VarKind::Global, }
3773 }
3774
3775 fn update(&mut self, trimmed: &str) {
3778 if let Some(kind) = c_line_scope_kind(trimmed) {
3779 self.pending = Some(kind);
3780 }
3781 let bytes = trimmed.as_bytes();
3782 let mut i = 0;
3783 let mut in_str: Option<u8> = None;
3784 while i < bytes.len() {
3785 if let Some(next) = skip_string_literal(bytes, i, &mut in_str) {
3787 i = next;
3788 continue;
3789 }
3790 let b = bytes[i];
3791 match b {
3792 b'"' | b'\'' => in_str = Some(b),
3793 b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'/' => break, b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'*' => {
3795 match trimmed[i + 2..].find("*/") {
3797 Some(off) => i += 2 + off + 2,
3798 None => break,
3799 }
3800 continue;
3801 }
3802 b'{' => self
3803 .stack
3804 .push(self.pending.take().unwrap_or(CScope::Block)),
3805 b'}' => {
3806 self.stack.pop();
3807 }
3808 _ => {}
3809 }
3810 i += 1;
3811 }
3812 }
3813}
3814
3815fn skip_string_literal(bytes: &[u8], i: usize, in_str: &mut Option<u8>) -> Option<usize> {
3819 let q = (*in_str)?;
3820 let b = bytes[i];
3821 if b == b'\\' {
3822 return Some(i + 2); }
3824 if b == q {
3825 *in_str = None;
3826 }
3827 Some(i + 1)
3828}
3829
3830fn c_line_scope_kind(trimmed: &str) -> Option<CScope> {
3834 if trimmed.contains(';') && !trimmed.contains('{') {
3836 return None;
3837 }
3838 if trimmed.starts_with("namespace") {
3839 return Some(CScope::Namespace);
3840 }
3841 if is_c_aggregate_opener(trimmed) {
3842 return Some(CScope::Aggregate);
3843 }
3844 if looks_like_c_function(trimmed) == 1 {
3845 return Some(CScope::Function);
3846 }
3847 None
3848}
3849
3850fn is_c_aggregate_opener(trimmed: &str) -> bool {
3853 if trimmed.contains('(') {
3854 return false;
3855 }
3856 trimmed
3857 .split(|c: char| !c.is_ascii_alphanumeric() && c != '_')
3858 .any(|tok| matches!(tok, "class" | "struct" | "union"))
3859}
3860
3861fn is_object_like_macro(trimmed: &str) -> bool {
3864 let Some(rest) = trimmed.strip_prefix('#') else {
3865 return false;
3866 };
3867 let Some(rest) = rest.trim_start().strip_prefix("define") else {
3868 return false;
3869 };
3870 if !rest.starts_with(char::is_whitespace) {
3871 return false; }
3873 let rest = rest.trim_start();
3874 let name_end = rest
3875 .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
3876 .unwrap_or(rest.len());
3877 if name_end == 0 {
3878 return false; }
3880 let after = &rest[name_end..];
3881 !after.starts_with('(') && !after.trim().is_empty()
3884}
3885
3886fn count_symbols(patterns: &SymbolPatterns, trimmed: &str) -> (u64, u64, u64, u64, u64, u64, u64) {
3887 let hit = |pats: &[&str]| prefix_hit(pats, trimmed);
3888 let c_style = !patterns.functions_prefix_paren.is_empty();
3896 let fn_extra = if c_style {
3897 looks_like_c_function(trimmed)
3898 } else {
3899 fn_prefix_paren_hit(patterns, trimmed)
3900 };
3901 let test_hit = hit(patterns.tests);
3902 let fn_hit = if test_hit == 0 {
3909 hit(patterns.functions) | fn_extra
3910 } else {
3911 0
3912 };
3913 let class_hit = if test_hit == 0 {
3914 hit(patterns.classes)
3915 } else {
3916 0
3917 };
3918 let var_hit = if c_style {
3919 if test_hit == 0 && fn_hit == 0 && class_hit == 0 {
3922 looks_like_c_variable(trimmed)
3923 } else {
3924 0
3925 }
3926 } else {
3927 hit(patterns.variables) | var_prefix_no_paren_hit(patterns, trimmed)
3928 };
3929 (
3930 fn_hit,
3931 class_hit,
3932 var_hit,
3933 hit(patterns.imports),
3934 test_hit,
3935 hit(patterns.assertions),
3936 hit(patterns.test_suites),
3937 )
3938}
3939
3940fn is_word_boundary(line: &[u8], start: usize, end: usize) -> bool {
3942 let before_ok =
3943 start == 0 || (!line[start - 1].is_ascii_alphanumeric() && line[start - 1] != b'_');
3944 let after_ok = end >= line.len() || (!line[end].is_ascii_alphanumeric() && line[end] != b'_');
3945 before_ok && after_ok
3946}
3947
3948fn keyword_matches_at(line: &[u8], i: usize, kw_bytes: &[u8], word_kw: bool) -> bool {
3950 if &line[i..i + kw_bytes.len()] != kw_bytes {
3951 return false;
3952 }
3953 !word_kw || is_word_boundary(line, i, i + kw_bytes.len())
3954}
3955
3956fn count_branch_in_line(line: &[u8], keywords: &[&str]) -> u32 {
3961 if keywords.is_empty() || line.is_empty() {
3962 return 0;
3963 }
3964 let mut total = 0u32;
3965 for &kw in keywords {
3966 let kw_bytes = kw.as_bytes();
3967 let word_kw = kw.bytes().all(|b| b.is_ascii_alphabetic() || b == b'_');
3968 let mut i = 0usize;
3969 while i + kw_bytes.len() <= line.len() {
3970 if keyword_matches_at(line, i, kw_bytes, word_kw) {
3971 total += 1;
3972 i += kw_bytes.len();
3973 } else {
3974 i += 1;
3975 }
3976 }
3977 }
3978 total
3979}
3980
3981fn starts_with(chars: &[char], index: usize, needle: &str) -> bool {
3982 let needle_chars: Vec<char> = needle.chars().collect();
3983 chars.get(index..index + needle_chars.len()) == Some(needle_chars.as_slice())
3984}
3985
3986#[derive(Debug, Clone)]
3987struct PyContext {
3988 indent: usize,
3989 expect_docstring: bool,
3990}
3991
3992fn py_pop_outdented_contexts(contexts: &mut Vec<PyContext>, indent: usize) {
3994 while contexts.len() > 1 && indent < contexts.last().map_or(0, |c| c.indent) {
3995 contexts.pop();
3996 }
3997}
3998
3999fn py_handle_pending_indent(
4002 pending_block_indent: &mut Option<usize>,
4003 contexts: &mut Vec<PyContext>,
4004 indent: usize,
4005 trimmed: &str,
4006) {
4007 let Some(base_indent) = *pending_block_indent else {
4008 return;
4009 };
4010 if indent > base_indent {
4011 contexts.push(PyContext {
4012 indent,
4013 expect_docstring: true,
4014 });
4015 *pending_block_indent = None;
4016 } else if !trimmed.starts_with('@') {
4017 *pending_block_indent = None;
4018 }
4019}
4020
4021fn py_try_record_docstring(
4027 ctx: &mut PyContext,
4028 trimmed: &str,
4029 idx: usize,
4030 docstring_lines: &mut HashSet<usize>,
4031 active_docstring: &mut Option<(&'static str, usize)>,
4032) -> bool {
4033 if !ctx.expect_docstring {
4034 return false;
4035 }
4036 if let Some(delim) = docstring_delimiter(trimmed) {
4037 docstring_lines.insert(idx);
4038 ctx.expect_docstring = false;
4039 if !closes_triple_docstring(trimmed, delim, true) {
4040 *active_docstring = Some((delim, idx));
4041 }
4042 return true;
4043 }
4044 ctx.expect_docstring = false;
4045 false
4046}
4047
4048fn track_active_docstring(
4052 active_docstring: &mut Option<(&'static str, usize)>,
4053 docstring_lines: &mut HashSet<usize>,
4054 idx: usize,
4055 trimmed: &str,
4056) -> bool {
4057 let Some((delim, start_line)) = *active_docstring else {
4058 return false;
4059 };
4060 docstring_lines.insert(idx);
4061 if closes_triple_docstring(trimmed, delim, idx == start_line) {
4062 *active_docstring = None;
4063 }
4064 true
4065}
4066
4067fn try_record_docstring_if_context(
4070 contexts: &mut [PyContext],
4071 trimmed: &str,
4072 idx: usize,
4073 docstring_lines: &mut HashSet<usize>,
4074 active_docstring: &mut Option<(&'static str, usize)>,
4075) -> bool {
4076 let Some(ctx) = contexts.last_mut() else {
4077 return false;
4078 };
4079 py_try_record_docstring(ctx, trimmed, idx, docstring_lines, active_docstring)
4080}
4081
4082fn mark_unclosed_docstring_lines(
4084 active_docstring: Option<&(&'static str, usize)>,
4085 docstring_lines: &mut HashSet<usize>,
4086 num_lines: usize,
4087) {
4088 if let Some(&(_, start_line)) = active_docstring {
4089 for idx in start_line..num_lines {
4090 docstring_lines.insert(idx);
4091 }
4092 }
4093}
4094
4095fn detect_python_docstring_lines(text: &str) -> HashSet<usize> {
4096 let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
4097 let lines: Vec<&str> = normalized.split_terminator('\n').collect();
4098
4099 let mut docstring_lines = HashSet::new();
4100 let mut contexts = vec![PyContext {
4101 indent: 0,
4102 expect_docstring: true,
4103 }];
4104 let mut pending_block_indent: Option<usize> = None;
4105 let mut active_docstring: Option<(&'static str, usize)> = None;
4106
4107 for (idx, line) in lines.iter().enumerate() {
4108 let trimmed = line.trim();
4109 let indent = leading_indent(line);
4110
4111 if track_active_docstring(&mut active_docstring, &mut docstring_lines, idx, trimmed) {
4112 continue;
4113 }
4114
4115 if trimmed.is_empty() || trimmed.starts_with('#') {
4117 continue;
4118 }
4119
4120 py_pop_outdented_contexts(&mut contexts, indent);
4121 py_handle_pending_indent(&mut pending_block_indent, &mut contexts, indent, trimmed);
4122
4123 if try_record_docstring_if_context(
4124 &mut contexts,
4125 trimmed,
4126 idx,
4127 &mut docstring_lines,
4128 &mut active_docstring,
4129 ) {
4130 continue;
4131 }
4132
4133 if is_python_block_header(trimmed) {
4134 pending_block_indent = Some(indent);
4135 }
4136 }
4137
4138 mark_unclosed_docstring_lines(active_docstring.as_ref(), &mut docstring_lines, lines.len());
4139
4140 docstring_lines
4141}
4142
4143fn leading_indent(line: &str) -> usize {
4144 line.chars().take_while(|c| c.is_whitespace()).count()
4145}
4146
4147fn is_python_block_header(trimmed: &str) -> bool {
4148 (trimmed.starts_with("def ")
4149 || trimmed.starts_with("async def ")
4150 || trimmed.starts_with("class "))
4151 && trimmed.ends_with(':')
4152}
4153
4154fn docstring_delimiter(trimmed: &str) -> Option<&'static str> {
4155 let mut idx = 0usize;
4156 let bytes = trimmed.as_bytes();
4157 while idx < bytes.len() {
4158 let c = bytes[idx] as char;
4159 if matches!(c, 'r' | 'R' | 'u' | 'U' | 'b' | 'B' | 'f' | 'F') {
4160 idx += 1;
4161 continue;
4162 }
4163 break;
4164 }
4165
4166 let rest = &trimmed[idx..];
4167 if rest.starts_with("\"\"\"") {
4168 Some("\"\"\"")
4169 } else if rest.starts_with("'''") {
4170 Some("'''")
4171 } else {
4172 None
4173 }
4174}
4175
4176fn closes_triple_docstring(trimmed: &str, delim: &str, same_line_as_start: bool) -> bool {
4177 let mut occurrences = 0usize;
4178 let mut search = trimmed;
4179 while let Some(index) = search.find(delim) {
4180 occurrences += 1;
4181 search = &search[index + delim.len()..];
4182 }
4183
4184 if same_line_as_start {
4185 occurrences >= 2
4186 } else {
4187 occurrences >= 1
4188 }
4189}
4190
4191#[cfg(feature = "tree-sitter")]
4196pub mod ts {
4197 use tree_sitter::Node;
4198
4199 use super::{ParseMode, RawFileAnalysis, RawLineCounts};
4200
4201 struct SymbolKinds {
4203 function_def: &'static str,
4205 class_def: &'static str,
4207 test_fn_prefix: &'static str,
4210 test_class_prefix: &'static str,
4213 assertion_attr_prefix: &'static str,
4217 }
4218
4219 impl SymbolKinds {
4220 const fn none() -> Self {
4221 Self {
4222 function_def: "",
4223 class_def: "",
4224 test_fn_prefix: "",
4225 test_class_prefix: "",
4226 assertion_attr_prefix: "",
4227 }
4228 }
4229 }
4230
4231 fn analyze_lines(
4237 text: &str,
4238 ts_language: &tree_sitter::Language,
4239 comment_node_kinds: &[&str],
4240 docstring_stmt_kind: Option<&str>,
4241 symbols: &SymbolKinds,
4242 ) -> Option<RawFileAnalysis> {
4243 let mut parser = tree_sitter::Parser::new();
4244 parser.set_language(ts_language).ok()?;
4245 let tree = parser.parse(text, None)?;
4246
4247 let lines: Vec<&str> = text.split_terminator('\n').collect();
4248 let n = lines.len();
4249
4250 let mut has_code = vec![false; n];
4251 let mut has_comment = vec![false; n];
4252 let mut comment_is_block = vec![false; n];
4253 let mut has_docstring = vec![false; n];
4254
4255 let mut ctx = VisitCtx {
4257 source: text.as_bytes(),
4258 comment_kinds: comment_node_kinds,
4259 docstring_stmt_kind,
4260 has_code: &mut has_code,
4261 has_comment: &mut has_comment,
4262 comment_is_block: &mut comment_is_block,
4263 has_docstring: &mut has_docstring,
4264 };
4265 visit(tree.root_node(), &mut ctx);
4266
4267 let mut raw = RawLineCounts::default();
4268 classify_ts_lines(
4269 &lines,
4270 &has_code,
4271 &has_comment,
4272 &comment_is_block,
4273 &has_docstring,
4274 &mut raw,
4275 );
4276
4277 if !symbols.function_def.is_empty() || !symbols.class_def.is_empty() {
4279 count_symbols(tree.root_node(), text.as_bytes(), symbols, &mut raw);
4280 }
4281
4282 Some(RawFileAnalysis {
4283 raw,
4284 parse_mode: ParseMode::TreeSitter,
4285 warnings: Vec::new(),
4286 style_analysis: None,
4287 })
4288 }
4289
4290 fn recurse_children(node: Node, source: &[u8], kinds: &SymbolKinds, raw: &mut RawLineCounts) {
4292 for i in 0..node.child_count() {
4293 #[allow(clippy::cast_possible_truncation)]
4294 if let Some(child) = node.child(i as u32) {
4295 count_symbols(child, source, kinds, raw);
4296 }
4297 }
4298 }
4299
4300 fn try_count_function(
4302 node: Node,
4303 source: &[u8],
4304 kinds: &SymbolKinds,
4305 raw: &mut RawLineCounts,
4306 ) -> bool {
4307 if kinds.function_def.is_empty() || node.kind() != kinds.function_def {
4308 return false;
4309 }
4310 let name = node
4311 .child_by_field_name("name")
4312 .and_then(|n| n.utf8_text(source).ok())
4313 .unwrap_or("");
4314 if !kinds.test_fn_prefix.is_empty() && name.starts_with(kinds.test_fn_prefix) {
4315 raw.test_count += 1;
4316 } else {
4317 raw.functions += 1;
4318 }
4319 recurse_children(node, source, kinds, raw);
4320 true
4321 }
4322
4323 fn try_count_class(
4325 node: Node,
4326 source: &[u8],
4327 kinds: &SymbolKinds,
4328 raw: &mut RawLineCounts,
4329 ) -> bool {
4330 if kinds.class_def.is_empty() || node.kind() != kinds.class_def {
4331 return false;
4332 }
4333 let name = node
4334 .child_by_field_name("name")
4335 .and_then(|n| n.utf8_text(source).ok())
4336 .unwrap_or("");
4337 if !kinds.test_class_prefix.is_empty() && name.starts_with(kinds.test_class_prefix) {
4338 raw.test_count += 1;
4339 } else {
4340 raw.classes += 1;
4341 }
4342 recurse_children(node, source, kinds, raw);
4343 true
4344 }
4345
4346 fn try_count_assertion(
4349 node: Node,
4350 source: &[u8],
4351 kinds: &SymbolKinds,
4352 raw: &mut RawLineCounts,
4353 ) -> bool {
4354 if kinds.assertion_attr_prefix.is_empty() || node.kind() != "call" {
4355 return false;
4356 }
4357 let Some(func) = node.child_by_field_name("function") else {
4358 return false;
4359 };
4360 if func.kind() != "attribute" {
4361 return false;
4362 }
4363 let attr_text = func
4364 .child_by_field_name("attribute")
4365 .and_then(|n| n.utf8_text(source).ok())
4366 .unwrap_or("");
4367 if !attr_text.starts_with(kinds.assertion_attr_prefix) {
4368 return false;
4369 }
4370 raw.test_assertion_count += 1;
4371 true
4372 }
4373
4374 fn count_symbols(node: Node, source: &[u8], kinds: &SymbolKinds, raw: &mut RawLineCounts) {
4377 if try_count_function(node, source, kinds, raw) {
4378 return;
4379 }
4380 if try_count_class(node, source, kinds, raw) {
4381 return;
4382 }
4383 if try_count_assertion(node, source, kinds, raw) {
4384 return;
4385 }
4386 recurse_children(node, source, kinds, raw);
4387 }
4388
4389 #[allow(clippy::struct_excessive_bools)]
4392 #[derive(Clone, Copy)]
4393 struct TsLineFlags {
4394 has_code: bool,
4395 has_comment: bool,
4396 comment_is_block: bool,
4397 has_docstring: bool,
4398 }
4399
4400 const fn classify_ts_line(trimmed: &str, flags: TsLineFlags, raw: &mut RawLineCounts) {
4402 if trimmed.is_empty() {
4403 raw.blank_only_lines += 1;
4404 } else if flags.has_docstring && !flags.has_code {
4405 raw.docstring_comment_lines += 1;
4406 } else if flags.has_code && flags.has_comment {
4407 if flags.comment_is_block {
4409 raw.mixed_code_multi_comment_lines += 1;
4410 } else {
4411 raw.mixed_code_single_comment_lines += 1;
4412 }
4413 } else if flags.has_comment {
4414 if flags.comment_is_block {
4415 raw.multi_comment_only_lines += 1;
4416 } else {
4417 raw.single_comment_only_lines += 1;
4418 }
4419 } else {
4420 raw.code_only_lines += 1;
4421 }
4422 }
4423
4424 fn classify_ts_lines(
4426 lines: &[&str],
4427 has_code: &[bool],
4428 has_comment: &[bool],
4429 comment_is_block: &[bool],
4430 has_docstring: &[bool],
4431 raw: &mut RawLineCounts,
4432 ) {
4433 for i in 0..lines.len() {
4434 raw.total_physical_lines += 1;
4435 classify_ts_line(
4436 lines[i].trim(),
4437 TsLineFlags {
4438 has_code: has_code[i],
4439 has_comment: has_comment[i],
4440 comment_is_block: comment_is_block[i],
4441 has_docstring: has_docstring[i],
4442 },
4443 raw,
4444 );
4445 }
4446 }
4447
4448 struct VisitCtx<'a> {
4449 source: &'a [u8],
4450 comment_kinds: &'a [&'a str],
4451 docstring_stmt_kind: Option<&'a str>,
4452 has_code: &'a mut Vec<bool>,
4453 has_comment: &'a mut Vec<bool>,
4454 comment_is_block: &'a mut Vec<bool>,
4455 has_docstring: &'a mut Vec<bool>,
4456 }
4457
4458 fn visit_comment_node(node: Node, ctx: &mut VisitCtx<'_>) {
4460 let start_row = node.start_position().row;
4461 let end_row = node.end_position().row;
4462 let first_two = node
4463 .utf8_text(ctx.source)
4464 .unwrap_or("")
4465 .get(..2)
4466 .unwrap_or("");
4467 let is_block = first_two == "/*" || first_two == "<#";
4468 for row in start_row..=end_row {
4469 if row < ctx.has_comment.len() {
4470 ctx.has_comment[row] = true;
4471 if is_block {
4472 ctx.comment_is_block[row] = true;
4473 }
4474 }
4475 }
4476 }
4477
4478 fn visit_maybe_docstring(node: Node, kind: &str, ctx: &mut VisitCtx<'_>) -> bool {
4481 let Some(stmt_kind) = ctx.docstring_stmt_kind else {
4482 return false;
4483 };
4484 if kind != stmt_kind || node.named_child_count() != 1 {
4485 return false;
4486 }
4487 let Some(child) = node.named_child(0) else {
4488 return false;
4489 };
4490 if child.kind() != "string" {
4491 return false;
4492 }
4493 let child_start = child.start_position().row;
4494 let child_end = child.end_position().row;
4495 for row in child_start..=child_end {
4496 if row < ctx.has_docstring.len() {
4497 ctx.has_docstring[row] = true;
4498 }
4499 }
4500 true
4501 }
4502
4503 fn visit_leaf_code(node: Node, ctx: &mut VisitCtx<'_>) {
4505 let start_row = node.start_position().row;
4506 let end_row = node.end_position().row;
4507 for row in start_row..=end_row {
4508 if row < ctx.has_code.len() {
4509 ctx.has_code[row] = true;
4510 }
4511 }
4512 }
4513
4514 #[allow(clippy::too_many_lines)]
4515 fn visit(node: Node, ctx: &mut VisitCtx<'_>) {
4516 let kind = node.kind();
4517
4518 if ctx.comment_kinds.contains(&kind) {
4520 visit_comment_node(node, ctx);
4521 return;
4522 }
4523
4524 if visit_maybe_docstring(node, kind, ctx) {
4526 return;
4527 }
4528
4529 if node.child_count() == 0 && !node.is_extra() {
4531 visit_leaf_code(node, ctx);
4532 return;
4533 }
4534
4535 for i in 0..node.child_count() {
4536 #[allow(clippy::cast_possible_truncation)]
4537 if let Some(child) = node.child(i as u32) {
4539 visit(child, ctx);
4540 }
4541 }
4542 }
4543
4544 const C_SYMBOLS: SymbolKinds = SymbolKinds::none();
4545
4546 const PYTHON_SYMBOLS: SymbolKinds = SymbolKinds {
4547 function_def: "function_definition",
4548 class_def: "class_definition",
4549 test_fn_prefix: "test_",
4550 test_class_prefix: "Test",
4551 assertion_attr_prefix: "assert",
4552 };
4553
4554 #[must_use]
4556 pub fn analyze_c(text: &str) -> Option<RawFileAnalysis> {
4557 let lang: tree_sitter::Language = tree_sitter_c::LANGUAGE.into();
4558 analyze_lines(text, &lang, &["comment"], None, &C_SYMBOLS)
4559 }
4560
4561 #[must_use]
4563 pub fn analyze_python(text: &str) -> Option<RawFileAnalysis> {
4564 let lang: tree_sitter::Language = tree_sitter_python::LANGUAGE.into();
4565 analyze_lines(
4566 text,
4567 &lang,
4568 &["comment"],
4569 Some("expression_statement"),
4570 &PYTHON_SYMBOLS,
4571 )
4572 }
4573}
4574
4575#[cfg(test)]
4576mod tests {
4577 use super::*;
4578
4579 #[test]
4580 fn python_docstrings_are_separated() {
4581 let input = r#""""module docs"""
4582
4583
4584def fn_a():
4585 """function docs"""
4586 value = 1 # trailing comment
4587 return value
4588"#;
4589
4590 let result = analyze_text(Language::Python, input, AnalysisOptions::default());
4591 assert_eq!(result.raw.docstring_comment_lines, 2);
4592 assert_eq!(result.raw.mixed_code_single_comment_lines, 1);
4593 assert_eq!(result.raw.code_only_lines, 2);
4594 }
4595
4596 #[test]
4597 fn c_style_mixed_lines_are_captured() {
4598 let input = "int x = 1; // note\n/* block */\n";
4599 let result = analyze_text(Language::C, input, AnalysisOptions::default());
4600 assert_eq!(result.raw.mixed_code_single_comment_lines, 1);
4601 assert_eq!(result.raw.multi_comment_only_lines, 1);
4602 }
4603
4604 #[test]
4605 fn branch_keywords_inside_strings_are_not_counted() {
4606 let s = analyze_text(
4608 Language::Rust,
4609 "let s = \"if a && b || c ? d : e\";\n",
4610 AnalysisOptions::default(),
4611 );
4612 assert_eq!(s.raw.cyclomatic_complexity, 0);
4613
4614 let raw = analyze_text(
4617 Language::Rust,
4618 "let h = r#\"<a href=\"x\">a && b ? c : d</a>\"#;\n",
4619 AnalysisOptions::default(),
4620 );
4621 assert_eq!(raw.raw.cyclomatic_complexity, 0);
4622
4623 let code = analyze_text(
4625 Language::Rust,
4626 "if a && b { c } else { d }\n",
4627 AnalysisOptions::default(),
4628 );
4629 assert!(code.raw.cyclomatic_complexity >= 2);
4630 }
4631
4632 #[test]
4633 fn multiline_raw_string_does_not_swallow_following_code() {
4634 let input = concat!(
4637 "let cfg = r##\"\n",
4638 "# looks like a comment but is string content\n",
4639 "key = \"value with \"\" inner quotes\"\n",
4640 "\"##;\n",
4641 "let x = 1;\n",
4642 );
4643 let r = analyze_text(Language::Rust, input, AnalysisOptions::default());
4644 assert!(
4645 r.raw.code_only_lines >= 1,
4646 "code after the raw string was swallowed"
4647 );
4648 assert_eq!(r.raw.single_comment_only_lines, 0);
4649 assert_eq!(r.raw.cyclomatic_complexity, 0);
4650 }
4651
4652 #[test]
4653 fn detect_language_by_shebang() {
4654 let language = detect_language(
4655 Path::new("script"),
4656 Some("#!/usr/bin/env bash"),
4657 &BTreeMap::new(),
4658 true,
4659 );
4660 assert_eq!(language, Some(Language::Shell));
4661 }
4662
4663 fn sym(lang: Language, line: &str) -> (u64, u64, u64, u64, u64, u64, u64) {
4666 let result = analyze_text(lang, &format!("{line}\n"), AnalysisOptions::default());
4667 let r = &result.raw;
4668 (
4669 r.functions,
4670 r.classes,
4671 r.variables,
4672 r.imports,
4673 r.test_count,
4674 r.test_assertion_count,
4675 r.test_suite_count,
4676 )
4677 }
4678
4679 #[test]
4680 fn python_test_fn_not_double_counted() {
4681 let (f, c, _, _, t, _, _) = sym(Language::Python, "def test_foo():");
4683 assert_eq!(f, 0, "test fn must not also increment functions");
4684 assert_eq!(t, 1, "must be counted as a test");
4685 assert_eq!(c, 0);
4686 }
4687
4688 #[test]
4689 fn python_test_class_not_double_counted() {
4690 let (f, c, _, _, t, _, _) = sym(Language::Python, "class TestFoo:");
4692 assert_eq!(c, 0, "test class must not also increment classes");
4693 assert_eq!(t, 1, "must be counted as a test");
4694 assert_eq!(f, 0);
4695 }
4696
4697 #[test]
4698 fn python_regular_fn_counts_as_function() {
4699 let (f, c, _, _, t, _, _) = sym(Language::Python, "def regular():");
4700 assert_eq!(f, 1, "regular function must be counted");
4701 assert_eq!(t, 0);
4702 assert_eq!(c, 0);
4703 }
4704
4705 #[test]
4706 fn python_regular_class_counts_as_class() {
4707 let (f, c, _, _, t, _, _) = sym(Language::Python, "class Regular:");
4708 assert_eq!(c, 1, "regular class must be counted");
4709 assert_eq!(t, 0);
4710 assert_eq!(f, 0);
4711 }
4712
4713 #[test]
4714 fn go_test_fn_not_double_counted() {
4715 let (f, _, _, _, t, _, _) = sym(Language::Go, "func TestFoo(t *testing.T) {");
4716 assert_eq!(f, 0, "Go test func must not also increment functions");
4717 assert_eq!(t, 1, "must be counted as a test");
4718 }
4719
4720 #[test]
4721 fn go_benchmark_fn_not_double_counted() {
4722 let (f, _, _, _, t, _, _) = sym(Language::Go, "func BenchmarkBar(b *testing.B) {");
4723 assert_eq!(f, 0, "Go benchmark func must not also increment functions");
4724 assert_eq!(t, 1, "must be counted as a test");
4725 }
4726
4727 #[test]
4728 fn go_regular_fn_counts_as_function() {
4729 let (f, _, _, _, t, _, _) = sym(Language::Go, "func doSomething() {");
4730 assert_eq!(f, 1, "regular Go func must be counted");
4731 assert_eq!(t, 0);
4732 }
4733
4734 #[test]
4735 fn rust_test_attr_counts_as_test_not_function() {
4736 let (f, _, _, _, t, _, _) = sym(Language::Rust, "#[test]");
4738 assert_eq!(t, 1, "#[test] must be counted as a test");
4739 assert_eq!(f, 0, "#[test] attribute must not be counted as a function");
4740 }
4741
4742 #[test]
4743 fn rust_fn_line_counts_as_function_not_test() {
4744 let (f, _, _, _, t, _, _) = sym(Language::Rust, "fn test_something() {");
4746 assert_eq!(f, 1, "fn declaration must count as a function");
4747 assert_eq!(
4748 t, 0,
4749 "fn declaration line must not be double-counted as a test"
4750 );
4751 }
4752
4753 #[test]
4754 fn js_describe_counts_as_test_not_function() {
4755 let (f, _, _, _, t, _, _) = sym(Language::JavaScript, "describe('suite', () => {");
4756 assert_eq!(t, 1, "describe must be counted as a test");
4757 assert_eq!(f, 0, "describe must not be counted as a function");
4758 }
4759
4760 #[test]
4761 fn js_regular_fn_counts_as_function() {
4762 let (f, _, _, _, t, _, _) = sym(Language::JavaScript, "function doWork() {");
4763 assert_eq!(f, 1, "JS function declaration must be counted");
4764 assert_eq!(t, 0);
4765 }
4766
4767 use std::collections::BTreeMap;
4770 use std::path::Path;
4771
4772 #[test]
4773 fn detect_language_rs_extension() {
4774 let lang = detect_language(Path::new("foo.rs"), None, &BTreeMap::new(), false);
4775 assert_eq!(lang, Some(Language::Rust));
4776 }
4777
4778 #[test]
4779 fn detect_language_py_extension() {
4780 let lang = detect_language(Path::new("foo.py"), None, &BTreeMap::new(), false);
4781 assert_eq!(lang, Some(Language::Python));
4782 }
4783
4784 #[test]
4785 fn detect_language_ts_extension() {
4786 let lang = detect_language(Path::new("app.ts"), None, &BTreeMap::new(), false);
4787 assert_eq!(lang, Some(Language::TypeScript));
4788 }
4789
4790 #[test]
4791 fn detect_language_js_extension() {
4792 let lang = detect_language(Path::new("app.js"), None, &BTreeMap::new(), false);
4793 assert_eq!(lang, Some(Language::JavaScript));
4794 }
4795
4796 #[test]
4797 fn detect_language_go_extension() {
4798 let lang = detect_language(Path::new("main.go"), None, &BTreeMap::new(), false);
4799 assert_eq!(lang, Some(Language::Go));
4800 }
4801
4802 #[test]
4803 fn detect_language_c_extension() {
4804 let lang = detect_language(Path::new("main.c"), None, &BTreeMap::new(), false);
4805 assert_eq!(lang, Some(Language::C));
4806 }
4807
4808 #[test]
4809 fn detect_language_cpp_extension() {
4810 let lang = detect_language(Path::new("main.cpp"), None, &BTreeMap::new(), false);
4811 assert_eq!(lang, Some(Language::Cpp));
4812 }
4813
4814 #[test]
4815 fn detect_language_java_extension() {
4816 let lang = detect_language(Path::new("Main.java"), None, &BTreeMap::new(), false);
4817 assert_eq!(lang, Some(Language::Java));
4818 }
4819
4820 #[test]
4821 fn detect_language_makefile_exact_name() {
4822 let lang = detect_language(Path::new("Makefile"), None, &BTreeMap::new(), false);
4823 assert_eq!(lang, Some(Language::Makefile));
4824 }
4825
4826 #[test]
4827 fn detect_language_dockerfile_exact_name() {
4828 let lang = detect_language(Path::new("Dockerfile"), None, &BTreeMap::new(), false);
4829 assert_eq!(lang, Some(Language::Dockerfile));
4830 }
4831
4832 #[test]
4833 fn detect_language_rakefile() {
4834 let lang = detect_language(Path::new("Rakefile"), None, &BTreeMap::new(), false);
4835 assert_eq!(lang, Some(Language::Ruby));
4836 }
4837
4838 #[test]
4839 fn detect_language_gemfile() {
4840 let lang = detect_language(Path::new("Gemfile"), None, &BTreeMap::new(), false);
4841 assert_eq!(lang, Some(Language::Ruby));
4842 }
4843
4844 #[test]
4845 fn detect_language_unknown_extension_returns_none() {
4846 let lang = detect_language(Path::new("foo.xyz123"), None, &BTreeMap::new(), false);
4847 assert_eq!(lang, None);
4848 }
4849
4850 #[test]
4851 fn detect_language_extension_override() {
4852 let mut overrides = BTreeMap::new();
4853 overrides.insert("h".into(), "cpp".into());
4854 let lang = detect_language(Path::new("header.h"), None, &overrides, false);
4855 assert_eq!(lang, Some(Language::Cpp));
4856 }
4857
4858 #[test]
4859 fn detect_language_shebang_python() {
4860 let lang = detect_language(
4861 Path::new("script"),
4862 Some("#!/usr/bin/env python3"),
4863 &BTreeMap::new(),
4864 true,
4865 );
4866 assert_eq!(lang, Some(Language::Python));
4867 }
4868
4869 #[test]
4870 fn detect_language_shebang_bash() {
4871 let lang = detect_language(
4872 Path::new("script"),
4873 Some("#!/bin/bash"),
4874 &BTreeMap::new(),
4875 true,
4876 );
4877 assert_eq!(lang, Some(Language::Shell));
4878 }
4879
4880 #[test]
4881 fn detect_language_shebang_ruby() {
4882 let lang = detect_language(
4883 Path::new("script"),
4884 Some("#!/usr/bin/env ruby"),
4885 &BTreeMap::new(),
4886 true,
4887 );
4888 assert_eq!(lang, Some(Language::Ruby));
4889 }
4890
4891 #[test]
4892 fn detect_language_shebang_disabled() {
4893 let lang = detect_language(
4895 Path::new("script"),
4896 Some("#!/usr/bin/env python3"),
4897 &BTreeMap::new(),
4898 false,
4899 );
4900 assert_eq!(lang, None);
4901 }
4902
4903 #[test]
4904 fn from_name_rust() {
4905 assert_eq!(Language::from_name("rust"), Some(Language::Rust));
4906 }
4907
4908 #[test]
4909 fn from_name_python() {
4910 assert_eq!(Language::from_name("python"), Some(Language::Python));
4911 }
4912
4913 #[test]
4914 fn from_name_unknown() {
4915 assert_eq!(Language::from_name("brainfuck"), None);
4916 }
4917
4918 #[test]
4919 fn from_name_roundtrip_all() {
4920 for lang in [
4922 Language::C,
4923 Language::Cpp,
4924 Language::CSharp,
4925 Language::Go,
4926 Language::Java,
4927 Language::JavaScript,
4928 Language::Python,
4929 Language::Rust,
4930 Language::Shell,
4931 Language::PowerShell,
4932 Language::TypeScript,
4933 Language::Assembly,
4934 Language::Clojure,
4935 Language::Css,
4936 Language::Dart,
4937 Language::Dockerfile,
4938 Language::Elixir,
4939 Language::Erlang,
4940 Language::FSharp,
4941 Language::Groovy,
4942 Language::Haskell,
4943 Language::Html,
4944 Language::Julia,
4945 Language::Kotlin,
4946 Language::Lua,
4947 Language::Makefile,
4948 Language::Nim,
4949 Language::ObjectiveC,
4950 Language::Ocaml,
4951 Language::Perl,
4952 Language::Php,
4953 Language::R,
4954 Language::Ruby,
4955 Language::Scala,
4956 Language::Scss,
4957 Language::Sql,
4958 Language::Svelte,
4959 Language::Swift,
4960 Language::Vue,
4961 Language::Xml,
4962 Language::Zig,
4963 ] {
4964 let slug = lang.as_slug();
4965 let roundtripped = Language::from_name(slug);
4966 assert_eq!(
4967 roundtripped,
4968 Some(lang),
4969 "from_name({slug:?}) should return {lang:?}"
4970 );
4971 }
4972 }
4973
4974 #[test]
4977 fn blank_in_block_comment_defaults_to_comment() {
4978 let input = "/*\n\n*/";
4980 let opts = AnalysisOptions {
4981 blank_in_block_comment_as_comment: true,
4982 ..Default::default()
4983 };
4984 let result = analyze_text(Language::C, input, opts);
4985 assert_eq!(
4986 result.raw.multi_comment_only_lines, 3,
4987 "all 3 block-comment lines must count as multi-comment with CountAsComment policy"
4988 );
4989 assert_eq!(
4990 result.raw.blank_only_lines, 0,
4991 "no blank lines expected with CountAsComment policy"
4992 );
4993 }
4994
4995 #[test]
4996 fn blank_in_block_comment_counted_as_blank_when_policy_false() {
4997 let input = "/*\n\n*/";
4999 let opts = AnalysisOptions {
5000 blank_in_block_comment_as_comment: false,
5001 ..Default::default()
5002 };
5003 let result = analyze_text(Language::C, input, opts);
5004 assert_eq!(
5005 result.raw.multi_comment_only_lines, 2,
5006 "opener and closer must count as multi-comment with CountAsBlank policy"
5007 );
5008 assert_eq!(
5009 result.raw.blank_only_lines, 1,
5010 "the blank line inside the block comment must count as blank with CountAsBlank policy"
5011 );
5012 }
5013
5014 #[test]
5017 fn continuation_lines_each_physical_default() {
5018 let input = "#define FOO \\\n 1 \\\n + 2\n";
5020 let opts = AnalysisOptions {
5021 collapse_continuation_lines: false,
5022 ..Default::default()
5023 };
5024 let result = analyze_text(Language::C, input, opts);
5025 assert_eq!(
5026 result.raw.total_physical_lines, 3,
5027 "3 physical lines expected"
5028 );
5029 assert_eq!(
5030 result.raw.code_only_lines, 3,
5031 "each physical line must count as code with EachPhysicalLine policy"
5032 );
5033 }
5034
5035 #[test]
5036 fn continuation_lines_collapse_to_logical() {
5037 let input = "#define FOO \\\n 1 \\\n + 2\n";
5039 let opts = AnalysisOptions {
5040 collapse_continuation_lines: true,
5041 ..Default::default()
5042 };
5043 let result = analyze_text(Language::C, input, opts);
5044 assert_eq!(
5045 result.raw.total_physical_lines, 3,
5046 "physical line count is always 3 regardless of policy"
5047 );
5048 assert_eq!(
5049 result.raw.code_only_lines, 1,
5050 "3 continuation lines must collapse to 1 logical code line"
5051 );
5052 }
5053}