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