Skip to main content

big_code_analysis/metrics/
tokens.rs

1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
8// Metric counts (token, function, branch, argument, etc.) are stored as
9// `usize` and crossed with `f64` averages, ratios, and Halstead scores
10// across the cyclomatic / MI / Halstead computations. The `usize as f64`
11// and `f64 as usize` casts are intentional and snapshot-anchored — every
12// site is bounded by the count it came from. Allowing the lints at the
13// module level keeps the metric arithmetic legible.
14#![allow(
15    clippy::cast_precision_loss,
16    clippy::cast_possible_truncation,
17    clippy::cast_sign_loss
18)]
19
20use std::fmt;
21
22use crate::checker::Checker;
23use crate::macros::implement_metric_trait;
24
25use crate::*;
26
27/// The `Tokens` metric: per-function and per-file count of tree-sitter
28/// leaf tokens, excluding any leaf that is itself a comment or has a
29/// comment among its ancestors. Both halves matter: most grammars emit
30/// comments as bare leaves, while some (Rust doc comments, Groovy
31/// groovydoc, JSX `html_comment`) give them structured children whose
32/// own leaves are not comment kinds.
33///
34/// This is a token-based size proxy: it counts the lexer's tokens
35/// (identifiers, literals, keywords, punctuation) rather than lines or
36/// Halstead operators/operands. Punctuation that Halstead skips
37/// (parentheses, semicolons, separators) does contribute, so
38/// `tokens` ≠ Halstead `N1 + N2`.
39#[derive(Clone, Debug, PartialEq)]
40#[non_exhaustive]
41pub struct Stats {
42    tokens: usize,
43    tokens_sum: usize,
44    tokens_min: usize,
45    tokens_max: usize,
46    space_count: usize,
47}
48
49impl Default for Stats {
50    fn default() -> Self {
51        Self {
52            tokens: 0,
53            tokens_sum: 0,
54            tokens_min: usize::MAX,
55            tokens_max: 0,
56            space_count: 1,
57        }
58    }
59}
60
61impl fmt::Display for Stats {
62    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
63        write!(
64            f,
65            "tokens: {}, \
66             tokens_average: {}, \
67             tokens_min: {}, \
68             tokens_max: {}",
69            self.tokens_sum(),
70            self.tokens_average(),
71            self.tokens_min(),
72            self.tokens_max(),
73        )
74    }
75}
76
77impl Stats {
78    /// Merges a second `Tokens` metric suite into the first one.
79    pub fn merge(&mut self, other: &Stats) {
80        self.tokens_min = self.tokens_min.min(other.tokens_min);
81        self.tokens_max = self.tokens_max.max(other.tokens_max);
82        self.tokens_sum += other.tokens_sum;
83        self.space_count += other.space_count;
84    }
85
86    /// Returns the total token count across all merged spaces.
87    #[inline]
88    #[must_use]
89    pub fn tokens_sum(&self) -> u64 {
90        self.tokens_sum as u64
91    }
92
93    /// Returns the average tokens per space.
94    #[inline]
95    #[must_use]
96    pub fn tokens_average(&self) -> f64 {
97        crate::metrics::average(self.tokens_sum() as f64, self.space_count)
98    }
99
100    /// Returns the smallest single-space token count.
101    ///
102    /// Diverges intentionally from `nom::Stats::functions_min`, which
103    /// surfaces the raw `usize::MAX` sentinel for a never-observed
104    /// space. We collapse the sentinel to `0` so a `Stats::default()`
105    /// that bypasses the metric pipeline serializes to a meaningful
106    /// number rather than `18446744073709551615`.
107    #[inline]
108    #[must_use]
109    pub fn tokens_min(&self) -> u64 {
110        if self.tokens_min == usize::MAX {
111            0
112        } else {
113            self.tokens_min as u64
114        }
115    }
116
117    /// Returns the largest single-space token count.
118    #[inline]
119    #[must_use]
120    pub fn tokens_max(&self) -> u64 {
121        self.tokens_max as u64
122    }
123
124    #[inline]
125    pub(crate) fn compute_sum(&mut self) {
126        self.tokens_sum += self.tokens;
127    }
128
129    #[inline]
130    pub(crate) fn compute_minmax(&mut self) {
131        self.tokens_min = self.tokens_min.min(self.tokens);
132        self.tokens_max = self.tokens_max.max(self.tokens);
133        self.compute_sum();
134    }
135}
136
137#[doc(hidden)]
138/// Per-language counting of tokens.
139pub(crate) trait Tokens
140where
141    Self: Checker,
142{
143    /// Walk `node` and update `stats` with this metric for the language
144    /// implementing the trait.
145    ///
146    /// `in_comment` is true when the node itself or any ancestor is a
147    /// comment, so grammars whose comments have internal structure (e.g.
148    /// Rust doc comments split into markers and content) exclude their
149    /// inner leaves too. The walker propagates it down the traversal
150    /// rather than having this function rediscover it per leaf: the old
151    /// ancestor walk was `O(depth)` per leaf over an `O(depth)`
152    /// `Node::parent`, which made the metric `O(leaves × depth²)` and let
153    /// a few kilobytes of nested source burn minutes of CPU (#1052).
154    fn compute(node: &Node, stats: &mut Stats, in_comment: bool) {
155        if in_comment || node.child_count() != 0 {
156            return;
157        }
158        stats.tokens += 1;
159    }
160}
161
162implement_metric_trait!(
163    [Tokens],
164    PythonCode,
165    MozjsCode,
166    JavascriptCode,
167    TypescriptCode,
168    TsxCode,
169    CppCode,
170    MozcppCode,
171    CCode,
172    ObjcCode,
173    RustCode,
174    PreprocCode,
175    CcommentCode,
176    JavaCode,
177    KotlinCode,
178    GoCode,
179    PerlCode,
180    BashCode,
181    LuaCode,
182    TclCode,
183    PhpCode,
184    CsharpCode,
185    ElixirCode,
186    RubyCode,
187    GroovyCode,
188    IrulesCode
189);
190
191#[cfg(test)]
192#[allow(
193    clippy::float_cmp,
194    clippy::cast_precision_loss,
195    clippy::cast_possible_truncation,
196    clippy::cast_sign_loss,
197    clippy::similar_names,
198    clippy::doc_markdown,
199    clippy::needless_raw_string_hashes,
200    clippy::too_many_lines
201)]
202mod tests {
203    use crate::test_support::{check_metrics_only_shim, metrics_verbatim};
204
205    use super::*;
206
207    check_metrics_only_shim!(check_metrics, Tokens);
208    // `*_tokens_distinct_from_halstead` compares `tokens_sum()` against
209    // Halstead's `N1 + N2`. Deselecting Halstead leaves that side at 0,
210    // where `tokens_sum() > 0` holds for the wrong reason — so these two
211    // ask for Halstead rather than passing vacuously.
212    check_metrics_only_shim!(check_tokens_and_halstead, Tokens, Halstead);
213
214    /// `def foo(x): return x` → leaves: `def`, `foo`, `(`, `x`, `)`,
215    /// `:`, `return`, `x` = 8 tokens, hand-counted.
216    #[cfg(feature = "python")]
217    #[test]
218    fn python_tokens_exact_count() {
219        check_metrics::<PythonParser>("def foo(x): return x", "foo.py", |metric| {
220            assert_eq!(metric.tokens.tokens_sum(), 8);
221            assert!(metric.tokens.tokens_max() >= 7);
222        });
223    }
224
225    /// Adding a Python comment must not change the token count.
226    #[cfg(feature = "python")]
227    #[test]
228    fn python_tokens_comments_excluded() {
229        check_metrics::<PythonParser>(
230            "def foo(x): return x  # explanation\n# header\n",
231            "foo.py",
232            |metric| {
233                assert_eq!(metric.tokens.tokens_sum(), 8);
234            },
235        );
236    }
237
238    /// Blank lines and indentation must not change the token count.
239    #[cfg(feature = "python")]
240    #[test]
241    fn python_tokens_whitespace_excluded() {
242        check_metrics::<PythonParser>(
243            "\n\n    def foo(x):\n        return x\n\n",
244            "foo.py",
245            |metric| {
246                assert_eq!(metric.tokens.tokens_sum(), 8);
247            },
248        );
249    }
250
251    /// Tokens must exceed Halstead `N1 + N2` for code containing
252    /// punctuation Halstead skips. Guards against accidental Halstead
253    /// reuse.
254    #[cfg(feature = "python")]
255    #[test]
256    fn python_tokens_distinct_from_halstead() {
257        check_tokens_and_halstead::<PythonParser>(
258            "def foo(x): return (x + 1)",
259            "foo.py",
260            |metric| {
261                let halstead_total =
262                    metric.halstead.total_operators() + metric.halstead.total_operands();
263                assert!(
264                    metric.tokens.tokens_sum() > halstead_total,
265                    "expected tokens ({}) > halstead N1+N2 ({}); punctuation \
266                     like `(`, `)`, `:` should contribute to tokens but not Halstead",
267                    metric.tokens.tokens_sum(),
268                    halstead_total,
269                );
270            },
271        );
272    }
273
274    /// Inner functions get attributed to their innermost scope. For
275    /// `def outer(): def inner(): return 1`, the inner scope owns
276    /// `def, inner, (, ), :, return, 1` = 7 tokens; the outer scope
277    /// owns `def, outer, (, ), :` = 5; the unit owns 0 directly.
278    /// Asserting the exact `tokens_max` is what catches an attribution
279    /// regression — a broken implementation that credited all 12
280    /// tokens to one scope would still pass `max <= sum`.
281    #[cfg(feature = "python")]
282    #[test]
283    fn python_tokens_nested_attribution() {
284        check_metrics::<PythonParser>(
285            "def outer():\n    def inner():\n        return 1\n",
286            "foo.py",
287            |metric| {
288                assert_eq!(metric.tokens.tokens_sum(), 12);
289                assert_eq!(metric.tokens.tokens_max(), 7);
290                assert_eq!(metric.tokens.tokens_min(), 0);
291            },
292        );
293    }
294
295    /// C++ `/* … */` block comments must not contribute.
296    /// Same fixture with and without comment yields the same count.
297    #[cfg(feature = "cpp")]
298    #[test]
299    fn cpp_tokens_block_comments_excluded() {
300        check_metrics::<CppParser>(
301            "int foo(int x) { /* multi\n   line */ return x; }",
302            "foo.cpp",
303            |m| {
304                // Leaves outside the comment:
305                // int, foo, (, int, x, ), {, return, x, ;, } = 11.
306                assert_eq!(m.tokens.tokens_sum(), 11);
307            },
308        );
309        check_metrics::<CppParser>("int foo(int x) { return x; }", "foo.cpp", |m| {
310            assert_eq!(m.tokens.tokens_sum(), 11);
311        });
312    }
313
314    /// C++ `// …` line comments must not contribute, matching the Python
315    /// hand-counted style.  Leaves outside the comment:
316    /// `int`, `x`, `=`, `1`, `;` = 5.
317    #[cfg(feature = "cpp")]
318    #[test]
319    fn cpp_tokens_line_comments_excluded() {
320        check_metrics::<CppParser>("int x = 1; // a one-line comment\n", "foo.cpp", |m| {
321            assert_eq!(m.tokens.tokens_sum(), 5);
322        });
323        check_metrics::<CppParser>("int x = 1;\n", "foo.cpp", |m| {
324            assert_eq!(m.tokens.tokens_sum(), 5);
325        });
326    }
327
328    /// Whitespace and blank lines must not contribute to the token count
329    /// (mirrors `python_tokens_whitespace_excluded`).
330    #[cfg(feature = "cpp")]
331    #[test]
332    fn cpp_tokens_whitespace_excluded() {
333        check_metrics::<CppParser>("\n\nint foo(int x) {\n    return x;\n}\n", "foo.cpp", |m| {
334            // int, foo, (, int, x, ), {, return, x, ;, } = 11.
335            assert_eq!(m.tokens.tokens_sum(), 11);
336        });
337    }
338
339    /// Tokens count punctuation that Halstead skips (parentheses, braces,
340    /// semicolons), so `tokens_sum` must exceed `N1 + N2` for a fixture
341    /// with significant punctuation.  Mirrors
342    /// `python_tokens_distinct_from_halstead`.
343    #[cfg(feature = "cpp")]
344    #[test]
345    fn cpp_tokens_distinct_from_halstead() {
346        check_tokens_and_halstead::<CppParser>(
347            "int foo(int x) { return (x + 1); }",
348            "foo.cpp",
349            |m| {
350                let halstead_total = m.halstead.total_operators() + m.halstead.total_operands();
351                assert!(
352                    m.tokens.tokens_sum() > halstead_total,
353                    "expected tokens ({}) > halstead N1+N2 ({}); punctuation like \
354                     `(`, `)`, `{{`, `}}` and `;` should contribute to tokens but not Halstead",
355                    m.tokens.tokens_sum(),
356                    halstead_total,
357                );
358            },
359        );
360    }
361
362    /// A C++ struct method and a free function each open their own
363    /// `FuncSpace`, so the leaves of
364    /// `struct S { int method(int a) { return a + 1; } }; int outer() { return 2; }`
365    /// split across scopes: `method` owns 13 and `outer` owns 9, with the
366    /// `struct`/unit framing owning the rest of the 27-token total.
367    /// Asserting the exact `tokens_max` (13 — the largest single scope,
368    /// strictly below the sum of 27) is what catches an attribution
369    /// regression: a broken implementation that credited every leaf to one
370    /// scope would raise `tokens_max` to 27 while still passing
371    /// `max <= sum`, mirroring the Python sibling's exact-max guard.
372    #[cfg(feature = "cpp")]
373    #[test]
374    fn cpp_tokens_nested_attribution() {
375        check_metrics::<CppParser>(
376            "struct S {\n    int method(int a) { return a + 1; }\n};\nint outer() { return 2; }\n",
377            "foo.cpp",
378            |m| {
379                assert_eq!(m.tokens.tokens_sum(), 27);
380                assert_eq!(m.tokens.tokens_max(), 13);
381                assert_eq!(m.tokens.tokens_min(), 1);
382            },
383        );
384    }
385
386    /// Java `// …` line comments must not contribute.
387    #[cfg(feature = "java")]
388    #[test]
389    fn java_tokens_line_comments_excluded() {
390        check_metrics::<JavaParser>(
391            "class A { void foo() { // hi\n return; } }",
392            "A.java",
393            |m| {
394                // class, A, {, void, foo, (, ), {, return, ;, }, } = 12.
395                assert_eq!(m.tokens.tokens_sum(), 12);
396            },
397        );
398        check_metrics::<JavaParser>("class A { void foo() { return; } }", "A.java", |m| {
399            assert_eq!(m.tokens.tokens_sum(), 12);
400        });
401    }
402
403    #[cfg(feature = "groovy")]
404    #[test]
405    fn groovy_tokens_line_comments_excluded() {
406        // Groovy mirror — `// …` line comments must not contribute.
407        check_metrics::<GroovyParser>(
408            "class A { void foo() { // hi\n return\n } }",
409            "A.groovy",
410            |m| {
411                // class, A, {, void, foo, (, ), {, return, newline,
412                // }, } = 11 tokens (Groovy's newline acts as the
413                // statement terminator that Java spells `;`).
414                assert_eq!(m.tokens.tokens_sum(), 11);
415            },
416        );
417    }
418
419    /// JS-family `<!-- -->` Annex-B `html_comment` leaves must not
420    /// contribute tokens — they classify as comments now (#697). The
421    /// count must match the comment-free source exactly.
422    #[cfg(feature = "javascript")]
423    #[test]
424    fn javascript_tokens_html_comment_excluded() {
425        check_metrics::<JavascriptParser>("<!-- hi -->\nlet x = 1;\n", "foo.js", |m| {
426            // let, x, =, 1, ; = 5.
427            assert_eq!(m.tokens.tokens_sum(), 5);
428        });
429        check_metrics::<JavascriptParser>("let x = 1;\n", "foo.js", |m| {
430            assert_eq!(m.tokens.tokens_sum(), 5);
431        });
432    }
433
434    /// Groovy `/** … */` `groovydoc_comment` leaves must not contribute
435    /// tokens (#697 — `is_comment` previously missed this kind even
436    /// though `Loc` counted it).
437    #[cfg(feature = "groovy")]
438    #[test]
439    fn groovy_tokens_groovydoc_excluded() {
440        check_metrics::<GroovyParser>(
441            "/** doc */\nclass A { void f() { return\n } }\n",
442            "A.groovy",
443            |m| {
444                // class, A, {, void, f, (, ), {, return, newline,
445                // }, } = 11.
446                assert_eq!(m.tokens.tokens_sum(), 11);
447            },
448        );
449        check_metrics::<GroovyParser>("class A { void f() { return\n } }\n", "A.groovy", |m| {
450            assert_eq!(m.tokens.tokens_sum(), 11);
451        });
452    }
453
454    /// Total token count for `source`, analysed byte-for-byte.
455    ///
456    /// Restricted to `Tokens` so the deep-nesting case below measures
457    /// this metric and not the rest of the walk. When it was written,
458    /// `cognitive`'s own parent lookups (#1062) dominated that case and
459    /// would have misattributed a regression; they are linear now, but
460    /// isolating the metric under test is still what makes the reading
461    /// mean something.
462    #[cfg(feature = "rust")]
463    fn tokens_of(source: &str) -> u64 {
464        metrics_verbatim(
465            crate::LANG::Rust,
466            source.as_bytes(),
467            crate::MetricsOptions::default().with_only(&[crate::Metric::Tokens]),
468        )
469        .tokens
470        .tokens_sum()
471    }
472
473    #[cfg(feature = "rust")]
474    fn nested_parens(depth: usize) -> String {
475        format!(
476            "fn f() -> i32 {{ {}1{} }}\n",
477            "(".repeat(depth),
478            ")".repeat(depth)
479        )
480    }
481
482    /// The token count holds thousands of levels deep (#1052).
483    ///
484    /// The metric used to walk each leaf's ancestor chain to decide
485    /// comment membership. `Node::parent` is itself `O(depth)`, so that
486    /// cost `O(leaves × depth²)`: depth 1000 took ~19 s and depth 2000
487    /// over two minutes. The walker now propagates the flag down the
488    /// traversal in `O(1)` per node, and this runs in milliseconds.
489    ///
490    /// The count is asserted by formula rather than hand-counted: each
491    /// added paren pair contributes exactly two leaves, so the delta
492    /// between consecutive depths pins the metric without depending on
493    /// how the grammar tokenises the surrounding function.
494    ///
495    /// Counts only. The pre-#1052 implementation produced these *same*
496    /// counts, only quadratically, so this test cannot tell the two
497    /// apart — that job belongs to the `tokens/nested-paren` probe in
498    /// the benchmark harness (#1068), which fits an exponent across
499    /// three depths: `cargo bench -p big-code-analysis-bench --bench
500    /// scaling`. The wall-clock budget that used to sit here was
501    /// calibrated to one machine and this suite also runs under `cargo
502    /// llvm-cov` and on shared Windows / macOS runners; the equivalent
503    /// assertion in `cognitive` produced false failures in four
504    /// separate environments before it was retired.
505    #[cfg(feature = "rust")]
506    #[test]
507    fn tokens_count_holds_at_depth() {
508        let shallow = tokens_of(&nested_parens(1));
509        assert_eq!(tokens_of(&nested_parens(2)), shallow + 2);
510        assert_eq!(tokens_of(&nested_parens(2000)), shallow + 2 * 1999);
511    }
512
513    /// A comment deep in the tree still contributes nothing.
514    ///
515    /// Honest scope: the comment's *own* subtree is only one level deep
516    /// (`line_comment` → marker / `doc_comment`), so the enclosing block
517    /// nesting exercises the walker's inheritance rather than any extra
518    /// level of comment-internal structure —
519    /// `rust_tokens_doc_comments_excluded` already covers the latter at
520    /// depth 1. What this adds is the anchored differential below: a
521    /// count that would not survive an `in_comment` wired to a constant.
522    #[cfg(feature = "rust")]
523    #[test]
524    fn rust_tokens_comment_excluded_at_depth() {
525        let deep_block = format!(
526            "fn f() {{ {}let x = 1;{} }}\n",
527            "{ ".repeat(50),
528            " }".repeat(50)
529        );
530        let with_doc = deep_block.replace("let x = 1;", "/// doc\nlet x = 1;");
531        let baseline = tokens_of(&deep_block);
532        // Anchor the differential: without this, an `in_comment` wired
533        // to always-true would return 0 on both sides and pass.
534        assert_eq!(
535            baseline, 111,
536            // 4 (`fn f ( )`) + 2 (outer braces) + 100 (50 nested brace
537            // pairs) + 5 (`let x = 1 ;`).
538            "expected 111 tokens for the comment-free baseline"
539        );
540        assert_eq!(
541            tokens_of(&with_doc),
542            baseline,
543            "a doc comment 50 blocks deep must contribute no tokens, \
544             including its structured inner leaves"
545        );
546    }
547
548    /// Rust doc comments split into structured children whose leaves are
549    /// not themselves comment kinds (`//`, `outer_doc_comment_marker`,
550    /// `doc_comment`), so excluding only the comment node is not enough
551    /// — every leaf beneath it must be filtered too.
552    #[cfg(feature = "rust")]
553    #[test]
554    fn rust_tokens_doc_comments_excluded() {
555        check_metrics::<RustParser>(
556            "/// outer doc\n/// more doc\nfn f() { let x = 1; }",
557            "foo.rs",
558            |m| {
559                // fn, f, (, ), {, let, x, =, 1, ;, } = 11.
560                assert_eq!(m.tokens.tokens_sum(), 11);
561            },
562        );
563        check_metrics::<RustParser>("fn f() { let x = 1; }", "foo.rs", |m| {
564            assert_eq!(m.tokens.tokens_sum(), 11);
565        });
566    }
567
568    // -- Per-language smoke tests --------------------------------------
569    //
570    // Lesson 1 (`docs/development/lessons_learned.md`): every supported
571    // language must have a positive test that asserts non-zero tokens
572    // on real source. Catches the silent-zero regression where a
573    // metric is registered but never fires. `check_metrics` takes a
574    // `fn` pointer so each test inlines its assertion directly.
575
576    #[cfg(feature = "python")]
577    #[test]
578    fn smoke_python() {
579        check_metrics::<PythonParser>("x = 1\n", "foo.py", |m| {
580            assert!(m.tokens.tokens_sum() > 0);
581        });
582    }
583
584    #[cfg(feature = "rust")]
585    #[test]
586    fn smoke_rust() {
587        check_metrics::<RustParser>("fn f() { let x = 1; }", "foo.rs", |m| {
588            assert!(m.tokens.tokens_sum() > 0);
589        });
590    }
591
592    #[cfg(feature = "cpp")]
593    #[test]
594    fn smoke_cpp() {
595        check_metrics::<CppParser>("int x = 1;", "foo.cpp", |m| {
596            assert!(m.tokens.tokens_sum() > 0);
597        });
598    }
599
600    #[cfg(feature = "java")]
601    #[test]
602    fn smoke_java() {
603        check_metrics::<JavaParser>("class A { int x = 1; }", "A.java", |m| {
604            assert!(m.tokens.tokens_sum() > 0);
605        });
606    }
607
608    #[cfg(feature = "csharp")]
609    #[test]
610    fn smoke_csharp() {
611        check_metrics::<CsharpParser>("class A { int X = 1; }", "A.cs", |m| {
612            assert!(m.tokens.tokens_sum() > 0);
613        });
614    }
615
616    #[cfg(feature = "javascript")]
617    #[test]
618    fn smoke_javascript() {
619        check_metrics::<JavascriptParser>("let x = 1;", "foo.js", |m| {
620            assert!(m.tokens.tokens_sum() > 0);
621        });
622    }
623
624    #[cfg(feature = "mozjs")]
625    #[test]
626    fn smoke_mozjs() {
627        check_metrics::<MozjsParser>("let x = 1;", "foo.js", |m| {
628            assert!(m.tokens.tokens_sum() > 0);
629        });
630    }
631
632    #[cfg(feature = "typescript")]
633    #[test]
634    fn smoke_typescript() {
635        check_metrics::<TypescriptParser>("const x: number = 1;", "foo.ts", |m| {
636            assert!(m.tokens.tokens_sum() > 0);
637        });
638    }
639
640    #[cfg(feature = "typescript")]
641    #[test]
642    fn smoke_tsx() {
643        check_metrics::<TsxParser>("const x: number = 1;", "foo.tsx", |m| {
644            assert!(m.tokens.tokens_sum() > 0);
645        });
646    }
647
648    #[cfg(feature = "go")]
649    #[test]
650    fn smoke_go() {
651        check_metrics::<GoParser>("package main\nfunc f() {}", "foo.go", |m| {
652            assert!(m.tokens.tokens_sum() > 0);
653        });
654    }
655
656    #[cfg(feature = "kotlin")]
657    #[test]
658    fn smoke_kotlin() {
659        check_metrics::<KotlinParser>("fun f(): Int = 1", "foo.kt", |m| {
660            assert!(m.tokens.tokens_sum() > 0);
661        });
662    }
663
664    #[cfg(feature = "lua")]
665    #[test]
666    fn smoke_lua() {
667        check_metrics::<LuaParser>("local x = 1", "foo.lua", |m| {
668            assert!(m.tokens.tokens_sum() > 0);
669        });
670    }
671
672    #[cfg(feature = "bash")]
673    #[test]
674    fn smoke_bash() {
675        check_metrics::<BashParser>("x=1", "foo.sh", |m| {
676            assert!(m.tokens.tokens_sum() > 0);
677        });
678    }
679
680    #[cfg(feature = "tcl")]
681    #[test]
682    fn smoke_tcl() {
683        check_metrics::<TclParser>("set x 1", "foo.tcl", |m| {
684            assert!(m.tokens.tokens_sum() > 0);
685        });
686    }
687
688    #[cfg(feature = "perl")]
689    #[test]
690    fn smoke_perl() {
691        check_metrics::<PerlParser>("my $x = 1;", "foo.pl", |m| {
692            assert!(m.tokens.tokens_sum() > 0);
693        });
694    }
695
696    #[cfg(feature = "php")]
697    #[test]
698    fn smoke_php() {
699        check_metrics::<PhpParser>("<?php $x = 1;", "foo.php", |m| {
700            assert!(m.tokens.tokens_sum() > 0);
701        });
702    }
703
704    #[cfg(any(
705        feature = "c",
706        feature = "c-family-helpers",
707        feature = "cpp",
708        feature = "mozcpp"
709    ))]
710    #[test]
711    fn smoke_preproc() {
712        check_metrics::<PreprocParser>("#define FOO 1\n", "foo.h", |m| {
713            assert!(m.tokens.tokens_sum() > 0);
714        });
715    }
716
717    #[cfg(any(
718        feature = "c",
719        feature = "c-family-helpers",
720        feature = "cpp",
721        feature = "mozcpp"
722    ))]
723    #[test]
724    fn smoke_ccomment() {
725        // Ccomment's grammar parses bare C source; non-comment text
726        // produces non-comment leaves.
727        check_metrics::<CcommentParser>("int x = 1;", "foo.c", |m| {
728            assert!(m.tokens.tokens_sum() > 0);
729        });
730    }
731
732    #[cfg(feature = "c")]
733    #[test]
734    fn smoke_c() {
735        check_metrics::<CParser>("int x = 1;\n", "foo.c", |m| {
736            assert!(m.tokens.tokens_sum() > 0);
737        });
738    }
739
740    #[cfg(feature = "objc")]
741    #[test]
742    fn smoke_objc() {
743        check_metrics::<ObjcParser>("int x = 1;\n", "foo.m", |m| {
744            assert!(m.tokens.tokens_sum() > 0);
745        });
746    }
747
748    #[cfg(feature = "elixir")]
749    #[test]
750    fn smoke_elixir() {
751        check_metrics::<ElixirParser>("defmodule Foo do\n  :ok\nend\n", "foo.ex", |m| {
752            assert!(m.tokens.tokens_sum() > 0);
753        });
754    }
755
756    #[cfg(feature = "ruby")]
757    #[test]
758    fn smoke_ruby() {
759        check_metrics::<RubyParser>("def foo\n  a = 1\nend\n", "foo.rb", |m| {
760            assert!(m.tokens.tokens_sum() > 0);
761        });
762    }
763
764    #[cfg(feature = "irules")]
765    #[test]
766    fn smoke_irules() {
767        check_metrics::<IrulesParser>("when X {\n    set x 1\n}\n", "foo.irule", |m| {
768            assert!(m.tokens.tokens_sum() > 0);
769        });
770    }
771}