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 if let Some(override_name) = extension_overrides.get(ext.as_str()) {
714 if let Some(lang) = Language::from_name(override_name) {
715 return Some(lang);
716 }
717 }
718 }
719
720 let filename = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
722 let filename_lower = filename.to_ascii_lowercase();
723
724 if let Some(lang) = detect_by_filename(filename, &filename_lower) {
725 return Some(lang);
726 }
727
728 if let Some(lang) = extension.as_deref().and_then(detect_by_extension) {
730 return Some(lang);
731 }
732
733 if shebang_detection {
735 if let Some(line) = first_line {
736 if let Some(lang) = detect_by_shebang(line) {
737 return Some(lang);
738 }
739 }
740 }
741
742 None
743}
744
745#[must_use]
753pub fn looks_like_cpp(text: &str) -> bool {
754 const MARKERS: &[&str] = &[
755 "namespace ",
756 "template<",
757 "template <",
758 "class ",
759 "public:",
760 "private:",
761 "protected:",
762 "std::",
763 "::",
764 "nullptr",
765 "constexpr ",
766 "noexcept",
767 ];
768 MARKERS.iter().any(|m| text.contains(m))
769}
770
771#[cfg(feature = "tree-sitter")]
777fn tree_sitter_fast_path(
778 language: Language,
779 text: &str,
780 options: &AnalysisOptions,
781) -> Option<RawFileAnalysis> {
782 if options.blank_in_block_comment_as_comment || options.collapse_continuation_lines {
783 return None;
784 }
785 match language {
786 Language::C | Language::Cpp => {
787 let mut result = ts::analyze_c(text)?;
788 if options.enable_style && should_style_analyse(language, options.style_lang_scope) {
789 result.style_analysis = style::analyze_style(language, text);
790 }
791 Some(result)
792 }
793 Language::Python => ts::analyze_python(text),
794 _ => None,
795 }
796}
797
798#[must_use]
799pub fn analyze_text(language: Language, text: &str, options: AnalysisOptions) -> RawFileAnalysis {
800 #[cfg(feature = "tree-sitter")]
802 if let Some(result) = tree_sitter_fast_path(language, text, &options) {
803 return result;
804 }
805
806 let (mut config, has_preprocessor) = language_scan_config(language);
807
808 if language == Language::Python {
810 config.skip_lines = detect_python_docstring_lines(text);
811 }
812
813 let flags = IeeeFlags {
816 has_preprocessor_directives: has_preprocessor,
817 blank_in_block_comment_as_comment: options.blank_in_block_comment_as_comment,
818 collapse_continuation_lines: options.collapse_continuation_lines,
819 };
820 let mut result = analyze_generic(text, config, flags);
821 if options.enable_style && should_style_analyse(language, options.style_lang_scope) {
822 result.style_analysis = style::analyze_style(language, text);
823 }
824 result
825}
826
827const fn should_style_analyse(language: Language, scope: StyleLangScope) -> bool {
829 match scope {
830 StyleLangScope::CFamilyOnly => {
831 matches!(language, Language::C | Language::Cpp | Language::ObjectiveC)
832 }
833 StyleLangScope::All => true,
834 }
835}
836
837fn language_scan_config(language: Language) -> (ScanConfig, bool) {
845 let cfg = LANG_SCAN_TABLE
846 .iter()
847 .find_map(|&(l, c)| (l == language).then_some(c))
848 .unwrap_or_else(|| panic!("language_scan_config: no entry for {language:?}"));
849 let (branch_keywords, lsloc_strategy) = language_complexity_config(language);
850 (
851 ScanConfig {
852 line_comments: cfg.line_comments,
853 block_comment: cfg.block_comment,
854 allow_single_quote_strings: cfg.allow_single_quote_strings,
855 allow_double_quote_strings: cfg.allow_double_quote_strings,
856 allow_triple_quote_strings: cfg.allow_triple_quote_strings,
857 allow_csharp_verbatim_strings: cfg.allow_csharp_verbatim_strings,
858 allow_raw_strings: cfg.allow_raw_strings,
859 skip_lines: HashSet::new(),
860 symbol_patterns: cfg.symbol_patterns,
861 branch_keywords,
862 lsloc_strategy,
863 },
864 cfg.has_preprocessor,
865 )
866}
867
868const BRANCH_C_FAMILY: &[&str] = &[
873 "if", "else", "for", "while", "switch", "case", "catch", "||", "&&",
874];
875const BRANCH_C_TERNARY: &[&str] = &[
876 "if", "else", "for", "while", "switch", "case", "catch", "||", "&&", "?",
877];
878const BRANCH_GO: &[&str] = &["if", "else", "for", "switch", "case", "select", "||", "&&"];
879const BRANCH_RUST: &[&str] = &["if", "else", "for", "while", "match", "||", "&&"];
880const BRANCH_ZIG: &[&str] = &["if", "else", "for", "while", "switch", "catch", "||", "&&"];
881const BRANCH_FSHARP: &[&str] = &["if", "then", "else", "elif", "match", "when", "||", "&&"];
882const BRANCH_LUA: &[&str] = &[
883 "if", "elseif", "else", "for", "while", "repeat", "and", "or",
884];
885const BRANCH_HASKELL: &[&str] = &["if", "then", "else", "case", "otherwise"];
886const BRANCH_SQL: &[&str] = &["CASE", "WHEN", "IF", "ELSE", "case", "when", "if", "else"];
887const BRANCH_OCAML: &[&str] = &["if", "then", "else", "match", "when", "||", "&&"];
888const BRANCH_CLOJURE: &[&str] = &["if", "when", "cond", "case", "and", "or"];
889const BRANCH_PHP: &[&str] = &[
890 "if", "elseif", "else", "for", "while", "switch", "case", "catch", "match", "||", "&&", "?",
891];
892const BRANCH_JULIA: &[&str] = &["if", "elseif", "else", "for", "while", "catch", "||", "&&"];
893const BRANCH_PYTHON: &[&str] = &["if", "elif", "else", "for", "while", "except", "or", "and"];
894const BRANCH_RUBY: &[&str] = &[
895 "if", "elsif", "else", "unless", "until", "while", "case", "when", "rescue", "||", "&&",
896];
897const BRANCH_SHELL: &[&str] = &["if", "elif", "else", "while", "until", "case", "||", "&&"];
898const BRANCH_ELIXIR: &[&str] = &[
899 "if", "else", "cond", "case", "when", "rescue", "||", "&&", "and", "or",
900];
901const BRANCH_POWERSHELL: &[&str] = &[
902 "if", "elseif", "else", "for", "while", "switch", "foreach", "||", "&&",
903];
904const BRANCH_NIM: &[&str] = &[
905 "if", "elif", "else", "for", "while", "case", "of", "except", "and", "or",
906];
907const BRANCH_PERL: &[&str] = &[
908 "if", "elsif", "else", "unless", "until", "for", "while", "foreach", "||", "&&",
909];
910const BRANCH_R: &[&str] = &["if", "else", "for", "while", "repeat", "||", "&&"];
911const BRANCH_ADA: &[&str] = &[
913 "if", "elsif", "else", "case", "when", "loop", "while", "for", "and", "or",
914];
915const BRANCH_VHDL: &[&str] = &[
916 "if", "elsif", "else", "case", "when", "loop", "while", "for", "and", "or", "nand", "nor",
917 "xor",
918];
919const BRANCH_VERILOG: &[&str] = &[
920 "if", "else", "case", "casex", "casez", "for", "while", "&&", "||",
921];
922const BRANCH_TCL: &[&str] = &["if", "elseif", "else", "switch", "while", "for", "foreach"];
923const BRANCH_PASCAL: &[&str] = &[
924 "if", "then", "else", "case", "while", "for", "repeat", "until", "and", "or",
925];
926const BRANCH_VB: &[&str] = &[
927 "If", "Then", "ElseIf", "Else", "Select", "Case", "While", "For", "Do", "And", "Or",
928];
929const BRANCH_LISP: &[&str] = &["if", "when", "unless", "cond", "case", "and", "or"];
930const BRANCH_FORTRAN: &[&str] = &[
932 "if", "then", "else", "elseif", "case", "do", "while", "where",
933];
934const BRANCH_NIX: &[&str] = &["if", "then", "else"];
935const BRANCH_CMAKE: &[&str] = &["if(", "elseif(", "else(", "while(", "foreach("];
936const BRANCH_ELM: &[&str] = &["if", "then", "else", "case", "of"];
937const BRANCH_AWK: &[&str] = &["if", "else", "while", "for", "do"];
938
939const fn language_complexity_config(
942 language: Language,
943) -> (&'static [&'static str], LslocStrategy) {
944 match language {
945 Language::C
947 | Language::Cpp
948 | Language::ObjectiveC
949 | Language::CSharp
950 | Language::JavaScript
951 | Language::TypeScript
952 | Language::Svelte
953 | Language::Vue
954 | Language::Dart
955 | Language::Groovy
956 | Language::Swift
957 | Language::Solidity => (BRANCH_C_TERNARY, LslocStrategy::Semicolons),
958 Language::Java | Language::Kotlin | Language::Scala | Language::D | Language::Glsl => {
960 (BRANCH_C_FAMILY, LslocStrategy::Semicolons)
961 }
962 Language::Go => (BRANCH_GO, LslocStrategy::Semicolons),
963 Language::Rust => (BRANCH_RUST, LslocStrategy::Semicolons),
964 Language::Zig => (BRANCH_ZIG, LslocStrategy::Semicolons),
965 Language::FSharp => (BRANCH_FSHARP, LslocStrategy::Unsupported),
966 Language::Shell => (BRANCH_SHELL, LslocStrategy::NonContinuationNewlines),
968 Language::Elixir => (BRANCH_ELIXIR, LslocStrategy::NonContinuationNewlines),
969 Language::Perl => (BRANCH_PERL, LslocStrategy::Semicolons),
970 Language::R => (BRANCH_R, LslocStrategy::NonContinuationNewlines),
971 Language::Ruby | Language::Crystal => (BRANCH_RUBY, LslocStrategy::NonContinuationNewlines),
972 Language::Python => (BRANCH_PYTHON, LslocStrategy::NonContinuationNewlines),
973 Language::PowerShell => (BRANCH_POWERSHELL, LslocStrategy::Unsupported),
974 Language::Nim => (BRANCH_NIM, LslocStrategy::NonContinuationNewlines),
975 Language::Lua => (BRANCH_LUA, LslocStrategy::Unsupported),
977 Language::Haskell => (BRANCH_HASKELL, LslocStrategy::Unsupported),
978 Language::Sql => (BRANCH_SQL, LslocStrategy::Semicolons),
979 Language::Ocaml => (BRANCH_OCAML, LslocStrategy::Semicolons),
980 Language::Clojure => (BRANCH_CLOJURE, LslocStrategy::Unsupported),
981 Language::Php => (BRANCH_PHP, LslocStrategy::Semicolons),
982 Language::Julia => (BRANCH_JULIA, LslocStrategy::NonContinuationNewlines),
983 Language::Protobuf => (&[], LslocStrategy::Semicolons),
984 Language::Hcl => (&[], LslocStrategy::NonContinuationNewlines),
985 Language::Ada => (BRANCH_ADA, LslocStrategy::Semicolons),
987 Language::Vhdl => (BRANCH_VHDL, LslocStrategy::Semicolons),
988 Language::Verilog => (BRANCH_VERILOG, LslocStrategy::Semicolons),
989 Language::Tcl => (BRANCH_TCL, LslocStrategy::NonContinuationNewlines),
990 Language::Pascal => (BRANCH_PASCAL, LslocStrategy::Semicolons),
991 Language::VisualBasic => (BRANCH_VB, LslocStrategy::NonContinuationNewlines),
992 Language::Lisp => (BRANCH_LISP, LslocStrategy::Unsupported),
993 Language::Fortran => (BRANCH_FORTRAN, LslocStrategy::NonContinuationNewlines),
995 Language::Nix => (BRANCH_NIX, LslocStrategy::Unsupported),
996 Language::Cmake => (BRANCH_CMAKE, LslocStrategy::Unsupported),
997 Language::Elm => (BRANCH_ELM, LslocStrategy::Unsupported),
998 Language::Awk => (BRANCH_AWK, LslocStrategy::NonContinuationNewlines),
999 Language::Makefile
1001 | Language::Dockerfile
1002 | Language::Css
1003 | Language::Html
1004 | Language::Xml
1005 | Language::Assembly
1006 | Language::Erlang
1007 | Language::GraphQl
1008 | Language::Scss => (&[], LslocStrategy::Unsupported),
1009 }
1010}
1011
1012#[derive(Debug, Clone, Copy)]
1016struct SymbolPatterns {
1017 functions: &'static [&'static str],
1018 functions_prefix_paren: &'static [&'static str],
1024 classes: &'static [&'static str],
1025 variables: &'static [&'static str],
1026 imports: &'static [&'static str],
1027 tests: &'static [&'static str],
1030 assertions: &'static [&'static str],
1033 test_suites: &'static [&'static str],
1036 variables_prefix_no_paren: &'static [&'static str],
1042}
1043
1044impl SymbolPatterns {
1045 const fn none() -> Self {
1046 Self {
1047 functions: &[],
1048 functions_prefix_paren: &[],
1049 classes: &[],
1050 variables: &[],
1051 imports: &[],
1052 tests: &[],
1053 assertions: &[],
1054 test_suites: &[],
1055 variables_prefix_no_paren: &[],
1056 }
1057 }
1058}
1059
1060const SP_NONE: SymbolPatterns = SymbolPatterns::none(); const SP_SOLIDITY: SymbolPatterns = SymbolPatterns {
1065 functions: &[
1066 "function ",
1067 "modifier ",
1068 "constructor",
1069 "receive ",
1070 "fallback ",
1071 ],
1072 functions_prefix_paren: &[],
1073 classes: &["contract ", "interface ", "library ", "struct ", "enum "],
1074 variables: &[],
1075 imports: &["import "],
1076 tests: &["function test", "function testFuzz", "function invariant"],
1079 assertions: &[
1080 "assertEq(",
1081 "assertEq0(",
1082 "assertTrue(",
1083 "assertFalse(",
1084 "assertGt(",
1085 "assertLt(",
1086 "assertGe(",
1087 "assertLe(",
1088 "assertApproxEq",
1089 "vm.expectRevert(",
1090 "vm.expectEmit(",
1091 ],
1092 test_suites: &[],
1093 variables_prefix_no_paren: &[],
1094};
1095
1096const SP_PROTOBUF: SymbolPatterns = SymbolPatterns {
1099 functions: &["rpc "],
1100 functions_prefix_paren: &[],
1101 classes: &["message ", "service ", "enum "],
1102 variables: &[],
1103 imports: &["import "],
1104 tests: &[],
1105 assertions: &[],
1106 test_suites: &[],
1107 variables_prefix_no_paren: &[],
1108};
1109
1110const SP_ADA: SymbolPatterns = SymbolPatterns {
1112 functions: &["procedure ", "function "],
1113 functions_prefix_paren: &[],
1114 classes: &["package ", "type ", "task ", "protected "],
1115 variables: &[],
1116 imports: &["with ", "use "],
1117 tests: &[],
1118 assertions: &[],
1119 test_suites: &[],
1120 variables_prefix_no_paren: &[],
1121};
1122
1123const SP_VHDL: SymbolPatterns = SymbolPatterns {
1124 functions: &["function ", "procedure ", "process "],
1125 functions_prefix_paren: &[],
1126 classes: &["entity ", "architecture ", "package ", "component "],
1127 variables: &[],
1128 imports: &["library ", "use "],
1129 tests: &[],
1130 assertions: &[],
1131 test_suites: &[],
1132 variables_prefix_no_paren: &[],
1133};
1134
1135const SP_VERILOG: SymbolPatterns = SymbolPatterns {
1136 functions: &["function ", "task "],
1137 functions_prefix_paren: &[],
1138 classes: &["module ", "interface ", "class ", "package "],
1139 variables: &[],
1140 imports: &["import ", "`include"],
1141 tests: &[],
1142 assertions: &[],
1143 test_suites: &[],
1144 variables_prefix_no_paren: &[],
1145};
1146
1147const SP_TCL: SymbolPatterns = SymbolPatterns {
1148 functions: &["proc "],
1149 functions_prefix_paren: &[],
1150 classes: &[],
1151 variables: &[],
1152 imports: &["source ", "package require "],
1153 tests: &["test "],
1155 assertions: &[],
1156 test_suites: &[],
1157 variables_prefix_no_paren: &[],
1158};
1159
1160const SP_PASCAL: SymbolPatterns = SymbolPatterns {
1161 functions: &["procedure ", "function "],
1162 functions_prefix_paren: &[],
1163 classes: &["type ", "class ", "record "],
1164 variables: &[],
1165 imports: &["uses "],
1166 tests: &["procedure Test"],
1168 assertions: &[
1169 "Check(",
1170 "CheckEquals(",
1171 "CheckTrue(",
1172 "CheckFalse(",
1173 "CheckNotNull(",
1174 ],
1175 test_suites: &[],
1176 variables_prefix_no_paren: &[],
1177};
1178
1179const SP_VB: SymbolPatterns = SymbolPatterns {
1180 functions: &[
1181 "Sub ",
1182 "Function ",
1183 "Private Sub ",
1184 "Public Sub ",
1185 "Private Function ",
1186 "Public Function ",
1187 ],
1188 functions_prefix_paren: &[],
1189 classes: &["Class ", "Module ", "Structure "],
1190 variables: &[],
1191 imports: &["Imports "],
1192 tests: &["<TestMethod>", "<TestMethod("],
1194 assertions: &["Assert.", "CollectionAssert.", "StringAssert."],
1195 test_suites: &["<TestClass>", "<TestClass("],
1196 variables_prefix_no_paren: &[],
1197};
1198
1199const SP_LISP: SymbolPatterns = SymbolPatterns {
1200 functions: &["(defun ", "(defmacro ", "(define ", "(defmethod ", "(defn "],
1201 functions_prefix_paren: &[],
1202 classes: &["(defclass ", "(defstruct "],
1203 variables: &[],
1204 imports: &["(require ", "(import ", "(use-package "],
1205 tests: &["(test ", "(deftest "],
1207 assertions: &["(is ", "(is-true ", "(is-false ", "(signals "],
1208 test_suites: &[],
1209 variables_prefix_no_paren: &[],
1210};
1211
1212const SP_FORTRAN: SymbolPatterns = SymbolPatterns {
1214 functions: &["subroutine ", "function "],
1215 functions_prefix_paren: &[],
1216 classes: &["module ", "program ", "type "],
1217 variables: &[],
1218 imports: &["use ", "include "],
1219 tests: &[],
1220 assertions: &[],
1221 test_suites: &[],
1222 variables_prefix_no_paren: &[],
1223};
1224
1225const SP_CRYSTAL: SymbolPatterns = SymbolPatterns {
1226 functions: &["def "],
1227 functions_prefix_paren: &[],
1228 classes: &["class ", "module ", "struct ", "enum "],
1229 variables: &[],
1230 imports: &["require "],
1231 tests: &["it ", "it(", "describe ", "context ", "pending "],
1233 assertions: &[],
1234 test_suites: &[],
1235 variables_prefix_no_paren: &[],
1236};
1237
1238const SP_D: SymbolPatterns = SymbolPatterns {
1239 functions: &[],
1240 functions_prefix_paren: &[],
1241 classes: &["class ", "struct ", "interface ", "enum ", "template "],
1242 variables: &[],
1243 imports: &["import "],
1244 tests: &["unittest"],
1246 assertions: &["assert(", "assertThrown", "assertNotThrown"],
1247 test_suites: &[],
1248 variables_prefix_no_paren: &[],
1249};
1250
1251const SP_CMAKE: SymbolPatterns = SymbolPatterns {
1252 functions: &["function(", "macro("],
1253 functions_prefix_paren: &[],
1254 classes: &[],
1255 variables: &[],
1256 imports: &["include(", "add_subdirectory("],
1257 tests: &[],
1258 assertions: &[],
1259 test_suites: &[],
1260 variables_prefix_no_paren: &[],
1261};
1262
1263const SP_ELM: SymbolPatterns = SymbolPatterns {
1264 functions: &[],
1265 functions_prefix_paren: &[],
1266 classes: &["type "],
1267 variables: &[],
1268 imports: &["import "],
1269 tests: &["test ", "describe ", "fuzz "],
1271 assertions: &["Expect."],
1272 test_suites: &[],
1273 variables_prefix_no_paren: &[],
1274};
1275
1276const SP_AWK: SymbolPatterns = SymbolPatterns {
1277 functions: &["function "],
1278 functions_prefix_paren: &[],
1279 classes: &[],
1280 variables: &[],
1281 imports: &[],
1282 tests: &[],
1283 assertions: &[],
1284 test_suites: &[],
1285 variables_prefix_no_paren: &[],
1286};
1287
1288const SP_RUST: SymbolPatterns = SymbolPatterns {
1289 functions: &[
1290 "fn ",
1291 "pub fn ",
1292 "pub(crate) fn ",
1293 "pub(super) fn ",
1294 "async fn ",
1295 "pub async fn ",
1296 "pub(crate) async fn ",
1297 "unsafe fn ",
1298 "pub unsafe fn ",
1299 "pub(crate) unsafe fn ",
1300 "const fn ",
1301 "pub const fn ",
1302 "pub(crate) const fn ",
1303 "extern fn ",
1304 "pub extern fn ",
1305 ],
1306 functions_prefix_paren: &[],
1307 classes: &[
1308 "struct ",
1309 "pub struct ",
1310 "pub(crate) struct ",
1311 "enum ",
1312 "pub enum ",
1313 "pub(crate) enum ",
1314 "trait ",
1315 "pub trait ",
1316 "pub(crate) trait ",
1317 "impl ",
1318 "impl<",
1319 "type ",
1320 "pub type ",
1321 "pub(crate) type ",
1322 ],
1323 variables: &["let ", "let mut "],
1324 imports: &["use ", "pub use ", "pub(crate) use ", "extern crate "],
1325 tests: &[
1327 "#[test]",
1328 "#[tokio::test]",
1329 "#[actix_web::test]",
1330 "#[rstest]",
1331 "#[test_case",
1332 ],
1333 assertions: &[
1334 "assert_eq!(",
1335 "assert_ne!(",
1336 "assert!(",
1337 "assert_matches!(",
1338 "assert_err!(",
1339 "assert_ok!(",
1340 ],
1341 test_suites: &[],
1342 variables_prefix_no_paren: &[],
1343};
1344
1345const SP_PYTHON: SymbolPatterns = SymbolPatterns {
1346 functions: &["def ", "async def "],
1347 functions_prefix_paren: &[],
1348 classes: &["class "],
1349 variables: &[],
1350 imports: &["import ", "from "],
1351 tests: &["def test_", "async def test_", "class Test"],
1353 assertions: &[
1354 "self.assertEqual(",
1355 "self.assertNotEqual(",
1356 "self.assertTrue(",
1357 "self.assertFalse(",
1358 "self.assertIsNone(",
1359 "self.assertIsNotNone(",
1360 "self.assertIn(",
1361 "self.assertNotIn(",
1362 "self.assertRaises(",
1363 "self.assertAlmostEqual(",
1364 ],
1365 test_suites: &[],
1366 variables_prefix_no_paren: &[],
1367};
1368
1369const SP_JS: SymbolPatterns = SymbolPatterns {
1370 functions: &[
1371 "function ",
1372 "async function ",
1373 "export function ",
1374 "export async function ",
1375 "export default function ",
1376 ],
1377 functions_prefix_paren: &[],
1378 classes: &["class ", "export class ", "export default class "],
1379 variables: &[
1380 "var ",
1381 "let ",
1382 "const ",
1383 "export var ",
1384 "export let ",
1385 "export const ",
1386 ],
1387 imports: &["import "],
1388 tests: &[
1390 "describe(",
1391 "it(",
1392 "test(",
1393 "it.each(",
1394 "test.each(",
1395 "describe.each(",
1396 ],
1397 assertions: &["expect("],
1398 test_suites: &[],
1399 variables_prefix_no_paren: &[],
1400};
1401
1402const SP_TS: SymbolPatterns = SymbolPatterns {
1403 functions: &[
1404 "function ",
1405 "async function ",
1406 "export function ",
1407 "export async function ",
1408 "export default function ",
1409 ],
1410 functions_prefix_paren: &[],
1411 classes: &[
1412 "class ",
1413 "export class ",
1414 "export default class ",
1415 "abstract class ",
1416 "export abstract class ",
1417 "interface ",
1418 "export interface ",
1419 "declare class ",
1420 "declare interface ",
1421 ],
1422 variables: &[
1423 "var ",
1424 "let ",
1425 "const ",
1426 "export var ",
1427 "export let ",
1428 "export const ",
1429 ],
1430 imports: &["import "],
1431 tests: &[
1433 "describe(",
1434 "it(",
1435 "test(",
1436 "it.each(",
1437 "test.each(",
1438 "describe.each(",
1439 ],
1440 assertions: &["expect("],
1441 test_suites: &[],
1442 variables_prefix_no_paren: &[],
1443};
1444
1445const SP_GO: SymbolPatterns = SymbolPatterns {
1446 functions: &["func "],
1447 functions_prefix_paren: &[],
1448 classes: &["type "],
1449 variables: &["var "],
1450 imports: &["import "],
1451 tests: &["func Test", "func Benchmark", "func Fuzz"],
1453 assertions: &[],
1454 test_suites: &[],
1455 variables_prefix_no_paren: &[],
1456};
1457
1458const SP_JAVA: SymbolPatterns = SymbolPatterns {
1459 functions: &[],
1460 functions_prefix_paren: &[],
1461 classes: &[
1462 "class ",
1463 "public class ",
1464 "private class ",
1465 "protected class ",
1466 "abstract class ",
1467 "final class ",
1468 "public abstract class ",
1469 "public final class ",
1470 "interface ",
1471 "public interface ",
1472 "enum ",
1473 "public enum ",
1474 "record ",
1475 "public record ",
1476 "@interface ",
1477 ],
1478 variables: &[],
1479 imports: &["import "],
1480 tests: &[
1482 "@Test",
1483 "@ParameterizedTest",
1484 "@RepeatedTest",
1485 "@TestFactory",
1486 "@TestTemplate",
1487 ],
1488 assertions: &[
1489 "assertEquals(",
1490 "assertNotEquals(",
1491 "assertTrue(",
1492 "assertFalse(",
1493 "assertNull(",
1494 "assertNotNull(",
1495 "assertThat(",
1496 "assertThrows(",
1497 "assertAll(",
1498 "assertArrayEquals(",
1499 "assertIterableEquals(",
1500 "assertLinesMatch(",
1501 ],
1502 test_suites: &[],
1503 variables_prefix_no_paren: &[],
1504};
1505
1506const SP_CSHARP: SymbolPatterns = SymbolPatterns {
1507 functions: &[],
1508 functions_prefix_paren: &[],
1509 classes: &[
1510 "class ",
1511 "public class ",
1512 "private class ",
1513 "protected class ",
1514 "internal class ",
1515 "abstract class ",
1516 "sealed class ",
1517 "static class ",
1518 "partial class ",
1519 "public abstract class ",
1520 "public sealed class ",
1521 "public static class ",
1522 "interface ",
1523 "public interface ",
1524 "internal interface ",
1525 "enum ",
1526 "public enum ",
1527 "struct ",
1528 "public struct ",
1529 "record ",
1530 "public record ",
1531 ],
1532 variables: &["var "],
1533 imports: &["using "],
1534 tests: &[
1536 "[TestMethod]",
1537 "[Test]",
1538 "[Fact]",
1539 "[Theory]",
1540 "[TestCase(",
1541 "[DataRow(",
1542 "[InlineData(",
1543 "[MemberData(",
1544 ],
1545 assertions: &[
1546 "Assert.AreEqual(",
1547 "Assert.AreNotEqual(",
1548 "Assert.IsTrue(",
1549 "Assert.IsFalse(",
1550 "Assert.IsNull(",
1551 "Assert.IsNotNull(",
1552 "Assert.Equal(",
1553 "Assert.NotEqual(",
1554 "Assert.True(",
1555 "Assert.False(",
1556 "Assert.That(",
1557 "Assert.Contains(",
1558 "Assert.Throws(",
1559 "Assert.ThrowsAsync(",
1560 "Assert.IsInstanceOfType(",
1561 ],
1562 test_suites: &["[TestClass]", "[TestFixture]", "[SetUpFixture]"],
1563 variables_prefix_no_paren: &[],
1564};
1565
1566const TEST_PATTERNS_C_CPP: &[&str] = &[
1568 "TEST(",
1570 "TEST_F(",
1571 "TEST_P(",
1572 "TYPED_TEST(",
1573 "TYPED_TEST_P(",
1574 "INSTANTIATE_TEST_SUITE_P(",
1575 "INSTANTIATE_TYPED_TEST_SUITE_P(",
1576 "TEST_CASE(",
1578 "SECTION(",
1579 "SCENARIO(",
1580 "SCENARIO_METHOD(",
1581 "TEST_CASE_METHOD(",
1582 "BOOST_AUTO_TEST_CASE(",
1584 "BOOST_FIXTURE_TEST_CASE(",
1585 "BOOST_AUTO_TEST_SUITE(",
1586 "BOOST_PARAM_TEST_CASE(",
1587 "CPPUNIT_TEST(",
1589 "CPPUNIT_TEST_SUITE(",
1590 "RUN_TEST(",
1592 "TEST_IGNORE(",
1593 "TEST_FAIL(",
1594 "START_TEST(",
1596 "tcase_add_test(",
1597 "suite_create(",
1598 "cmocka_unit_test(",
1600 "cmocka_run_group_tests(",
1601 "IGNORE_TEST(",
1603 "TEST_GROUP(",
1604 "TEST_GROUP_BASE(",
1605];
1606
1607const ASSERT_PATTERNS_C_CPP: &[&str] = &[
1609 "ASSERT_EQ(",
1611 "ASSERT_NE(",
1612 "ASSERT_LT(",
1613 "ASSERT_LE(",
1614 "ASSERT_GT(",
1615 "ASSERT_GE(",
1616 "ASSERT_TRUE(",
1617 "ASSERT_FALSE(",
1618 "ASSERT_STREQ(",
1619 "ASSERT_STRNE(",
1620 "ASSERT_FLOAT_EQ(",
1621 "ASSERT_DOUBLE_EQ(",
1622 "ASSERT_NEAR(",
1623 "ASSERT_THROW(",
1624 "ASSERT_NO_THROW(",
1625 "ASSERT_ANY_THROW(",
1626 "EXPECT_EQ(",
1628 "EXPECT_NE(",
1629 "EXPECT_LT(",
1630 "EXPECT_LE(",
1631 "EXPECT_GT(",
1632 "EXPECT_GE(",
1633 "EXPECT_TRUE(",
1634 "EXPECT_FALSE(",
1635 "EXPECT_STREQ(",
1636 "EXPECT_STRNE(",
1637 "EXPECT_FLOAT_EQ(",
1638 "EXPECT_DOUBLE_EQ(",
1639 "EXPECT_NEAR(",
1640 "EXPECT_THROW(",
1641 "EXPECT_NO_THROW(",
1642 "EXPECT_ANY_THROW(",
1643 "REQUIRE(",
1645 "CHECK(",
1646 "REQUIRE_FALSE(",
1647 "CHECK_FALSE(",
1648 "REQUIRE_NOTHROW(",
1649 "CHECK_NOTHROW(",
1650 "REQUIRE_THROWS(",
1651 "CHECK_THROWS(",
1652 "REQUIRE_THAT(",
1653 "CHECK_THAT(",
1654 "TEST_ASSERT_EQUAL(",
1656 "TEST_ASSERT_EQUAL_INT(",
1657 "TEST_ASSERT_EQUAL_STRING(",
1658 "TEST_ASSERT_EQUAL_FLOAT(",
1659 "TEST_ASSERT_EQUAL_DOUBLE(",
1660 "TEST_ASSERT_EQUAL_PTR(",
1661 "TEST_ASSERT_TRUE(",
1662 "TEST_ASSERT_FALSE(",
1663 "TEST_ASSERT_NULL(",
1664 "TEST_ASSERT_NOT_NULL(",
1665 "TEST_ASSERT_BITS_HIGH(",
1666 "TEST_ASSERT_BITS_LOW(",
1667 "assert_int_equal(",
1669 "assert_int_not_equal(",
1670 "assert_string_equal(",
1671 "assert_string_not_equal(",
1672 "assert_true(",
1673 "assert_false(",
1674 "assert_null(",
1675 "assert_non_null(",
1676 "assert_ptr_equal(",
1677 "assert_memory_equal(",
1678 "assert_return_code(",
1679];
1680
1681const SUITE_PATTERNS_C_CPP: &[&str] = &[
1683 "TEST_GROUP(",
1684 "TEST_GROUP_BASE(",
1685 "BOOST_AUTO_TEST_SUITE(",
1686 "CPPUNIT_TEST_SUITE(",
1687 "CPPUNIT_TEST_SUITE_END(",
1688];
1689
1690const SP_C: SymbolPatterns = SymbolPatterns {
1691 functions: &[],
1693 functions_prefix_paren: &[
1694 "void ",
1695 "int ",
1696 "char ",
1697 "float ",
1698 "double ",
1699 "long ",
1700 "unsigned ",
1701 "size_t ",
1702 "static ",
1703 "inline ",
1704 "const ",
1705 "extern ",
1706 ],
1707 classes: &[
1708 "struct ",
1709 "typedef struct ",
1710 "union ",
1711 "typedef union ",
1712 "typedef enum ",
1713 ],
1714 variables: &[],
1715 imports: &["#include "],
1716 tests: TEST_PATTERNS_C_CPP,
1717 assertions: ASSERT_PATTERNS_C_CPP,
1718 test_suites: SUITE_PATTERNS_C_CPP,
1719 variables_prefix_no_paren: &[
1722 "void ",
1723 "int ",
1724 "char ",
1725 "float ",
1726 "double ",
1727 "long ",
1728 "unsigned ",
1729 "size_t ",
1730 "static ",
1731 "inline ",
1732 "const ",
1733 "extern ",
1734 ],
1735};
1736
1737const SP_CPP: SymbolPatterns = SymbolPatterns {
1738 functions: &[
1740 "virtual ", "explicit ", "~", "operator", ],
1745 functions_prefix_paren: &[
1746 "void ",
1747 "bool ",
1748 "int ",
1749 "char ",
1750 "float ",
1751 "double ",
1752 "long ",
1753 "unsigned ",
1754 "size_t ",
1755 "auto ",
1756 "static ",
1757 "inline ",
1758 "constexpr ",
1759 "const ",
1760 "extern ",
1761 ],
1762 classes: &["class ", "struct ", "namespace ", "template ", "template<"],
1764 variables: &[],
1765 imports: &["#include "],
1766 tests: TEST_PATTERNS_C_CPP,
1767 assertions: ASSERT_PATTERNS_C_CPP,
1768 test_suites: SUITE_PATTERNS_C_CPP,
1769 variables_prefix_no_paren: &[
1771 "void ",
1772 "bool ",
1773 "int ",
1774 "char ",
1775 "float ",
1776 "double ",
1777 "long ",
1778 "unsigned ",
1779 "size_t ",
1780 "auto ",
1781 "static ",
1782 "inline ",
1783 "constexpr ",
1784 "const ",
1785 "extern ",
1786 ],
1787};
1788
1789const SP_SHELL: SymbolPatterns = SymbolPatterns {
1790 functions: &["function "],
1791 functions_prefix_paren: &[],
1792 classes: &[],
1793 variables: &["declare ", "local ", "export "],
1794 imports: &["source ", ". "],
1795 tests: &["@test "],
1797 assertions: &[],
1798 test_suites: &[],
1799 variables_prefix_no_paren: &[],
1800};
1801
1802const SP_POWERSHELL: SymbolPatterns = SymbolPatterns {
1803 functions: &["function ", "Function "],
1804 functions_prefix_paren: &[],
1805 classes: &["class "],
1806 variables: &[],
1807 imports: &["Import-Module ", "using "],
1808 tests: &["Describe ", "It ", "Context "],
1810 assertions: &[],
1811 test_suites: &[],
1812 variables_prefix_no_paren: &[],
1813};
1814
1815const SP_KOTLIN: SymbolPatterns = SymbolPatterns {
1816 functions: &[
1817 "fun ",
1818 "private fun ",
1819 "public fun ",
1820 "protected fun ",
1821 "internal fun ",
1822 "override fun ",
1823 "suspend fun ",
1824 "abstract fun ",
1825 "open fun ",
1826 "private suspend fun ",
1827 "public suspend fun ",
1828 ],
1829 functions_prefix_paren: &[],
1830 classes: &[
1831 "class ",
1832 "data class ",
1833 "sealed class ",
1834 "abstract class ",
1835 "open class ",
1836 "object ",
1837 "companion object",
1838 "interface ",
1839 "enum class ",
1840 "annotation class ",
1841 ],
1842 variables: &["val ", "var ", "private val ", "private var ", "const val "],
1843 imports: &["import "],
1844 tests: &[
1846 "@Test",
1847 "@ParameterizedTest",
1848 "@RepeatedTest",
1849 "\"should ",
1850 "\"it ",
1851 ],
1852 assertions: &[
1853 "assertEquals(",
1854 "assertNotEquals(",
1855 "assertTrue(",
1856 "assertFalse(",
1857 "assertNull(",
1858 "assertNotNull(",
1859 "assertThat(",
1860 "assertThrows(",
1861 "shouldBe(",
1862 "shouldNotBe(",
1863 "shouldThrow(",
1864 ],
1865 test_suites: &[],
1866 variables_prefix_no_paren: &[],
1867};
1868
1869const SP_SWIFT: SymbolPatterns = SymbolPatterns {
1870 functions: &[
1871 "func ",
1872 "private func ",
1873 "public func ",
1874 "internal func ",
1875 "override func ",
1876 "open func ",
1877 "static func ",
1878 "class func ",
1879 "mutating func ",
1880 "private static func ",
1881 "public static func ",
1882 ],
1883 functions_prefix_paren: &[],
1884 classes: &[
1885 "class ",
1886 "struct ",
1887 "protocol ",
1888 "enum ",
1889 "extension ",
1890 "actor ",
1891 "public class ",
1892 "private class ",
1893 "open class ",
1894 "final class ",
1895 "public struct ",
1896 "private struct ",
1897 "public protocol ",
1898 ],
1899 variables: &[
1900 "var ",
1901 "let ",
1902 "private var ",
1903 "private let ",
1904 "static var ",
1905 "static let ",
1906 ],
1907 imports: &["import "],
1908 tests: &["func test", "func Test", "@Test"],
1910 assertions: &[
1911 "XCTAssertEqual(",
1912 "XCTAssertNotEqual(",
1913 "XCTAssertTrue(",
1914 "XCTAssertFalse(",
1915 "XCTAssertNil(",
1916 "XCTAssertNotNil(",
1917 "XCTAssertGreaterThan(",
1918 "XCTAssertLessThan(",
1919 "XCTAssertThrowsError(",
1920 "XCTAssertNoThrow(",
1921 "#expect(",
1922 ],
1923 test_suites: &[],
1924 variables_prefix_no_paren: &[],
1925};
1926
1927const SP_RUBY: SymbolPatterns = SymbolPatterns {
1928 functions: &["def ", "private def ", "protected def "],
1929 functions_prefix_paren: &[],
1930 classes: &["class ", "module "],
1931 variables: &[],
1932 imports: &["require ", "require_relative "],
1933 tests: &["it ", "it(", "describe ", "context ", "test "],
1935 assertions: &[],
1936 test_suites: &[],
1937 variables_prefix_no_paren: &[],
1938};
1939
1940const SP_SCALA: SymbolPatterns = SymbolPatterns {
1941 functions: &["def ", "private def ", "protected def ", "override def "],
1942 functions_prefix_paren: &[],
1943 classes: &[
1944 "class ",
1945 "case class ",
1946 "abstract class ",
1947 "sealed class ",
1948 "object ",
1949 "trait ",
1950 ],
1951 variables: &["val ", "var ", "lazy val "],
1952 imports: &["import "],
1953 tests: &["test(", "it(", "describe("],
1955 assertions: &[],
1956 test_suites: &[],
1957 variables_prefix_no_paren: &[],
1958};
1959
1960const SP_PHP: SymbolPatterns = SymbolPatterns {
1961 functions: &[
1962 "function ",
1963 "public function ",
1964 "private function ",
1965 "protected function ",
1966 "static function ",
1967 "abstract function ",
1968 "final function ",
1969 "public static function ",
1970 "private static function ",
1971 "protected static function ",
1972 ],
1973 functions_prefix_paren: &[],
1974 classes: &[
1975 "class ",
1976 "abstract class ",
1977 "final class ",
1978 "interface ",
1979 "trait ",
1980 "enum ",
1981 ],
1982 variables: &[],
1983 imports: &[
1984 "use ",
1985 "require ",
1986 "require_once ",
1987 "include ",
1988 "include_once ",
1989 ],
1990 tests: &[
1992 "public function test",
1993 "function test",
1994 "#[Test]",
1995 "#[DataProvider(",
1996 ],
1997 assertions: &[],
1998 test_suites: &[],
1999 variables_prefix_no_paren: &[],
2000};
2001
2002const SP_ELIXIR: SymbolPatterns = SymbolPatterns {
2003 functions: &[
2004 "def ",
2005 "defp ",
2006 "defmacro ",
2007 "defmacrop ",
2008 "defguard ",
2009 "defguardp ",
2010 ],
2011 functions_prefix_paren: &[],
2012 classes: &["defmodule ", "defprotocol ", "defimpl "],
2013 variables: &[],
2014 imports: &["import ", "alias ", "use ", "require "],
2015 tests: &["test ", "describe "],
2017 assertions: &[],
2018 test_suites: &[],
2019 variables_prefix_no_paren: &[],
2020};
2021
2022const SP_ERLANG: SymbolPatterns = SymbolPatterns {
2023 functions: &[],
2024 functions_prefix_paren: &[],
2025 classes: &["-module("],
2026 variables: &[],
2027 imports: &["-import(", "-include(", "-include_lib("],
2028 tests: &[],
2031 assertions: &[
2032 "?assert(",
2033 "?assertEqual(",
2034 "?assertNotEqual(",
2035 "?assertMatch(",
2036 "?assertError(",
2037 "?assertThrow(",
2038 "?assertException(",
2039 ],
2040 test_suites: &[],
2041 variables_prefix_no_paren: &[],
2042};
2043
2044const SP_FSHARP: SymbolPatterns = SymbolPatterns {
2045 functions: &[
2046 "let ",
2047 "let rec ",
2048 "member ",
2049 "override ",
2050 "abstract member ",
2051 ],
2052 functions_prefix_paren: &[],
2053 classes: &["type "],
2054 variables: &["let mutable "],
2055 imports: &["open "],
2056 tests: &["[<Test>]", "[<Fact>]", "[<Theory>]", "[<TestCase("],
2058 assertions: &[],
2059 test_suites: &[],
2060 variables_prefix_no_paren: &[],
2061};
2062
2063const SP_GROOVY: SymbolPatterns = SymbolPatterns {
2064 functions: &["def ", "private def ", "public def ", "protected def "],
2065 functions_prefix_paren: &[],
2066 classes: &["class ", "abstract class ", "interface ", "enum ", "trait "],
2067 variables: &[],
2068 imports: &["import "],
2069 tests: &["def \"", "@Test", "given:", "when:", "then:", "expect:"],
2071 assertions: &[],
2072 test_suites: &[],
2073 variables_prefix_no_paren: &[],
2074};
2075
2076const SP_HASKELL: SymbolPatterns = SymbolPatterns {
2077 functions: &[],
2078 functions_prefix_paren: &[],
2079 classes: &["class ", "data ", "newtype ", "type "],
2080 variables: &[],
2081 imports: &["import "],
2082 tests: &["describe ", "it ", "prop_"],
2085 assertions: &[],
2086 test_suites: &[],
2087 variables_prefix_no_paren: &[],
2088};
2089
2090const SP_LUA: SymbolPatterns = SymbolPatterns {
2091 functions: &["function ", "local function "],
2092 functions_prefix_paren: &[],
2093 classes: &[],
2094 variables: &["local "],
2095 imports: &[],
2096 tests: &["it(", "describe(", "pending("],
2098 assertions: &[],
2099 test_suites: &[],
2100 variables_prefix_no_paren: &[],
2101};
2102
2103const SP_NIM: SymbolPatterns = SymbolPatterns {
2104 functions: &[
2105 "proc ",
2106 "func ",
2107 "method ",
2108 "iterator ",
2109 "converter ",
2110 "template ",
2111 "macro ",
2112 ],
2113 functions_prefix_paren: &[],
2114 classes: &["type "],
2115 variables: &["var ", "let ", "const "],
2116 imports: &["import ", "from "],
2117 tests: &["test "],
2119 assertions: &[],
2120 test_suites: &[],
2121 variables_prefix_no_paren: &[],
2122};
2123
2124const SP_OBJECTIVEC: SymbolPatterns = SymbolPatterns {
2125 functions: &["- (", "+ ("],
2126 functions_prefix_paren: &[],
2127 classes: &["@interface ", "@implementation ", "@protocol "],
2128 variables: &[],
2129 imports: &["#import ", "#include "],
2130 tests: &["- (void)test"],
2132 assertions: &[
2133 "XCTAssertEqual(",
2134 "XCTAssertNotEqual(",
2135 "XCTAssertTrue(",
2136 "XCTAssertFalse(",
2137 "XCTAssertNil(",
2138 "XCTAssertNotNil(",
2139 "XCTAssertGreaterThan(",
2140 "XCTAssertLessThan(",
2141 "XCTAssertThrowsError(",
2142 "XCTAssertNoThrow(",
2143 ],
2144 test_suites: &[],
2145 variables_prefix_no_paren: &[],
2146};
2147
2148const SP_OCAML: SymbolPatterns = SymbolPatterns {
2149 functions: &["let ", "let rec "],
2150 functions_prefix_paren: &[],
2151 classes: &["type ", "module ", "class "],
2152 variables: &[],
2153 imports: &["open "],
2154 tests: &["let test_", "test_case "],
2156 assertions: &[
2157 "assert_equal ",
2158 "assert_bool ",
2159 "assert_raises ",
2160 "assert_failure ",
2161 "OUnit.assert",
2162 ],
2163 test_suites: &[],
2164 variables_prefix_no_paren: &[],
2165};
2166
2167const SP_PERL: SymbolPatterns = SymbolPatterns {
2168 functions: &["sub "],
2169 functions_prefix_paren: &[],
2170 classes: &["package "],
2171 variables: &["my ", "our ", "local "],
2172 imports: &["use ", "require "],
2173 tests: &["subtest "],
2175 assertions: &[
2176 "ok(",
2177 "is(",
2178 "isnt(",
2179 "like(",
2180 "unlike(",
2181 "cmp_ok(",
2182 "is_deeply(",
2183 "isa_ok(",
2184 "can_ok(",
2185 ],
2186 test_suites: &[],
2187 variables_prefix_no_paren: &[],
2188};
2189
2190const SP_CLOJURE: SymbolPatterns = SymbolPatterns {
2191 functions: &["(defn ", "(defn- ", "(defmacro ", "(defmulti "],
2192 functions_prefix_paren: &[],
2193 classes: &[
2194 "(defrecord ",
2195 "(defprotocol ",
2196 "(deftype ",
2197 "(definterface ",
2198 ],
2199 variables: &["(def ", "(defonce "],
2200 imports: &["(ns ", "(require "],
2201 tests: &["(deftest ", "(testing "],
2203 assertions: &[],
2204 test_suites: &[],
2205 variables_prefix_no_paren: &[],
2206};
2207
2208const SP_JULIA: SymbolPatterns = SymbolPatterns {
2209 functions: &["function ", "macro "],
2210 functions_prefix_paren: &[],
2211 classes: &[
2212 "struct ",
2213 "mutable struct ",
2214 "abstract type ",
2215 "primitive type ",
2216 ],
2217 variables: &["const "],
2218 imports: &["import ", "using "],
2219 tests: &["@test ", "@testset "],
2221 assertions: &[],
2222 test_suites: &[],
2223 variables_prefix_no_paren: &[],
2224};
2225
2226const SP_DART: SymbolPatterns = SymbolPatterns {
2227 functions: &[],
2228 functions_prefix_paren: &[],
2229 classes: &["class ", "abstract class ", "mixin ", "extension ", "enum "],
2230 variables: &["var ", "final ", "const ", "late "],
2231 imports: &["import "],
2232 tests: &["test(", "testWidgets(", "group("],
2234 assertions: &[],
2235 test_suites: &[],
2236 variables_prefix_no_paren: &[],
2237};
2238
2239const SP_R: SymbolPatterns = SymbolPatterns {
2240 functions: &[],
2241 functions_prefix_paren: &[],
2242 classes: &[],
2243 variables: &[],
2244 imports: &["library(", "source("],
2245 tests: &["test_that(", "it(", "describe(", "expect_"],
2247 assertions: &[],
2248 test_suites: &[],
2249 variables_prefix_no_paren: &[],
2250};
2251
2252const SP_SQL: SymbolPatterns = SymbolPatterns {
2253 functions: &[
2254 "create function ",
2255 "create or replace function ",
2256 "create procedure ",
2257 "create or replace procedure ",
2258 "CREATE FUNCTION ",
2259 "CREATE OR REPLACE FUNCTION ",
2260 "CREATE PROCEDURE ",
2261 "CREATE OR REPLACE PROCEDURE ",
2262 ],
2263 functions_prefix_paren: &[],
2264 classes: &[
2265 "create table ",
2266 "create view ",
2267 "create schema ",
2268 "CREATE TABLE ",
2269 "CREATE VIEW ",
2270 "CREATE SCHEMA ",
2271 ],
2272 variables: &["declare ", "DECLARE "],
2273 imports: &[],
2274 tests: &[],
2275 assertions: &[],
2276 test_suites: &[],
2277 variables_prefix_no_paren: &[],
2278};
2279
2280const SP_ASSEMBLY: SymbolPatterns = SymbolPatterns {
2281 functions: &["proc ", "PROC "],
2282 functions_prefix_paren: &[],
2283 classes: &[],
2284 variables: &[],
2285 imports: &["include ", "INCLUDE ", "%include "],
2286 tests: &[],
2287 assertions: &[],
2288 test_suites: &[],
2289 variables_prefix_no_paren: &[],
2290};
2291
2292const SP_ZIG: SymbolPatterns = SymbolPatterns {
2293 functions: &[
2294 "fn ",
2295 "pub fn ",
2296 "export fn ",
2297 "inline fn ",
2298 "pub inline fn ",
2299 ],
2300 functions_prefix_paren: &[],
2301 classes: &[],
2302 variables: &["var ", "pub var "],
2303 imports: &[],
2304 tests: &["test \"", "test{"],
2306 assertions: &[],
2307 test_suites: &[],
2308 variables_prefix_no_paren: &[],
2309};
2310
2311#[allow(clippy::struct_excessive_bools)]
2315#[derive(Clone, Copy)]
2316struct StaticLangConfig {
2317 line_comments: &'static [&'static str],
2318 block_comment: Option<(&'static str, &'static str)>,
2319 allow_single_quote_strings: bool,
2320 allow_double_quote_strings: bool,
2321 allow_triple_quote_strings: bool,
2322 allow_csharp_verbatim_strings: bool,
2323 allow_raw_strings: bool,
2326 symbol_patterns: SymbolPatterns,
2327 has_preprocessor: bool,
2329}
2330
2331#[allow(clippy::struct_excessive_bools)]
2332#[derive(Debug, Clone)]
2333struct ScanConfig {
2334 line_comments: &'static [&'static str],
2335 block_comment: Option<(&'static str, &'static str)>,
2336 allow_single_quote_strings: bool,
2337 allow_double_quote_strings: bool,
2338 allow_triple_quote_strings: bool,
2339 allow_csharp_verbatim_strings: bool,
2340 allow_raw_strings: bool,
2341 skip_lines: HashSet<usize>,
2342 symbol_patterns: SymbolPatterns,
2343 branch_keywords: &'static [&'static str],
2345 lsloc_strategy: LslocStrategy,
2347}
2348
2349const C_SLASH_BASE: StaticLangConfig = StaticLangConfig {
2359 line_comments: &["//"],
2360 block_comment: Some(("/*", "*/")),
2361 allow_single_quote_strings: true,
2362 allow_double_quote_strings: true,
2363 allow_triple_quote_strings: false,
2364 allow_csharp_verbatim_strings: false,
2365 allow_raw_strings: false,
2366 symbol_patterns: SP_NONE,
2367 has_preprocessor: false,
2368};
2369
2370const HASH_BASE: StaticLangConfig = StaticLangConfig {
2374 line_comments: &["#"],
2375 block_comment: None,
2376 allow_single_quote_strings: true,
2377 allow_double_quote_strings: true,
2378 allow_triple_quote_strings: false,
2379 allow_csharp_verbatim_strings: false,
2380 allow_raw_strings: false,
2381 symbol_patterns: SP_NONE,
2382 has_preprocessor: false,
2383};
2384
2385static LANG_SCAN_TABLE: &[(Language, StaticLangConfig)] = &[
2389 (
2391 Language::C,
2392 StaticLangConfig {
2393 symbol_patterns: SP_C,
2394 has_preprocessor: true,
2395 ..C_SLASH_BASE
2396 },
2397 ),
2398 (
2399 Language::Cpp,
2400 StaticLangConfig {
2401 symbol_patterns: SP_CPP,
2402 has_preprocessor: true,
2403 ..C_SLASH_BASE
2404 },
2405 ),
2406 (
2407 Language::ObjectiveC,
2408 StaticLangConfig {
2409 symbol_patterns: SP_OBJECTIVEC,
2410 has_preprocessor: true,
2411 ..C_SLASH_BASE
2412 },
2413 ),
2414 (
2416 Language::CSharp,
2417 StaticLangConfig {
2418 symbol_patterns: SP_CSHARP,
2419 allow_csharp_verbatim_strings: true,
2420 ..C_SLASH_BASE
2421 },
2422 ),
2423 (
2424 Language::Go,
2425 StaticLangConfig {
2426 symbol_patterns: SP_GO,
2427 ..C_SLASH_BASE
2428 },
2429 ),
2430 (
2431 Language::Java,
2432 StaticLangConfig {
2433 symbol_patterns: SP_JAVA,
2434 ..C_SLASH_BASE
2435 },
2436 ),
2437 (
2438 Language::JavaScript,
2439 StaticLangConfig {
2440 symbol_patterns: SP_JS,
2441 ..C_SLASH_BASE
2442 },
2443 ),
2444 (
2445 Language::TypeScript,
2446 StaticLangConfig {
2447 symbol_patterns: SP_TS,
2448 ..C_SLASH_BASE
2449 },
2450 ),
2451 (
2452 Language::Svelte,
2453 StaticLangConfig {
2454 symbol_patterns: SP_JS,
2455 ..C_SLASH_BASE
2456 },
2457 ),
2458 (
2459 Language::Vue,
2460 StaticLangConfig {
2461 symbol_patterns: SP_JS,
2462 ..C_SLASH_BASE
2463 },
2464 ),
2465 (
2466 Language::Dart,
2467 StaticLangConfig {
2468 symbol_patterns: SP_DART,
2469 ..C_SLASH_BASE
2470 },
2471 ),
2472 (
2473 Language::Groovy,
2474 StaticLangConfig {
2475 symbol_patterns: SP_GROOVY,
2476 ..C_SLASH_BASE
2477 },
2478 ),
2479 (
2480 Language::Kotlin,
2481 StaticLangConfig {
2482 symbol_patterns: SP_KOTLIN,
2483 ..C_SLASH_BASE
2484 },
2485 ),
2486 (
2487 Language::Scala,
2488 StaticLangConfig {
2489 symbol_patterns: SP_SCALA,
2490 ..C_SLASH_BASE
2491 },
2492 ),
2493 (
2494 Language::Scss,
2495 StaticLangConfig {
2496 symbol_patterns: SP_NONE,
2497 ..C_SLASH_BASE
2498 },
2499 ),
2500 (
2502 Language::Rust,
2503 StaticLangConfig {
2504 symbol_patterns: SP_RUST,
2505 allow_single_quote_strings: false,
2506 allow_raw_strings: true,
2507 ..C_SLASH_BASE
2508 },
2509 ),
2510 (
2512 Language::Swift,
2513 StaticLangConfig {
2514 symbol_patterns: SP_SWIFT,
2515 allow_single_quote_strings: false,
2516 ..C_SLASH_BASE
2517 },
2518 ),
2519 (
2521 Language::Zig,
2522 StaticLangConfig {
2523 symbol_patterns: SP_ZIG,
2524 block_comment: None,
2525 ..C_SLASH_BASE
2526 },
2527 ),
2528 (
2530 Language::FSharp,
2531 StaticLangConfig {
2532 line_comments: &["//"],
2533 block_comment: Some(("(*", "*)")),
2534 allow_single_quote_strings: false,
2535 allow_double_quote_strings: true,
2536 symbol_patterns: SP_FSHARP,
2537 ..C_SLASH_BASE
2538 },
2539 ),
2540 (
2542 Language::Shell,
2543 StaticLangConfig {
2544 symbol_patterns: SP_SHELL,
2545 ..HASH_BASE
2546 },
2547 ),
2548 (
2549 Language::Elixir,
2550 StaticLangConfig {
2551 symbol_patterns: SP_ELIXIR,
2552 ..HASH_BASE
2553 },
2554 ),
2555 (
2556 Language::Perl,
2557 StaticLangConfig {
2558 symbol_patterns: SP_PERL,
2559 ..HASH_BASE
2560 },
2561 ),
2562 (
2563 Language::R,
2564 StaticLangConfig {
2565 symbol_patterns: SP_R,
2566 ..HASH_BASE
2567 },
2568 ),
2569 (
2570 Language::Ruby,
2571 StaticLangConfig {
2572 symbol_patterns: SP_RUBY,
2573 ..HASH_BASE
2574 },
2575 ),
2576 (
2578 Language::Python,
2579 StaticLangConfig {
2580 symbol_patterns: SP_PYTHON,
2581 allow_triple_quote_strings: true,
2582 ..HASH_BASE
2583 },
2584 ),
2585 (
2587 Language::PowerShell,
2588 StaticLangConfig {
2589 symbol_patterns: SP_POWERSHELL,
2590 block_comment: Some(("<#", "#>")),
2591 ..HASH_BASE
2592 },
2593 ),
2594 (
2596 Language::Nim,
2597 StaticLangConfig {
2598 symbol_patterns: SP_NIM,
2599 block_comment: Some(("#[", "]#")),
2600 ..HASH_BASE
2601 },
2602 ),
2603 (
2605 Language::Makefile,
2606 StaticLangConfig {
2607 symbol_patterns: SP_NONE,
2608 allow_single_quote_strings: false,
2609 allow_double_quote_strings: false,
2610 ..HASH_BASE
2611 },
2612 ),
2613 (
2614 Language::Dockerfile,
2615 StaticLangConfig {
2616 symbol_patterns: SP_NONE,
2617 allow_single_quote_strings: false,
2618 allow_double_quote_strings: false,
2619 ..HASH_BASE
2620 },
2621 ),
2622 (
2625 Language::Css,
2626 StaticLangConfig {
2627 line_comments: &[],
2628 block_comment: Some(("/*", "*/")),
2629 symbol_patterns: SP_NONE,
2630 ..C_SLASH_BASE
2631 },
2632 ),
2633 (
2635 Language::Html,
2636 StaticLangConfig {
2637 line_comments: &[],
2638 block_comment: Some(("<!--", "-->")),
2639 allow_single_quote_strings: false,
2640 allow_double_quote_strings: false,
2641 symbol_patterns: SP_NONE,
2642 ..C_SLASH_BASE
2643 },
2644 ),
2645 (
2646 Language::Xml,
2647 StaticLangConfig {
2648 line_comments: &[],
2649 block_comment: Some(("<!--", "-->")),
2650 allow_single_quote_strings: false,
2651 allow_double_quote_strings: false,
2652 symbol_patterns: SP_NONE,
2653 ..C_SLASH_BASE
2654 },
2655 ),
2656 (
2658 Language::Lua,
2659 StaticLangConfig {
2660 line_comments: &["--"],
2661 block_comment: Some(("--[[", "]]")),
2662 symbol_patterns: SP_LUA,
2663 ..C_SLASH_BASE
2664 },
2665 ),
2666 (
2668 Language::Haskell,
2669 StaticLangConfig {
2670 line_comments: &["--"],
2671 block_comment: Some(("{-", "-}")),
2672 symbol_patterns: SP_HASKELL,
2673 ..C_SLASH_BASE
2674 },
2675 ),
2676 (
2678 Language::Sql,
2679 StaticLangConfig {
2680 line_comments: &["--"],
2681 block_comment: Some(("/*", "*/")),
2682 allow_single_quote_strings: true,
2683 allow_double_quote_strings: false,
2684 symbol_patterns: SP_SQL,
2685 ..C_SLASH_BASE
2686 },
2687 ),
2688 (
2690 Language::Ocaml,
2691 StaticLangConfig {
2692 line_comments: &[],
2693 block_comment: Some(("(*", "*)")),
2694 allow_single_quote_strings: false,
2695 symbol_patterns: SP_OCAML,
2696 ..C_SLASH_BASE
2697 },
2698 ),
2699 (
2705 Language::Assembly,
2706 StaticLangConfig {
2707 line_comments: &[";"],
2708 block_comment: Some(("/*", "*/")),
2709 allow_single_quote_strings: false,
2710 allow_double_quote_strings: true,
2711 symbol_patterns: SP_ASSEMBLY,
2712 ..C_SLASH_BASE
2713 },
2714 ),
2715 (
2716 Language::Clojure,
2717 StaticLangConfig {
2718 line_comments: &[";"],
2719 block_comment: None,
2720 allow_single_quote_strings: false,
2721 symbol_patterns: SP_CLOJURE,
2722 ..C_SLASH_BASE
2723 },
2724 ),
2725 (
2727 Language::Erlang,
2728 StaticLangConfig {
2729 line_comments: &["%"],
2730 block_comment: None,
2731 allow_single_quote_strings: false,
2732 symbol_patterns: SP_ERLANG,
2733 ..C_SLASH_BASE
2734 },
2735 ),
2736 (
2738 Language::Php,
2739 StaticLangConfig {
2740 line_comments: &["//", "#"],
2741 block_comment: Some(("/*", "*/")),
2742 symbol_patterns: SP_PHP,
2743 ..C_SLASH_BASE
2744 },
2745 ),
2746 (
2748 Language::Julia,
2749 StaticLangConfig {
2750 line_comments: &["#"],
2751 block_comment: Some(("#=", "=#")),
2752 allow_single_quote_strings: false,
2753 allow_triple_quote_strings: true,
2754 symbol_patterns: SP_JULIA,
2755 ..C_SLASH_BASE
2756 },
2757 ),
2758 (
2761 Language::Solidity,
2762 StaticLangConfig {
2763 symbol_patterns: SP_SOLIDITY,
2764 ..C_SLASH_BASE
2765 },
2766 ),
2767 (
2769 Language::Protobuf,
2770 StaticLangConfig {
2771 symbol_patterns: SP_PROTOBUF,
2772 ..C_SLASH_BASE
2773 },
2774 ),
2775 (
2777 Language::Hcl,
2778 StaticLangConfig {
2779 line_comments: &["#", "//"],
2780 allow_single_quote_strings: false,
2781 symbol_patterns: SP_NONE,
2782 ..C_SLASH_BASE
2783 },
2784 ),
2785 (
2787 Language::GraphQl,
2788 StaticLangConfig {
2789 allow_single_quote_strings: false,
2790 allow_triple_quote_strings: true,
2791 symbol_patterns: SP_NONE,
2792 ..HASH_BASE
2793 },
2794 ),
2795 (
2798 Language::Ada,
2799 StaticLangConfig {
2800 line_comments: &["--"],
2801 block_comment: None,
2802 allow_single_quote_strings: false,
2803 symbol_patterns: SP_ADA,
2804 ..C_SLASH_BASE
2805 },
2806 ),
2807 (
2809 Language::Vhdl,
2810 StaticLangConfig {
2811 line_comments: &["--"],
2812 block_comment: None,
2813 allow_single_quote_strings: false,
2814 symbol_patterns: SP_VHDL,
2815 ..C_SLASH_BASE
2816 },
2817 ),
2818 (
2820 Language::Verilog,
2821 StaticLangConfig {
2822 allow_single_quote_strings: false,
2823 symbol_patterns: SP_VERILOG,
2824 ..C_SLASH_BASE
2825 },
2826 ),
2827 (
2829 Language::Tcl,
2830 StaticLangConfig {
2831 allow_single_quote_strings: false,
2832 symbol_patterns: SP_TCL,
2833 ..HASH_BASE
2834 },
2835 ),
2836 (
2838 Language::Pascal,
2839 StaticLangConfig {
2840 line_comments: &["//"],
2841 block_comment: Some(("{", "}")),
2842 allow_single_quote_strings: true,
2843 allow_double_quote_strings: false,
2844 symbol_patterns: SP_PASCAL,
2845 ..C_SLASH_BASE
2846 },
2847 ),
2848 (
2850 Language::VisualBasic,
2851 StaticLangConfig {
2852 line_comments: &["'"],
2853 block_comment: None,
2854 allow_single_quote_strings: false,
2855 allow_double_quote_strings: true,
2856 symbol_patterns: SP_VB,
2857 ..C_SLASH_BASE
2858 },
2859 ),
2860 (
2862 Language::Lisp,
2863 StaticLangConfig {
2864 line_comments: &[";"],
2865 block_comment: Some(("#|", "|#")),
2866 allow_single_quote_strings: false,
2867 symbol_patterns: SP_LISP,
2868 ..C_SLASH_BASE
2869 },
2870 ),
2871 (
2874 Language::Fortran,
2875 StaticLangConfig {
2876 line_comments: &["!"],
2877 block_comment: None,
2878 symbol_patterns: SP_FORTRAN,
2879 ..C_SLASH_BASE
2880 },
2881 ),
2882 (
2884 Language::Nix,
2885 StaticLangConfig {
2886 block_comment: Some(("/*", "*/")),
2887 allow_single_quote_strings: false,
2888 symbol_patterns: SP_NONE,
2889 ..HASH_BASE
2890 },
2891 ),
2892 (
2894 Language::Crystal,
2895 StaticLangConfig {
2896 symbol_patterns: SP_CRYSTAL,
2897 ..HASH_BASE
2898 },
2899 ),
2900 (
2902 Language::D,
2903 StaticLangConfig {
2904 symbol_patterns: SP_D,
2905 ..C_SLASH_BASE
2906 },
2907 ),
2908 (
2910 Language::Glsl,
2911 StaticLangConfig {
2912 allow_single_quote_strings: false,
2913 symbol_patterns: SP_NONE,
2914 ..C_SLASH_BASE
2915 },
2916 ),
2917 (
2919 Language::Cmake,
2920 StaticLangConfig {
2921 block_comment: Some(("#[[", "]]")),
2922 allow_single_quote_strings: false,
2923 symbol_patterns: SP_CMAKE,
2924 ..HASH_BASE
2925 },
2926 ),
2927 (
2929 Language::Elm,
2930 StaticLangConfig {
2931 line_comments: &["--"],
2932 block_comment: Some(("{-", "-}")),
2933 allow_single_quote_strings: false,
2934 symbol_patterns: SP_ELM,
2935 ..C_SLASH_BASE
2936 },
2937 ),
2938 (
2940 Language::Awk,
2941 StaticLangConfig {
2942 allow_single_quote_strings: false,
2943 symbol_patterns: SP_AWK,
2944 ..HASH_BASE
2945 },
2946 ),
2947];
2948
2949#[derive(Debug, Clone, Copy)]
2952struct IeeeFlags {
2953 has_preprocessor_directives: bool,
2955 blank_in_block_comment_as_comment: bool,
2957 collapse_continuation_lines: bool,
2959}
2960
2961#[derive(Debug, Clone, Copy)]
2962enum StringState {
2963 Single(char),
2964 Triple(&'static str),
2965 VerbatimDouble,
2966 RawHash(usize),
2969}
2970
2971#[allow(clippy::struct_excessive_bools)]
2972#[derive(Debug, Default)]
2973struct LineFacts {
2974 has_code: bool,
2975 has_single_comment: bool,
2976 has_multi_comment: bool,
2977 has_docstring: bool,
2978}
2979
2980fn process_string_char(
2984 state: StringState,
2985 chars: &[char],
2986 i: usize,
2987) -> (Option<StringState>, usize) {
2988 match state {
2989 StringState::Single(delim) => step_single(state, delim, chars, i),
2990 StringState::Triple(delim) => step_triple(state, delim, chars, i),
2991 StringState::VerbatimDouble => step_verbatim(state, chars, i),
2992 StringState::RawHash(hashes) => step_raw_hash(state, hashes, chars, i),
2993 }
2994}
2995
2996fn step_single(
2998 state: StringState,
2999 delim: char,
3000 chars: &[char],
3001 i: usize,
3002) -> (Option<StringState>, usize) {
3003 if chars[i] == '\\' {
3004 return (Some(state), 2); }
3006 if chars[i] == delim {
3007 (None, 1)
3008 } else {
3009 (Some(state), 1)
3010 }
3011}
3012
3013fn step_triple(
3015 state: StringState,
3016 delim: &'static str,
3017 chars: &[char],
3018 i: usize,
3019) -> (Option<StringState>, usize) {
3020 if starts_with(chars, i, delim) {
3021 (None, delim.len())
3022 } else {
3023 (Some(state), 1)
3024 }
3025}
3026
3027fn step_verbatim(state: StringState, chars: &[char], i: usize) -> (Option<StringState>, usize) {
3029 if starts_with(chars, i, "\"\"") {
3030 return (Some(state), 2); }
3032 if chars[i] == '"' {
3033 (None, 1)
3034 } else {
3035 (Some(state), 1)
3036 }
3037}
3038
3039fn step_raw_hash(
3041 state: StringState,
3042 hashes: usize,
3043 chars: &[char],
3044 i: usize,
3045) -> (Option<StringState>, usize) {
3046 if chars[i] == '"' && count_leading_hashes(chars, i + 1) >= hashes {
3047 (None, 1 + hashes)
3048 } else {
3049 (Some(state), 1)
3050 }
3051}
3052
3053fn count_leading_hashes(chars: &[char], index: usize) -> usize {
3055 let mut n = 0;
3056 while chars.get(index + n) == Some(&'#') {
3057 n += 1;
3058 }
3059 n
3060}
3061
3062fn try_open_raw_string(chars: &[char], i: usize) -> Option<(usize, usize)> {
3067 let word_before = i
3068 .checked_sub(1)
3069 .and_then(|p| chars.get(p))
3070 .is_some_and(|c| c.is_alphanumeric() || *c == '_');
3071 if word_before {
3072 return None;
3073 }
3074 let mut j = i;
3075 if chars.get(j) == Some(&'b') {
3076 j += 1; }
3078 if chars.get(j) != Some(&'r') {
3079 return None;
3080 }
3081 j += 1;
3082 let hashes = count_leading_hashes(chars, j);
3083 j += hashes;
3084 if chars.get(j) != Some(&'"') {
3085 return None;
3086 }
3087 Some((hashes, j + 1 - i))
3088}
3089
3090fn process_block_comment_char(chars: &[char], i: usize, close: &str) -> (bool, usize) {
3094 if starts_with(chars, i, close) {
3095 (false, close.len())
3096 } else {
3097 (true, 1)
3098 }
3099}
3100
3101fn try_open_string(chars: &[char], i: usize, config: &ScanConfig) -> Option<(StringState, usize)> {
3105 if config.allow_raw_strings {
3106 if let Some((hashes, advance)) = try_open_raw_string(chars, i) {
3107 return Some((StringState::RawHash(hashes), advance));
3108 }
3109 }
3110 if config.allow_csharp_verbatim_strings && starts_with(chars, i, "@\"") {
3111 return Some((StringState::VerbatimDouble, 2));
3112 }
3113 if config.allow_triple_quote_strings {
3114 if starts_with(chars, i, "\"\"\"") {
3115 return Some((StringState::Triple("\"\"\""), 3));
3116 }
3117 if starts_with(chars, i, "'''") {
3118 return Some((StringState::Triple("'''"), 3));
3119 }
3120 }
3121 if config.allow_single_quote_strings && chars[i] == '\'' {
3122 return Some((StringState::Single('\''), 1));
3123 }
3124 if config.allow_double_quote_strings && chars[i] == '"' {
3125 return Some((StringState::Single('"'), 1));
3126 }
3127 None
3128}
3129
3130fn step_through_block_comment(
3136 chars: &[char],
3137 i: usize,
3138 block_comment: Option<(&'static str, &'static str)>,
3139 in_block_comment: &mut bool,
3140) -> usize {
3141 if let Some((_, close)) = block_comment {
3142 let (still_in, advance) = process_block_comment_char(chars, i, close);
3143 *in_block_comment = still_in;
3144 return advance;
3145 }
3146 0
3147}
3148
3149fn try_open_block_comment(
3152 chars: &[char],
3153 i: usize,
3154 block_comment: Option<(&'static str, &'static str)>,
3155) -> Option<usize> {
3156 let (open, _) = block_comment?;
3157 starts_with(chars, i, open).then_some(open.len())
3158}
3159
3160fn advance_inside_span(
3164 chars: &[char],
3165 i: usize,
3166 config: &ScanConfig,
3167 facts: &mut LineFacts,
3168 in_block_comment: &mut bool,
3169 string_state: &mut Option<StringState>,
3170 code: &mut Vec<u8>,
3171) -> Option<usize> {
3172 if let Some(state) = *string_state {
3174 facts.has_code = true;
3175 let (new_state, advance) = process_string_char(state, chars, i);
3176 *string_state = new_state;
3177 blank_mask(code, advance);
3178 return Some(advance);
3179 }
3180
3181 if *in_block_comment {
3183 facts.has_multi_comment = true;
3184 let advance = step_through_block_comment(chars, i, config.block_comment, in_block_comment);
3185 blank_mask(code, advance);
3186 return Some(advance);
3187 }
3188
3189 None
3190}
3191
3192fn scan_line(
3196 chars: &[char],
3197 config: &ScanConfig,
3198 facts: &mut LineFacts,
3199 in_block_comment: &mut bool,
3200 string_state: &mut Option<StringState>,
3201 code: &mut Vec<u8>,
3202) {
3203 let mut i = 0usize;
3204 while i < chars.len() {
3205 if let Some(advance) = advance_inside_span(
3207 chars,
3208 i,
3209 config,
3210 facts,
3211 in_block_comment,
3212 string_state,
3213 code,
3214 ) {
3215 i += advance;
3216 continue;
3217 }
3218
3219 if chars[i].is_whitespace() {
3221 code.push(b' ');
3222 i += 1;
3223 continue;
3224 }
3225
3226 if let Some((new_state, advance)) = try_open_string(chars, i, config) {
3228 facts.has_code = true;
3229 *string_state = Some(new_state);
3230 blank_mask(code, advance);
3231 i += advance;
3232 continue;
3233 }
3234
3235 if let Some(advance) = try_open_block_comment(chars, i, config.block_comment) {
3237 facts.has_multi_comment = true;
3238 *in_block_comment = true;
3239 blank_mask(code, advance);
3240 i += advance;
3241 continue;
3242 }
3243
3244 if config
3246 .line_comments
3247 .iter()
3248 .any(|prefix| starts_with(chars, i, prefix))
3249 {
3250 facts.has_single_comment = true;
3251 break;
3252 }
3253
3254 facts.has_code = true;
3257 let ch = chars[i];
3258 code.push(if ch.is_ascii() { ch as u8 } else { b' ' });
3259 i += 1;
3260 }
3261}
3262
3263fn blank_mask(code: &mut Vec<u8>, n: usize) {
3266 code.resize(code.len() + n, b' ');
3267}
3268
3269fn finalize_line_facts(
3274 facts: LineFacts,
3275 trimmed: &str,
3276 raw: &mut RawLineCounts,
3277 ieee: IeeeFlags,
3278 in_block_comment: bool,
3279 string_state: Option<StringState>,
3280 pending_continuation: &mut Option<LineFacts>,
3281) -> Option<LineFacts> {
3282 if ieee.has_preprocessor_directives
3286 && facts.has_code
3287 && !facts.has_single_comment
3288 && !facts.has_multi_comment
3289 && trimmed.starts_with('#')
3290 {
3291 raw.compiler_directive_lines += 1;
3292 }
3293
3294 let is_continuation = ieee.collapse_continuation_lines
3297 && !in_block_comment
3298 && string_state.is_none()
3299 && trimmed.ends_with('\\');
3300
3301 if is_continuation {
3302 let pending = pending_continuation.get_or_insert_with(LineFacts::default);
3303 pending.has_code |= facts.has_code;
3304 pending.has_single_comment |= facts.has_single_comment;
3305 pending.has_multi_comment |= facts.has_multi_comment;
3306 pending.has_docstring |= facts.has_docstring;
3307 return None; }
3309
3310 let emit = if let Some(pending) = pending_continuation.take() {
3312 LineFacts {
3313 has_code: pending.has_code | facts.has_code,
3314 has_single_comment: pending.has_single_comment | facts.has_single_comment,
3315 has_multi_comment: pending.has_multi_comment | facts.has_multi_comment,
3316 has_docstring: pending.has_docstring | facts.has_docstring,
3317 }
3318 } else {
3319 facts
3320 };
3321 Some(emit)
3322}
3323
3324#[allow(clippy::needless_pass_by_value)]
3329#[allow(clippy::too_many_arguments)]
3330#[allow(clippy::many_single_char_names)] fn process_physical_line(
3332 line: &str,
3333 line_idx: usize,
3334 config: &ScanConfig,
3335 raw: &mut RawLineCounts,
3336 in_block_comment: &mut bool,
3337 string_state: &mut Option<StringState>,
3338 pending_continuation: &mut Option<LineFacts>,
3339 ieee: IeeeFlags,
3340 scope: &mut CScopeState,
3341) {
3342 raw.total_physical_lines += 1;
3343
3344 if config.skip_lines.contains(&line_idx) {
3345 raw.docstring_comment_lines += 1;
3346 return;
3347 }
3348
3349 let trimmed = line.trim();
3350 let mut facts = LineFacts::default();
3351
3352 if *in_block_comment && (ieee.blank_in_block_comment_as_comment || !trimmed.is_empty()) {
3356 facts.has_multi_comment = true;
3357 }
3358
3359 let chars: Vec<char> = line.chars().collect();
3360 let mut code_mask: Vec<u8> = Vec::with_capacity(chars.len());
3365 scan_line(
3366 &chars,
3367 config,
3368 &mut facts,
3369 in_block_comment,
3370 string_state,
3371 &mut code_mask,
3372 );
3373
3374 let Some(emit) = finalize_line_facts(
3375 facts,
3376 trimmed,
3377 raw,
3378 ieee,
3379 *in_block_comment,
3380 *string_state,
3381 pending_continuation,
3382 ) else {
3383 return;
3384 };
3385
3386 classify_line(raw, &emit, trimmed);
3387
3388 if emit.has_code {
3389 accumulate_code_line(raw, config, trimmed, scope, &code_mask);
3390 }
3391}
3392
3393#[allow(clippy::many_single_char_names)] fn accumulate_code_line(
3397 raw: &mut RawLineCounts,
3398 config: &ScanConfig,
3399 trimmed: &str,
3400 scope: &mut CScopeState,
3401 code_mask: &[u8],
3402) {
3403 use std::hash::{DefaultHasher, Hash, Hasher};
3404 let (f, c, v, i, t, a, s) = count_symbols(&config.symbol_patterns, trimmed);
3405 raw.functions += f;
3406 raw.classes += c;
3407 raw.variables += v;
3408 raw.imports += i;
3409 raw.test_count += t;
3410 raw.test_assertion_count += a;
3411 raw.test_suite_count += s;
3412
3413 if !config.symbol_patterns.functions_prefix_paren.is_empty() {
3417 accumulate_c_family(raw, v, trimmed, scope);
3418 }
3419
3420 raw.cyclomatic_complexity += count_branch_in_line(code_mask, config.branch_keywords);
3423
3424 accumulate_lsloc(raw, trimmed, config.lsloc_strategy);
3426
3427 let mut h = DefaultHasher::new();
3429 trimmed.hash(&mut h);
3430 raw.code_line_hashes.push(h.finish());
3431}
3432
3433fn accumulate_c_family(
3436 raw: &mut RawLineCounts,
3437 var_count: u64,
3438 trimmed: &str,
3439 scope: &mut CScopeState,
3440) {
3441 if var_count == 1 {
3442 match scope.current_var_kind() {
3443 VarKind::Member => raw.variables_member += 1,
3444 VarKind::Local => raw.variables_local += 1,
3445 VarKind::Global => raw.variables_global += 1,
3446 }
3447 }
3448 if is_object_like_macro(trimmed) {
3449 raw.macro_definitions += 1;
3450 }
3451 scope.update(trimmed);
3452}
3453
3454fn accumulate_lsloc(raw: &mut RawLineCounts, trimmed: &str, strategy: LslocStrategy) {
3456 match strategy {
3457 LslocStrategy::Semicolons => {
3458 let semi =
3459 u32::try_from(trimmed.bytes().filter(|&b| b == b';').count()).unwrap_or(u32::MAX);
3460 *raw.lsloc.get_or_insert(0) += semi;
3461 }
3462 LslocStrategy::NonContinuationNewlines => {
3463 let cont = trimmed.ends_with('\\')
3464 || trimmed.ends_with(',')
3465 || trimmed.ends_with('(')
3466 || trimmed.ends_with('[')
3467 || trimmed.ends_with('{');
3468 if !cont {
3469 *raw.lsloc.get_or_insert(0) += 1;
3470 }
3471 }
3472 LslocStrategy::Unsupported => {}
3473 }
3474}
3475
3476#[allow(clippy::needless_pass_by_value)]
3477fn analyze_generic(text: &str, config: ScanConfig, ieee: IeeeFlags) -> RawFileAnalysis {
3478 let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
3479 let lines: Vec<&str> = normalized.split_terminator('\n').collect();
3480
3481 let mut raw = RawLineCounts::default();
3482 let mut warnings = Vec::new();
3483
3484 let mut in_block_comment = false;
3485 let mut string_state: Option<StringState> = None;
3486 let mut pending_continuation: Option<LineFacts> = None;
3488 let mut scope = CScopeState::default();
3490
3491 for (line_idx, line) in lines.iter().enumerate() {
3492 process_physical_line(
3493 line,
3494 line_idx,
3495 &config,
3496 &mut raw,
3497 &mut in_block_comment,
3498 &mut string_state,
3499 &mut pending_continuation,
3500 ieee,
3501 &mut scope,
3502 );
3503 }
3504
3505 if let Some(pending) = pending_continuation.take() {
3507 classify_line(&mut raw, &pending, "");
3508 }
3509
3510 if in_block_comment {
3511 warnings.push("unclosed block comment detected; result is best effort".into());
3512 }
3513 if string_state.is_some() {
3514 warnings.push("unclosed string literal detected; result is best effort".into());
3515 }
3516
3517 RawFileAnalysis {
3518 raw,
3519 parse_mode: if warnings.is_empty() {
3520 ParseMode::Lexical
3521 } else {
3522 ParseMode::LexicalBestEffort
3523 },
3524 warnings,
3525 style_analysis: None,
3526 }
3527}
3528
3529const fn classify_line(raw: &mut RawLineCounts, facts: &LineFacts, trimmed: &str) {
3530 if facts.has_docstring {
3531 raw.docstring_comment_lines += 1;
3532 } else if !facts.has_code
3533 && !facts.has_single_comment
3534 && !facts.has_multi_comment
3535 && trimmed.is_empty()
3536 {
3537 raw.blank_only_lines += 1;
3538 } else if facts.has_code && facts.has_single_comment {
3539 raw.mixed_code_single_comment_lines += 1;
3540 } else if facts.has_code && facts.has_multi_comment {
3541 raw.mixed_code_multi_comment_lines += 1;
3542 } else if facts.has_code {
3543 raw.code_only_lines += 1;
3544 } else if facts.has_single_comment {
3545 raw.single_comment_only_lines += 1;
3546 } else if facts.has_multi_comment {
3547 raw.multi_comment_only_lines += 1;
3548 } else if trimmed.is_empty() {
3549 raw.blank_only_lines += 1;
3550 } else {
3551 raw.skipped_unknown_lines += 1;
3552 }
3553}
3554
3555fn prefix_hit(pats: &[&str], trimmed: &str) -> u64 {
3557 u64::from(pats.iter().any(|p| trimmed.starts_with(p)))
3558}
3559
3560fn fn_prefix_paren_hit(patterns: &SymbolPatterns, trimmed: &str) -> u64 {
3563 if patterns.functions_prefix_paren.is_empty() {
3564 return 0;
3565 }
3566 let Some(paren_pos) = trimmed.find('(') else {
3567 return 0;
3568 };
3569 if trimmed[..paren_pos].contains('=') {
3570 0
3571 } else {
3572 prefix_hit(patterns.functions_prefix_paren, trimmed)
3573 }
3574}
3575
3576fn var_prefix_no_paren_hit(patterns: &SymbolPatterns, trimmed: &str) -> u64 {
3579 if patterns.variables_prefix_no_paren.is_empty()
3580 || prefix_hit(patterns.variables_prefix_no_paren, trimmed) == 0
3581 {
3582 return 0;
3583 }
3584 trimmed
3585 .find('(')
3586 .map_or(1, |pp| u64::from(trimmed[..pp].contains('=')))
3587}
3588
3589const C_STMT_KEYWORDS: &[&str] = &[
3592 "if",
3593 "for",
3594 "while",
3595 "switch",
3596 "return",
3597 "catch",
3598 "sizeof",
3599 "do",
3600 "else",
3601 "case",
3602 "throw",
3603 "goto",
3604 "using",
3605 "namespace",
3606 "typedef",
3607 "friend",
3608 "decltype",
3609 "alignof",
3610 "new",
3611 "delete",
3612 "static_assert",
3613 "template",
3614 "co_await",
3615 "co_return",
3616 "co_yield",
3617 "assert",
3618 "default",
3619 "class",
3620 "struct",
3621 "union",
3622 "enum",
3623 "public",
3624 "private",
3625 "protected",
3626 "try",
3627];
3628
3629const fn c_type_char_ok(c: char) -> bool {
3632 c.is_ascii_alphanumeric()
3633 || matches!(
3634 c,
3635 '_' | ':' | '<' | '>' | '*' | '&' | '~' | ' ' | '\t' | ','
3636 )
3637}
3638
3639fn c_name_is_identifier(name: &str) -> bool {
3642 let core = name.trim_start_matches(['*', '&', '~']);
3643 let seg = core.rsplit("::").next().unwrap_or(core);
3644 let mut chars = seg.chars();
3645 match chars.next() {
3646 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
3647 _ => return false,
3648 }
3649 seg.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
3650}
3651
3652fn looks_like_c_function(trimmed: &str) -> u64 {
3660 let Some(paren) = trimmed.find('(') else {
3661 return 0;
3662 };
3663 let pre = trimmed[..paren].trim();
3664 if pre.is_empty() || pre.contains("->") || !pre.chars().all(c_type_char_ok) {
3668 return 0;
3669 }
3670 let mut toks = pre.split_whitespace();
3672 let Some(first) = toks.next() else {
3673 return 0;
3674 };
3675 if C_STMT_KEYWORDS.contains(&first) || toks.next().is_none() {
3676 return 0;
3677 }
3678 let Some(name) = pre.split_whitespace().next_back() else {
3679 return 0;
3680 };
3681 if !c_name_is_identifier(name) {
3682 return 0;
3683 }
3684 if trimmed.ends_with(';') {
3689 let args = trimmed[paren + 1..]
3690 .rsplit_once(')')
3691 .map_or("", |(a, _)| a)
3692 .trim();
3693 let looks_like_params = args.is_empty()
3694 || args.contains(',')
3695 || args.contains('&')
3696 || args.contains('*')
3697 || args.contains("::")
3698 || args.split_whitespace().count() >= 2;
3699 if !looks_like_params {
3700 return 0;
3701 }
3702 }
3703 1
3704}
3705
3706fn looks_like_c_variable(trimmed: &str) -> u64 {
3712 let Some(delim_pos) = trimmed.find(['=', ';', '{', '(']) else {
3717 return 0;
3718 };
3719 let head = trimmed[..delim_pos].trim();
3720 if head.is_empty() || head.contains("->") || !head.chars().all(c_type_char_ok) {
3721 return 0;
3722 }
3723 let mut toks = head.split_whitespace();
3724 let Some(first) = toks.next() else {
3725 return 0;
3726 };
3727 if C_STMT_KEYWORDS.contains(&first) || toks.next().is_none() {
3728 return 0;
3729 }
3730 let Some(name) = head.split_whitespace().next_back() else {
3731 return 0;
3732 };
3733 u64::from(c_name_is_identifier(name))
3734}
3735
3736#[derive(Clone, Copy, PartialEq, Eq)]
3739enum CScope {
3740 Aggregate,
3742 Function,
3744 Namespace,
3746 Block,
3748}
3749
3750#[derive(Clone, Copy)]
3752enum VarKind {
3753 Member,
3754 Local,
3755 Global,
3756}
3757
3758#[derive(Default)]
3760struct CScopeState {
3761 stack: Vec<CScope>,
3762 pending: Option<CScope>,
3765}
3766
3767impl CScopeState {
3768 fn current_var_kind(&self) -> VarKind {
3770 match self.stack.last() {
3771 Some(CScope::Aggregate) => VarKind::Member,
3772 Some(CScope::Function | CScope::Block) => VarKind::Local,
3773 _ => VarKind::Global, }
3775 }
3776
3777 fn update(&mut self, trimmed: &str) {
3780 if let Some(kind) = c_line_scope_kind(trimmed) {
3781 self.pending = Some(kind);
3782 }
3783 let bytes = trimmed.as_bytes();
3784 let mut i = 0;
3785 let mut in_str: Option<u8> = None;
3786 while i < bytes.len() {
3787 if let Some(next) = skip_string_literal(bytes, i, &mut in_str) {
3789 i = next;
3790 continue;
3791 }
3792 let b = bytes[i];
3793 match b {
3794 b'"' | b'\'' => in_str = Some(b),
3795 b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'/' => break, b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'*' => {
3797 match trimmed[i + 2..].find("*/") {
3799 Some(off) => i += 2 + off + 2,
3800 None => break,
3801 }
3802 continue;
3803 }
3804 b'{' => self
3805 .stack
3806 .push(self.pending.take().unwrap_or(CScope::Block)),
3807 b'}' => {
3808 self.stack.pop();
3809 }
3810 _ => {}
3811 }
3812 i += 1;
3813 }
3814 }
3815}
3816
3817fn skip_string_literal(bytes: &[u8], i: usize, in_str: &mut Option<u8>) -> Option<usize> {
3821 let q = (*in_str)?;
3822 let b = bytes[i];
3823 if b == b'\\' {
3824 return Some(i + 2); }
3826 if b == q {
3827 *in_str = None;
3828 }
3829 Some(i + 1)
3830}
3831
3832fn c_line_scope_kind(trimmed: &str) -> Option<CScope> {
3836 if trimmed.contains(';') && !trimmed.contains('{') {
3838 return None;
3839 }
3840 if trimmed.starts_with("namespace") {
3841 return Some(CScope::Namespace);
3842 }
3843 if is_c_aggregate_opener(trimmed) {
3844 return Some(CScope::Aggregate);
3845 }
3846 if looks_like_c_function(trimmed) == 1 {
3847 return Some(CScope::Function);
3848 }
3849 None
3850}
3851
3852fn is_c_aggregate_opener(trimmed: &str) -> bool {
3855 if trimmed.contains('(') {
3856 return false;
3857 }
3858 trimmed
3859 .split(|c: char| !c.is_ascii_alphanumeric() && c != '_')
3860 .any(|tok| matches!(tok, "class" | "struct" | "union"))
3861}
3862
3863fn is_object_like_macro(trimmed: &str) -> bool {
3866 let Some(rest) = trimmed.strip_prefix('#') else {
3867 return false;
3868 };
3869 let Some(rest) = rest.trim_start().strip_prefix("define") else {
3870 return false;
3871 };
3872 if !rest.starts_with(char::is_whitespace) {
3873 return false; }
3875 let rest = rest.trim_start();
3876 let name_end = rest
3877 .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
3878 .unwrap_or(rest.len());
3879 if name_end == 0 {
3880 return false; }
3882 let after = &rest[name_end..];
3883 !after.starts_with('(') && !after.trim().is_empty()
3886}
3887
3888fn count_symbols(patterns: &SymbolPatterns, trimmed: &str) -> (u64, u64, u64, u64, u64, u64, u64) {
3889 let hit = |pats: &[&str]| prefix_hit(pats, trimmed);
3890 let c_style = !patterns.functions_prefix_paren.is_empty();
3898 let fn_extra = if c_style {
3899 looks_like_c_function(trimmed)
3900 } else {
3901 fn_prefix_paren_hit(patterns, trimmed)
3902 };
3903 let test_hit = hit(patterns.tests);
3904 let fn_hit = if test_hit == 0 {
3911 hit(patterns.functions) | fn_extra
3912 } else {
3913 0
3914 };
3915 let class_hit = if test_hit == 0 {
3916 hit(patterns.classes)
3917 } else {
3918 0
3919 };
3920 let var_hit = if c_style {
3921 if test_hit == 0 && fn_hit == 0 && class_hit == 0 {
3924 looks_like_c_variable(trimmed)
3925 } else {
3926 0
3927 }
3928 } else {
3929 hit(patterns.variables) | var_prefix_no_paren_hit(patterns, trimmed)
3930 };
3931 (
3932 fn_hit,
3933 class_hit,
3934 var_hit,
3935 hit(patterns.imports),
3936 test_hit,
3937 hit(patterns.assertions),
3938 hit(patterns.test_suites),
3939 )
3940}
3941
3942fn is_word_boundary(line: &[u8], start: usize, end: usize) -> bool {
3944 let before_ok =
3945 start == 0 || (!line[start - 1].is_ascii_alphanumeric() && line[start - 1] != b'_');
3946 let after_ok = end >= line.len() || (!line[end].is_ascii_alphanumeric() && line[end] != b'_');
3947 before_ok && after_ok
3948}
3949
3950fn keyword_matches_at(line: &[u8], i: usize, kw_bytes: &[u8], word_kw: bool) -> bool {
3952 if &line[i..i + kw_bytes.len()] != kw_bytes {
3953 return false;
3954 }
3955 !word_kw || is_word_boundary(line, i, i + kw_bytes.len())
3956}
3957
3958fn count_branch_in_line(line: &[u8], keywords: &[&str]) -> u32 {
3963 if keywords.is_empty() || line.is_empty() {
3964 return 0;
3965 }
3966 let mut total = 0u32;
3967 for &kw in keywords {
3968 let kw_bytes = kw.as_bytes();
3969 let word_kw = kw.bytes().all(|b| b.is_ascii_alphabetic() || b == b'_');
3970 let mut i = 0usize;
3971 while i + kw_bytes.len() <= line.len() {
3972 if keyword_matches_at(line, i, kw_bytes, word_kw) {
3973 total += 1;
3974 i += kw_bytes.len();
3975 } else {
3976 i += 1;
3977 }
3978 }
3979 }
3980 total
3981}
3982
3983fn starts_with(chars: &[char], index: usize, needle: &str) -> bool {
3984 let needle_chars: Vec<char> = needle.chars().collect();
3985 chars.get(index..index + needle_chars.len()) == Some(needle_chars.as_slice())
3986}
3987
3988#[derive(Debug, Clone)]
3989struct PyContext {
3990 indent: usize,
3991 expect_docstring: bool,
3992}
3993
3994fn py_pop_outdented_contexts(contexts: &mut Vec<PyContext>, indent: usize) {
3996 while contexts.len() > 1 && indent < contexts.last().map_or(0, |c| c.indent) {
3997 contexts.pop();
3998 }
3999}
4000
4001fn py_handle_pending_indent(
4004 pending_block_indent: &mut Option<usize>,
4005 contexts: &mut Vec<PyContext>,
4006 indent: usize,
4007 trimmed: &str,
4008) {
4009 let Some(base_indent) = *pending_block_indent else {
4010 return;
4011 };
4012 if indent > base_indent {
4013 contexts.push(PyContext {
4014 indent,
4015 expect_docstring: true,
4016 });
4017 *pending_block_indent = None;
4018 } else if !trimmed.starts_with('@') {
4019 *pending_block_indent = None;
4020 }
4021}
4022
4023fn py_try_record_docstring(
4029 ctx: &mut PyContext,
4030 trimmed: &str,
4031 idx: usize,
4032 docstring_lines: &mut HashSet<usize>,
4033 active_docstring: &mut Option<(&'static str, usize)>,
4034) -> bool {
4035 if !ctx.expect_docstring {
4036 return false;
4037 }
4038 if let Some(delim) = docstring_delimiter(trimmed) {
4039 docstring_lines.insert(idx);
4040 ctx.expect_docstring = false;
4041 if !closes_triple_docstring(trimmed, delim, true) {
4042 *active_docstring = Some((delim, idx));
4043 }
4044 return true;
4045 }
4046 ctx.expect_docstring = false;
4047 false
4048}
4049
4050fn track_active_docstring(
4054 active_docstring: &mut Option<(&'static str, usize)>,
4055 docstring_lines: &mut HashSet<usize>,
4056 idx: usize,
4057 trimmed: &str,
4058) -> bool {
4059 let Some((delim, start_line)) = *active_docstring else {
4060 return false;
4061 };
4062 docstring_lines.insert(idx);
4063 if closes_triple_docstring(trimmed, delim, idx == start_line) {
4064 *active_docstring = None;
4065 }
4066 true
4067}
4068
4069fn try_record_docstring_if_context(
4072 contexts: &mut [PyContext],
4073 trimmed: &str,
4074 idx: usize,
4075 docstring_lines: &mut HashSet<usize>,
4076 active_docstring: &mut Option<(&'static str, usize)>,
4077) -> bool {
4078 let Some(ctx) = contexts.last_mut() else {
4079 return false;
4080 };
4081 py_try_record_docstring(ctx, trimmed, idx, docstring_lines, active_docstring)
4082}
4083
4084fn mark_unclosed_docstring_lines(
4086 active_docstring: Option<&(&'static str, usize)>,
4087 docstring_lines: &mut HashSet<usize>,
4088 num_lines: usize,
4089) {
4090 if let Some(&(_, start_line)) = active_docstring {
4091 for idx in start_line..num_lines {
4092 docstring_lines.insert(idx);
4093 }
4094 }
4095}
4096
4097fn detect_python_docstring_lines(text: &str) -> HashSet<usize> {
4098 let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
4099 let lines: Vec<&str> = normalized.split_terminator('\n').collect();
4100
4101 let mut docstring_lines = HashSet::new();
4102 let mut contexts = vec![PyContext {
4103 indent: 0,
4104 expect_docstring: true,
4105 }];
4106 let mut pending_block_indent: Option<usize> = None;
4107 let mut active_docstring: Option<(&'static str, usize)> = None;
4108
4109 for (idx, line) in lines.iter().enumerate() {
4110 let trimmed = line.trim();
4111 let indent = leading_indent(line);
4112
4113 if track_active_docstring(&mut active_docstring, &mut docstring_lines, idx, trimmed) {
4114 continue;
4115 }
4116
4117 if trimmed.is_empty() || trimmed.starts_with('#') {
4119 continue;
4120 }
4121
4122 py_pop_outdented_contexts(&mut contexts, indent);
4123 py_handle_pending_indent(&mut pending_block_indent, &mut contexts, indent, trimmed);
4124
4125 if try_record_docstring_if_context(
4126 &mut contexts,
4127 trimmed,
4128 idx,
4129 &mut docstring_lines,
4130 &mut active_docstring,
4131 ) {
4132 continue;
4133 }
4134
4135 if is_python_block_header(trimmed) {
4136 pending_block_indent = Some(indent);
4137 }
4138 }
4139
4140 mark_unclosed_docstring_lines(active_docstring.as_ref(), &mut docstring_lines, lines.len());
4141
4142 docstring_lines
4143}
4144
4145fn leading_indent(line: &str) -> usize {
4146 line.chars().take_while(|c| c.is_whitespace()).count()
4147}
4148
4149fn is_python_block_header(trimmed: &str) -> bool {
4150 (trimmed.starts_with("def ")
4151 || trimmed.starts_with("async def ")
4152 || trimmed.starts_with("class "))
4153 && trimmed.ends_with(':')
4154}
4155
4156fn docstring_delimiter(trimmed: &str) -> Option<&'static str> {
4157 let mut idx = 0usize;
4158 let bytes = trimmed.as_bytes();
4159 while idx < bytes.len() {
4160 let c = bytes[idx] as char;
4161 if matches!(c, 'r' | 'R' | 'u' | 'U' | 'b' | 'B' | 'f' | 'F') {
4162 idx += 1;
4163 continue;
4164 }
4165 break;
4166 }
4167
4168 let rest = &trimmed[idx..];
4169 if rest.starts_with("\"\"\"") {
4170 Some("\"\"\"")
4171 } else if rest.starts_with("'''") {
4172 Some("'''")
4173 } else {
4174 None
4175 }
4176}
4177
4178fn closes_triple_docstring(trimmed: &str, delim: &str, same_line_as_start: bool) -> bool {
4179 let mut occurrences = 0usize;
4180 let mut search = trimmed;
4181 while let Some(index) = search.find(delim) {
4182 occurrences += 1;
4183 search = &search[index + delim.len()..];
4184 }
4185
4186 if same_line_as_start {
4187 occurrences >= 2
4188 } else {
4189 occurrences >= 1
4190 }
4191}
4192
4193#[cfg(feature = "tree-sitter")]
4198pub mod ts {
4199 use tree_sitter::Node;
4200
4201 use super::{ParseMode, RawFileAnalysis, RawLineCounts};
4202
4203 struct SymbolKinds {
4205 function_def: &'static str,
4207 class_def: &'static str,
4209 test_fn_prefix: &'static str,
4212 test_class_prefix: &'static str,
4215 assertion_attr_prefix: &'static str,
4219 }
4220
4221 impl SymbolKinds {
4222 const fn none() -> Self {
4223 Self {
4224 function_def: "",
4225 class_def: "",
4226 test_fn_prefix: "",
4227 test_class_prefix: "",
4228 assertion_attr_prefix: "",
4229 }
4230 }
4231 }
4232
4233 fn analyze_lines(
4239 text: &str,
4240 ts_language: &tree_sitter::Language,
4241 comment_node_kinds: &[&str],
4242 docstring_stmt_kind: Option<&str>,
4243 symbols: &SymbolKinds,
4244 ) -> Option<RawFileAnalysis> {
4245 let mut parser = tree_sitter::Parser::new();
4246 parser.set_language(ts_language).ok()?;
4247 let tree = parser.parse(text, None)?;
4248
4249 let lines: Vec<&str> = text.split_terminator('\n').collect();
4250 let n = lines.len();
4251
4252 let mut has_code = vec![false; n];
4253 let mut has_comment = vec![false; n];
4254 let mut comment_is_block = vec![false; n];
4255 let mut has_docstring = vec![false; n];
4256
4257 let mut ctx = VisitCtx {
4259 source: text.as_bytes(),
4260 comment_kinds: comment_node_kinds,
4261 docstring_stmt_kind,
4262 has_code: &mut has_code,
4263 has_comment: &mut has_comment,
4264 comment_is_block: &mut comment_is_block,
4265 has_docstring: &mut has_docstring,
4266 };
4267 visit(tree.root_node(), &mut ctx);
4268
4269 let mut raw = RawLineCounts::default();
4270 classify_ts_lines(
4271 &lines,
4272 &has_code,
4273 &has_comment,
4274 &comment_is_block,
4275 &has_docstring,
4276 &mut raw,
4277 );
4278
4279 if !symbols.function_def.is_empty() || !symbols.class_def.is_empty() {
4281 count_symbols(tree.root_node(), text.as_bytes(), symbols, &mut raw);
4282 }
4283
4284 Some(RawFileAnalysis {
4285 raw,
4286 parse_mode: ParseMode::TreeSitter,
4287 warnings: Vec::new(),
4288 style_analysis: None,
4289 })
4290 }
4291
4292 fn recurse_children(node: Node, source: &[u8], kinds: &SymbolKinds, raw: &mut RawLineCounts) {
4294 for i in 0..node.child_count() {
4295 #[allow(clippy::cast_possible_truncation)]
4296 if let Some(child) = node.child(i as u32) {
4297 count_symbols(child, source, kinds, raw);
4298 }
4299 }
4300 }
4301
4302 fn try_count_function(
4304 node: Node,
4305 source: &[u8],
4306 kinds: &SymbolKinds,
4307 raw: &mut RawLineCounts,
4308 ) -> bool {
4309 if kinds.function_def.is_empty() || node.kind() != kinds.function_def {
4310 return false;
4311 }
4312 let name = node
4313 .child_by_field_name("name")
4314 .and_then(|n| n.utf8_text(source).ok())
4315 .unwrap_or("");
4316 if !kinds.test_fn_prefix.is_empty() && name.starts_with(kinds.test_fn_prefix) {
4317 raw.test_count += 1;
4318 } else {
4319 raw.functions += 1;
4320 }
4321 recurse_children(node, source, kinds, raw);
4322 true
4323 }
4324
4325 fn try_count_class(
4327 node: Node,
4328 source: &[u8],
4329 kinds: &SymbolKinds,
4330 raw: &mut RawLineCounts,
4331 ) -> bool {
4332 if kinds.class_def.is_empty() || node.kind() != kinds.class_def {
4333 return false;
4334 }
4335 let name = node
4336 .child_by_field_name("name")
4337 .and_then(|n| n.utf8_text(source).ok())
4338 .unwrap_or("");
4339 if !kinds.test_class_prefix.is_empty() && name.starts_with(kinds.test_class_prefix) {
4340 raw.test_count += 1;
4341 } else {
4342 raw.classes += 1;
4343 }
4344 recurse_children(node, source, kinds, raw);
4345 true
4346 }
4347
4348 fn try_count_assertion(
4351 node: Node,
4352 source: &[u8],
4353 kinds: &SymbolKinds,
4354 raw: &mut RawLineCounts,
4355 ) -> bool {
4356 if kinds.assertion_attr_prefix.is_empty() || node.kind() != "call" {
4357 return false;
4358 }
4359 let Some(func) = node.child_by_field_name("function") else {
4360 return false;
4361 };
4362 if func.kind() != "attribute" {
4363 return false;
4364 }
4365 let attr_text = func
4366 .child_by_field_name("attribute")
4367 .and_then(|n| n.utf8_text(source).ok())
4368 .unwrap_or("");
4369 if !attr_text.starts_with(kinds.assertion_attr_prefix) {
4370 return false;
4371 }
4372 raw.test_assertion_count += 1;
4373 true
4374 }
4375
4376 fn count_symbols(node: Node, source: &[u8], kinds: &SymbolKinds, raw: &mut RawLineCounts) {
4379 if try_count_function(node, source, kinds, raw) {
4380 return;
4381 }
4382 if try_count_class(node, source, kinds, raw) {
4383 return;
4384 }
4385 if try_count_assertion(node, source, kinds, raw) {
4386 return;
4387 }
4388 recurse_children(node, source, kinds, raw);
4389 }
4390
4391 #[allow(clippy::struct_excessive_bools)]
4394 #[derive(Clone, Copy)]
4395 struct TsLineFlags {
4396 has_code: bool,
4397 has_comment: bool,
4398 comment_is_block: bool,
4399 has_docstring: bool,
4400 }
4401
4402 const fn classify_ts_line(trimmed: &str, flags: TsLineFlags, raw: &mut RawLineCounts) {
4404 if trimmed.is_empty() {
4405 raw.blank_only_lines += 1;
4406 } else if flags.has_docstring && !flags.has_code {
4407 raw.docstring_comment_lines += 1;
4408 } else if flags.has_code && flags.has_comment {
4409 if flags.comment_is_block {
4411 raw.mixed_code_multi_comment_lines += 1;
4412 } else {
4413 raw.mixed_code_single_comment_lines += 1;
4414 }
4415 } else if flags.has_comment {
4416 if flags.comment_is_block {
4417 raw.multi_comment_only_lines += 1;
4418 } else {
4419 raw.single_comment_only_lines += 1;
4420 }
4421 } else {
4422 raw.code_only_lines += 1;
4423 }
4424 }
4425
4426 fn classify_ts_lines(
4428 lines: &[&str],
4429 has_code: &[bool],
4430 has_comment: &[bool],
4431 comment_is_block: &[bool],
4432 has_docstring: &[bool],
4433 raw: &mut RawLineCounts,
4434 ) {
4435 for i in 0..lines.len() {
4436 raw.total_physical_lines += 1;
4437 classify_ts_line(
4438 lines[i].trim(),
4439 TsLineFlags {
4440 has_code: has_code[i],
4441 has_comment: has_comment[i],
4442 comment_is_block: comment_is_block[i],
4443 has_docstring: has_docstring[i],
4444 },
4445 raw,
4446 );
4447 }
4448 }
4449
4450 struct VisitCtx<'a> {
4451 source: &'a [u8],
4452 comment_kinds: &'a [&'a str],
4453 docstring_stmt_kind: Option<&'a str>,
4454 has_code: &'a mut Vec<bool>,
4455 has_comment: &'a mut Vec<bool>,
4456 comment_is_block: &'a mut Vec<bool>,
4457 has_docstring: &'a mut Vec<bool>,
4458 }
4459
4460 fn visit_comment_node(node: Node, ctx: &mut VisitCtx<'_>) {
4462 let start_row = node.start_position().row;
4463 let end_row = node.end_position().row;
4464 let first_two = node
4465 .utf8_text(ctx.source)
4466 .unwrap_or("")
4467 .get(..2)
4468 .unwrap_or("");
4469 let is_block = first_two == "/*" || first_two == "<#";
4470 for row in start_row..=end_row {
4471 if row < ctx.has_comment.len() {
4472 ctx.has_comment[row] = true;
4473 if is_block {
4474 ctx.comment_is_block[row] = true;
4475 }
4476 }
4477 }
4478 }
4479
4480 fn visit_maybe_docstring(node: Node, kind: &str, ctx: &mut VisitCtx<'_>) -> bool {
4483 let Some(stmt_kind) = ctx.docstring_stmt_kind else {
4484 return false;
4485 };
4486 if kind != stmt_kind || node.named_child_count() != 1 {
4487 return false;
4488 }
4489 let Some(child) = node.named_child(0) else {
4490 return false;
4491 };
4492 if child.kind() != "string" {
4493 return false;
4494 }
4495 let child_start = child.start_position().row;
4496 let child_end = child.end_position().row;
4497 for row in child_start..=child_end {
4498 if row < ctx.has_docstring.len() {
4499 ctx.has_docstring[row] = true;
4500 }
4501 }
4502 true
4503 }
4504
4505 fn visit_leaf_code(node: Node, ctx: &mut VisitCtx<'_>) {
4507 let start_row = node.start_position().row;
4508 let end_row = node.end_position().row;
4509 for row in start_row..=end_row {
4510 if row < ctx.has_code.len() {
4511 ctx.has_code[row] = true;
4512 }
4513 }
4514 }
4515
4516 #[allow(clippy::too_many_lines)]
4517 fn visit(node: Node, ctx: &mut VisitCtx<'_>) {
4518 let kind = node.kind();
4519
4520 if ctx.comment_kinds.contains(&kind) {
4522 visit_comment_node(node, ctx);
4523 return;
4524 }
4525
4526 if visit_maybe_docstring(node, kind, ctx) {
4528 return;
4529 }
4530
4531 if node.child_count() == 0 && !node.is_extra() {
4533 visit_leaf_code(node, ctx);
4534 return;
4535 }
4536
4537 for i in 0..node.child_count() {
4538 #[allow(clippy::cast_possible_truncation)]
4539 if let Some(child) = node.child(i as u32) {
4541 visit(child, ctx);
4542 }
4543 }
4544 }
4545
4546 const C_SYMBOLS: SymbolKinds = SymbolKinds::none();
4547
4548 const PYTHON_SYMBOLS: SymbolKinds = SymbolKinds {
4549 function_def: "function_definition",
4550 class_def: "class_definition",
4551 test_fn_prefix: "test_",
4552 test_class_prefix: "Test",
4553 assertion_attr_prefix: "assert",
4554 };
4555
4556 #[must_use]
4558 pub fn analyze_c(text: &str) -> Option<RawFileAnalysis> {
4559 let lang: tree_sitter::Language = tree_sitter_c::LANGUAGE.into();
4560 analyze_lines(text, &lang, &["comment"], None, &C_SYMBOLS)
4561 }
4562
4563 #[must_use]
4565 pub fn analyze_python(text: &str) -> Option<RawFileAnalysis> {
4566 let lang: tree_sitter::Language = tree_sitter_python::LANGUAGE.into();
4567 analyze_lines(
4568 text,
4569 &lang,
4570 &["comment"],
4571 Some("expression_statement"),
4572 &PYTHON_SYMBOLS,
4573 )
4574 }
4575}
4576
4577#[cfg(test)]
4578mod tests {
4579 use super::*;
4580
4581 #[test]
4582 fn python_docstrings_are_separated() {
4583 let input = r#""""module docs"""
4584
4585
4586def fn_a():
4587 """function docs"""
4588 value = 1 # trailing comment
4589 return value
4590"#;
4591
4592 let result = analyze_text(Language::Python, input, AnalysisOptions::default());
4593 assert_eq!(result.raw.docstring_comment_lines, 2);
4594 assert_eq!(result.raw.mixed_code_single_comment_lines, 1);
4595 assert_eq!(result.raw.code_only_lines, 2);
4596 }
4597
4598 #[test]
4599 fn c_style_mixed_lines_are_captured() {
4600 let input = "int x = 1; // note\n/* block */\n";
4601 let result = analyze_text(Language::C, input, AnalysisOptions::default());
4602 assert_eq!(result.raw.mixed_code_single_comment_lines, 1);
4603 assert_eq!(result.raw.multi_comment_only_lines, 1);
4604 }
4605
4606 #[test]
4607 fn branch_keywords_inside_strings_are_not_counted() {
4608 let s = analyze_text(
4610 Language::Rust,
4611 "let s = \"if a && b || c ? d : e\";\n",
4612 AnalysisOptions::default(),
4613 );
4614 assert_eq!(s.raw.cyclomatic_complexity, 0);
4615
4616 let raw = analyze_text(
4619 Language::Rust,
4620 "let h = r#\"<a href=\"x\">a && b ? c : d</a>\"#;\n",
4621 AnalysisOptions::default(),
4622 );
4623 assert_eq!(raw.raw.cyclomatic_complexity, 0);
4624
4625 let code = analyze_text(
4627 Language::Rust,
4628 "if a && b { c } else { d }\n",
4629 AnalysisOptions::default(),
4630 );
4631 assert!(code.raw.cyclomatic_complexity >= 2);
4632 }
4633
4634 #[test]
4635 fn multiline_raw_string_does_not_swallow_following_code() {
4636 let input = concat!(
4639 "let cfg = r##\"\n",
4640 "# looks like a comment but is string content\n",
4641 "key = \"value with \"\" inner quotes\"\n",
4642 "\"##;\n",
4643 "let x = 1;\n",
4644 );
4645 let r = analyze_text(Language::Rust, input, AnalysisOptions::default());
4646 assert!(
4647 r.raw.code_only_lines >= 1,
4648 "code after the raw string was swallowed"
4649 );
4650 assert_eq!(r.raw.single_comment_only_lines, 0);
4651 assert_eq!(r.raw.cyclomatic_complexity, 0);
4652 }
4653
4654 #[test]
4655 fn detect_language_by_shebang() {
4656 let language = detect_language(
4657 Path::new("script"),
4658 Some("#!/usr/bin/env bash"),
4659 &BTreeMap::new(),
4660 true,
4661 );
4662 assert_eq!(language, Some(Language::Shell));
4663 }
4664
4665 fn sym(lang: Language, line: &str) -> (u64, u64, u64, u64, u64, u64, u64) {
4668 let result = analyze_text(lang, &format!("{line}\n"), AnalysisOptions::default());
4669 let r = &result.raw;
4670 (
4671 r.functions,
4672 r.classes,
4673 r.variables,
4674 r.imports,
4675 r.test_count,
4676 r.test_assertion_count,
4677 r.test_suite_count,
4678 )
4679 }
4680
4681 #[test]
4682 fn python_test_fn_not_double_counted() {
4683 let (f, c, _, _, t, _, _) = sym(Language::Python, "def test_foo():");
4685 assert_eq!(f, 0, "test fn must not also increment functions");
4686 assert_eq!(t, 1, "must be counted as a test");
4687 assert_eq!(c, 0);
4688 }
4689
4690 #[test]
4691 fn python_test_class_not_double_counted() {
4692 let (f, c, _, _, t, _, _) = sym(Language::Python, "class TestFoo:");
4694 assert_eq!(c, 0, "test class must not also increment classes");
4695 assert_eq!(t, 1, "must be counted as a test");
4696 assert_eq!(f, 0);
4697 }
4698
4699 #[test]
4700 fn python_regular_fn_counts_as_function() {
4701 let (f, c, _, _, t, _, _) = sym(Language::Python, "def regular():");
4702 assert_eq!(f, 1, "regular function must be counted");
4703 assert_eq!(t, 0);
4704 assert_eq!(c, 0);
4705 }
4706
4707 #[test]
4708 fn python_regular_class_counts_as_class() {
4709 let (f, c, _, _, t, _, _) = sym(Language::Python, "class Regular:");
4710 assert_eq!(c, 1, "regular class must be counted");
4711 assert_eq!(t, 0);
4712 assert_eq!(f, 0);
4713 }
4714
4715 #[test]
4716 fn go_test_fn_not_double_counted() {
4717 let (f, _, _, _, t, _, _) = sym(Language::Go, "func TestFoo(t *testing.T) {");
4718 assert_eq!(f, 0, "Go test func must not also increment functions");
4719 assert_eq!(t, 1, "must be counted as a test");
4720 }
4721
4722 #[test]
4723 fn go_benchmark_fn_not_double_counted() {
4724 let (f, _, _, _, t, _, _) = sym(Language::Go, "func BenchmarkBar(b *testing.B) {");
4725 assert_eq!(f, 0, "Go benchmark func must not also increment functions");
4726 assert_eq!(t, 1, "must be counted as a test");
4727 }
4728
4729 #[test]
4730 fn go_regular_fn_counts_as_function() {
4731 let (f, _, _, _, t, _, _) = sym(Language::Go, "func doSomething() {");
4732 assert_eq!(f, 1, "regular Go func must be counted");
4733 assert_eq!(t, 0);
4734 }
4735
4736 #[test]
4737 fn rust_test_attr_counts_as_test_not_function() {
4738 let (f, _, _, _, t, _, _) = sym(Language::Rust, "#[test]");
4740 assert_eq!(t, 1, "#[test] must be counted as a test");
4741 assert_eq!(f, 0, "#[test] attribute must not be counted as a function");
4742 }
4743
4744 #[test]
4745 fn rust_fn_line_counts_as_function_not_test() {
4746 let (f, _, _, _, t, _, _) = sym(Language::Rust, "fn test_something() {");
4748 assert_eq!(f, 1, "fn declaration must count as a function");
4749 assert_eq!(
4750 t, 0,
4751 "fn declaration line must not be double-counted as a test"
4752 );
4753 }
4754
4755 #[test]
4756 fn js_describe_counts_as_test_not_function() {
4757 let (f, _, _, _, t, _, _) = sym(Language::JavaScript, "describe('suite', () => {");
4758 assert_eq!(t, 1, "describe must be counted as a test");
4759 assert_eq!(f, 0, "describe must not be counted as a function");
4760 }
4761
4762 #[test]
4763 fn js_regular_fn_counts_as_function() {
4764 let (f, _, _, _, t, _, _) = sym(Language::JavaScript, "function doWork() {");
4765 assert_eq!(f, 1, "JS function declaration must be counted");
4766 assert_eq!(t, 0);
4767 }
4768
4769 use std::collections::BTreeMap;
4772 use std::path::Path;
4773
4774 #[test]
4775 fn detect_language_rs_extension() {
4776 let lang = detect_language(Path::new("foo.rs"), None, &BTreeMap::new(), false);
4777 assert_eq!(lang, Some(Language::Rust));
4778 }
4779
4780 #[test]
4781 fn detect_language_py_extension() {
4782 let lang = detect_language(Path::new("foo.py"), None, &BTreeMap::new(), false);
4783 assert_eq!(lang, Some(Language::Python));
4784 }
4785
4786 #[test]
4787 fn detect_language_ts_extension() {
4788 let lang = detect_language(Path::new("app.ts"), None, &BTreeMap::new(), false);
4789 assert_eq!(lang, Some(Language::TypeScript));
4790 }
4791
4792 #[test]
4793 fn detect_language_js_extension() {
4794 let lang = detect_language(Path::new("app.js"), None, &BTreeMap::new(), false);
4795 assert_eq!(lang, Some(Language::JavaScript));
4796 }
4797
4798 #[test]
4799 fn detect_language_go_extension() {
4800 let lang = detect_language(Path::new("main.go"), None, &BTreeMap::new(), false);
4801 assert_eq!(lang, Some(Language::Go));
4802 }
4803
4804 #[test]
4805 fn detect_language_c_extension() {
4806 let lang = detect_language(Path::new("main.c"), None, &BTreeMap::new(), false);
4807 assert_eq!(lang, Some(Language::C));
4808 }
4809
4810 #[test]
4811 fn detect_language_cpp_extension() {
4812 let lang = detect_language(Path::new("main.cpp"), None, &BTreeMap::new(), false);
4813 assert_eq!(lang, Some(Language::Cpp));
4814 }
4815
4816 #[test]
4817 fn detect_language_java_extension() {
4818 let lang = detect_language(Path::new("Main.java"), None, &BTreeMap::new(), false);
4819 assert_eq!(lang, Some(Language::Java));
4820 }
4821
4822 #[test]
4823 fn detect_language_makefile_exact_name() {
4824 let lang = detect_language(Path::new("Makefile"), None, &BTreeMap::new(), false);
4825 assert_eq!(lang, Some(Language::Makefile));
4826 }
4827
4828 #[test]
4829 fn detect_language_dockerfile_exact_name() {
4830 let lang = detect_language(Path::new("Dockerfile"), None, &BTreeMap::new(), false);
4831 assert_eq!(lang, Some(Language::Dockerfile));
4832 }
4833
4834 #[test]
4835 fn detect_language_rakefile() {
4836 let lang = detect_language(Path::new("Rakefile"), None, &BTreeMap::new(), false);
4837 assert_eq!(lang, Some(Language::Ruby));
4838 }
4839
4840 #[test]
4841 fn detect_language_gemfile() {
4842 let lang = detect_language(Path::new("Gemfile"), None, &BTreeMap::new(), false);
4843 assert_eq!(lang, Some(Language::Ruby));
4844 }
4845
4846 #[test]
4847 fn detect_language_unknown_extension_returns_none() {
4848 let lang = detect_language(Path::new("foo.xyz123"), None, &BTreeMap::new(), false);
4849 assert_eq!(lang, None);
4850 }
4851
4852 #[test]
4853 fn detect_language_extension_override() {
4854 let mut overrides = BTreeMap::new();
4855 overrides.insert("h".into(), "cpp".into());
4856 let lang = detect_language(Path::new("header.h"), None, &overrides, false);
4857 assert_eq!(lang, Some(Language::Cpp));
4858 }
4859
4860 #[test]
4861 fn detect_language_shebang_python() {
4862 let lang = detect_language(
4863 Path::new("script"),
4864 Some("#!/usr/bin/env python3"),
4865 &BTreeMap::new(),
4866 true,
4867 );
4868 assert_eq!(lang, Some(Language::Python));
4869 }
4870
4871 #[test]
4872 fn detect_language_shebang_bash() {
4873 let lang = detect_language(
4874 Path::new("script"),
4875 Some("#!/bin/bash"),
4876 &BTreeMap::new(),
4877 true,
4878 );
4879 assert_eq!(lang, Some(Language::Shell));
4880 }
4881
4882 #[test]
4883 fn detect_language_shebang_ruby() {
4884 let lang = detect_language(
4885 Path::new("script"),
4886 Some("#!/usr/bin/env ruby"),
4887 &BTreeMap::new(),
4888 true,
4889 );
4890 assert_eq!(lang, Some(Language::Ruby));
4891 }
4892
4893 #[test]
4894 fn detect_language_shebang_disabled() {
4895 let lang = detect_language(
4897 Path::new("script"),
4898 Some("#!/usr/bin/env python3"),
4899 &BTreeMap::new(),
4900 false,
4901 );
4902 assert_eq!(lang, None);
4903 }
4904
4905 #[test]
4906 fn from_name_rust() {
4907 assert_eq!(Language::from_name("rust"), Some(Language::Rust));
4908 }
4909
4910 #[test]
4911 fn from_name_python() {
4912 assert_eq!(Language::from_name("python"), Some(Language::Python));
4913 }
4914
4915 #[test]
4916 fn from_name_unknown() {
4917 assert_eq!(Language::from_name("brainfuck"), None);
4918 }
4919
4920 #[test]
4921 fn from_name_roundtrip_all() {
4922 for lang in [
4924 Language::C,
4925 Language::Cpp,
4926 Language::CSharp,
4927 Language::Go,
4928 Language::Java,
4929 Language::JavaScript,
4930 Language::Python,
4931 Language::Rust,
4932 Language::Shell,
4933 Language::PowerShell,
4934 Language::TypeScript,
4935 Language::Assembly,
4936 Language::Clojure,
4937 Language::Css,
4938 Language::Dart,
4939 Language::Dockerfile,
4940 Language::Elixir,
4941 Language::Erlang,
4942 Language::FSharp,
4943 Language::Groovy,
4944 Language::Haskell,
4945 Language::Html,
4946 Language::Julia,
4947 Language::Kotlin,
4948 Language::Lua,
4949 Language::Makefile,
4950 Language::Nim,
4951 Language::ObjectiveC,
4952 Language::Ocaml,
4953 Language::Perl,
4954 Language::Php,
4955 Language::R,
4956 Language::Ruby,
4957 Language::Scala,
4958 Language::Scss,
4959 Language::Sql,
4960 Language::Svelte,
4961 Language::Swift,
4962 Language::Vue,
4963 Language::Xml,
4964 Language::Zig,
4965 ] {
4966 let slug = lang.as_slug();
4967 let roundtripped = Language::from_name(slug);
4968 assert_eq!(
4969 roundtripped,
4970 Some(lang),
4971 "from_name({slug:?}) should return {lang:?}"
4972 );
4973 }
4974 }
4975
4976 #[test]
4979 fn blank_in_block_comment_defaults_to_comment() {
4980 let input = "/*\n\n*/";
4982 let opts = AnalysisOptions {
4983 blank_in_block_comment_as_comment: true,
4984 ..Default::default()
4985 };
4986 let result = analyze_text(Language::C, input, opts);
4987 assert_eq!(
4988 result.raw.multi_comment_only_lines, 3,
4989 "all 3 block-comment lines must count as multi-comment with CountAsComment policy"
4990 );
4991 assert_eq!(
4992 result.raw.blank_only_lines, 0,
4993 "no blank lines expected with CountAsComment policy"
4994 );
4995 }
4996
4997 #[test]
4998 fn blank_in_block_comment_counted_as_blank_when_policy_false() {
4999 let input = "/*\n\n*/";
5001 let opts = AnalysisOptions {
5002 blank_in_block_comment_as_comment: false,
5003 ..Default::default()
5004 };
5005 let result = analyze_text(Language::C, input, opts);
5006 assert_eq!(
5007 result.raw.multi_comment_only_lines, 2,
5008 "opener and closer must count as multi-comment with CountAsBlank policy"
5009 );
5010 assert_eq!(
5011 result.raw.blank_only_lines, 1,
5012 "the blank line inside the block comment must count as blank with CountAsBlank policy"
5013 );
5014 }
5015
5016 #[test]
5019 fn continuation_lines_each_physical_default() {
5020 let input = "#define FOO \\\n 1 \\\n + 2\n";
5022 let opts = AnalysisOptions {
5023 collapse_continuation_lines: false,
5024 ..Default::default()
5025 };
5026 let result = analyze_text(Language::C, input, opts);
5027 assert_eq!(
5028 result.raw.total_physical_lines, 3,
5029 "3 physical lines expected"
5030 );
5031 assert_eq!(
5032 result.raw.code_only_lines, 3,
5033 "each physical line must count as code with EachPhysicalLine policy"
5034 );
5035 }
5036
5037 #[test]
5038 fn continuation_lines_collapse_to_logical() {
5039 let input = "#define FOO \\\n 1 \\\n + 2\n";
5041 let opts = AnalysisOptions {
5042 collapse_continuation_lines: true,
5043 ..Default::default()
5044 };
5045 let result = analyze_text(Language::C, input, opts);
5046 assert_eq!(
5047 result.raw.total_physical_lines, 3,
5048 "physical line count is always 3 regardless of policy"
5049 );
5050 assert_eq!(
5051 result.raw.code_only_lines, 1,
5052 "3 continuation lines must collapse to 1 logical code line"
5053 );
5054 }
5055}