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    #[test]
217    fn python_tokens_exact_count() {
218        check_metrics::<PythonParser>("def foo(x): return x", "foo.py", |metric| {
219            assert_eq!(metric.tokens.tokens_sum(), 8);
220            assert!(metric.tokens.tokens_max() >= 7);
221        });
222    }
223
224    /// Adding a Python comment must not change the token count.
225    #[test]
226    fn python_tokens_comments_excluded() {
227        check_metrics::<PythonParser>(
228            "def foo(x): return x  # explanation\n# header\n",
229            "foo.py",
230            |metric| {
231                assert_eq!(metric.tokens.tokens_sum(), 8);
232            },
233        );
234    }
235
236    /// Blank lines and indentation must not change the token count.
237    #[test]
238    fn python_tokens_whitespace_excluded() {
239        check_metrics::<PythonParser>(
240            "\n\n    def foo(x):\n        return x\n\n",
241            "foo.py",
242            |metric| {
243                assert_eq!(metric.tokens.tokens_sum(), 8);
244            },
245        );
246    }
247
248    /// Tokens must exceed Halstead `N1 + N2` for code containing
249    /// punctuation Halstead skips. Guards against accidental Halstead
250    /// reuse.
251    #[test]
252    fn python_tokens_distinct_from_halstead() {
253        check_tokens_and_halstead::<PythonParser>(
254            "def foo(x): return (x + 1)",
255            "foo.py",
256            |metric| {
257                let halstead_total =
258                    metric.halstead.total_operators() + metric.halstead.total_operands();
259                assert!(
260                    metric.tokens.tokens_sum() > halstead_total,
261                    "expected tokens ({}) > halstead N1+N2 ({}); punctuation \
262                     like `(`, `)`, `:` should contribute to tokens but not Halstead",
263                    metric.tokens.tokens_sum(),
264                    halstead_total,
265                );
266            },
267        );
268    }
269
270    /// Inner functions get attributed to their innermost scope. For
271    /// `def outer(): def inner(): return 1`, the inner scope owns
272    /// `def, inner, (, ), :, return, 1` = 7 tokens; the outer scope
273    /// owns `def, outer, (, ), :` = 5; the unit owns 0 directly.
274    /// Asserting the exact `tokens_max` is what catches an attribution
275    /// regression — a broken implementation that credited all 12
276    /// tokens to one scope would still pass `max <= sum`.
277    #[test]
278    fn python_tokens_nested_attribution() {
279        check_metrics::<PythonParser>(
280            "def outer():\n    def inner():\n        return 1\n",
281            "foo.py",
282            |metric| {
283                assert_eq!(metric.tokens.tokens_sum(), 12);
284                assert_eq!(metric.tokens.tokens_max(), 7);
285                assert_eq!(metric.tokens.tokens_min(), 0);
286            },
287        );
288    }
289
290    /// C++ `/* … */` block comments must not contribute.
291    /// Same fixture with and without comment yields the same count.
292    #[test]
293    fn cpp_tokens_block_comments_excluded() {
294        check_metrics::<CppParser>(
295            "int foo(int x) { /* multi\n   line */ return x; }",
296            "foo.cpp",
297            |m| {
298                // Leaves outside the comment:
299                // int, foo, (, int, x, ), {, return, x, ;, } = 11.
300                assert_eq!(m.tokens.tokens_sum(), 11);
301            },
302        );
303        check_metrics::<CppParser>("int foo(int x) { return x; }", "foo.cpp", |m| {
304            assert_eq!(m.tokens.tokens_sum(), 11);
305        });
306    }
307
308    /// C++ `// …` line comments must not contribute, matching the Python
309    /// hand-counted style.  Leaves outside the comment:
310    /// `int`, `x`, `=`, `1`, `;` = 5.
311    #[test]
312    fn cpp_tokens_line_comments_excluded() {
313        check_metrics::<CppParser>("int x = 1; // a one-line comment\n", "foo.cpp", |m| {
314            assert_eq!(m.tokens.tokens_sum(), 5);
315        });
316        check_metrics::<CppParser>("int x = 1;\n", "foo.cpp", |m| {
317            assert_eq!(m.tokens.tokens_sum(), 5);
318        });
319    }
320
321    /// Whitespace and blank lines must not contribute to the token count
322    /// (mirrors `python_tokens_whitespace_excluded`).
323    #[test]
324    fn cpp_tokens_whitespace_excluded() {
325        check_metrics::<CppParser>("\n\nint foo(int x) {\n    return x;\n}\n", "foo.cpp", |m| {
326            // int, foo, (, int, x, ), {, return, x, ;, } = 11.
327            assert_eq!(m.tokens.tokens_sum(), 11);
328        });
329    }
330
331    /// Tokens count punctuation that Halstead skips (parentheses, braces,
332    /// semicolons), so `tokens_sum` must exceed `N1 + N2` for a fixture
333    /// with significant punctuation.  Mirrors
334    /// `python_tokens_distinct_from_halstead`.
335    #[test]
336    fn cpp_tokens_distinct_from_halstead() {
337        check_tokens_and_halstead::<CppParser>(
338            "int foo(int x) { return (x + 1); }",
339            "foo.cpp",
340            |m| {
341                let halstead_total = m.halstead.total_operators() + m.halstead.total_operands();
342                assert!(
343                    m.tokens.tokens_sum() > halstead_total,
344                    "expected tokens ({}) > halstead N1+N2 ({}); punctuation like \
345                     `(`, `)`, `{{`, `}}` and `;` should contribute to tokens but not Halstead",
346                    m.tokens.tokens_sum(),
347                    halstead_total,
348                );
349            },
350        );
351    }
352
353    /// A C++ struct method and a free function each open their own
354    /// `FuncSpace`, so the leaves of
355    /// `struct S { int method(int a) { return a + 1; } }; int outer() { return 2; }`
356    /// split across scopes: `method` owns 13 and `outer` owns 9, with the
357    /// `struct`/unit framing owning the rest of the 27-token total.
358    /// Asserting the exact `tokens_max` (13 — the largest single scope,
359    /// strictly below the sum of 27) is what catches an attribution
360    /// regression: a broken implementation that credited every leaf to one
361    /// scope would raise `tokens_max` to 27 while still passing
362    /// `max <= sum`, mirroring the Python sibling's exact-max guard.
363    #[test]
364    fn cpp_tokens_nested_attribution() {
365        check_metrics::<CppParser>(
366            "struct S {\n    int method(int a) { return a + 1; }\n};\nint outer() { return 2; }\n",
367            "foo.cpp",
368            |m| {
369                assert_eq!(m.tokens.tokens_sum(), 27);
370                assert_eq!(m.tokens.tokens_max(), 13);
371                assert_eq!(m.tokens.tokens_min(), 1);
372            },
373        );
374    }
375
376    /// Java `// …` line comments must not contribute.
377    #[test]
378    fn java_tokens_line_comments_excluded() {
379        check_metrics::<JavaParser>(
380            "class A { void foo() { // hi\n return; } }",
381            "A.java",
382            |m| {
383                // class, A, {, void, foo, (, ), {, return, ;, }, } = 12.
384                assert_eq!(m.tokens.tokens_sum(), 12);
385            },
386        );
387        check_metrics::<JavaParser>("class A { void foo() { return; } }", "A.java", |m| {
388            assert_eq!(m.tokens.tokens_sum(), 12);
389        });
390    }
391
392    #[test]
393    fn groovy_tokens_line_comments_excluded() {
394        // Groovy mirror — `// …` line comments must not contribute.
395        check_metrics::<GroovyParser>(
396            "class A { void foo() { // hi\n return\n } }",
397            "A.groovy",
398            |m| {
399                // class, A, {, void, foo, (, ), {, return, newline,
400                // }, } = 11 tokens (Groovy's newline acts as the
401                // statement terminator that Java spells `;`).
402                assert_eq!(m.tokens.tokens_sum(), 11);
403            },
404        );
405    }
406
407    /// JS-family `<!-- -->` Annex-B `html_comment` leaves must not
408    /// contribute tokens — they classify as comments now (#697). The
409    /// count must match the comment-free source exactly.
410    #[test]
411    fn javascript_tokens_html_comment_excluded() {
412        check_metrics::<JavascriptParser>("<!-- hi -->\nlet x = 1;\n", "foo.js", |m| {
413            // let, x, =, 1, ; = 5.
414            assert_eq!(m.tokens.tokens_sum(), 5);
415        });
416        check_metrics::<JavascriptParser>("let x = 1;\n", "foo.js", |m| {
417            assert_eq!(m.tokens.tokens_sum(), 5);
418        });
419    }
420
421    /// Groovy `/** … */` `groovydoc_comment` leaves must not contribute
422    /// tokens (#697 — `is_comment` previously missed this kind even
423    /// though `Loc` counted it).
424    #[test]
425    fn groovy_tokens_groovydoc_excluded() {
426        check_metrics::<GroovyParser>(
427            "/** doc */\nclass A { void f() { return\n } }\n",
428            "A.groovy",
429            |m| {
430                // class, A, {, void, f, (, ), {, return, newline,
431                // }, } = 11.
432                assert_eq!(m.tokens.tokens_sum(), 11);
433            },
434        );
435        check_metrics::<GroovyParser>("class A { void f() { return\n } }\n", "A.groovy", |m| {
436            assert_eq!(m.tokens.tokens_sum(), 11);
437        });
438    }
439
440    /// Total token count for `source`, analysed byte-for-byte.
441    ///
442    /// Restricted to `Tokens` so the deep-nesting case below measures
443    /// this metric and not the rest of the walk. When it was written,
444    /// `cognitive`'s own parent lookups (#1062) dominated that case and
445    /// would have misattributed a regression; they are linear now, but
446    /// isolating the metric under test is still what makes the reading
447    /// mean something.
448    fn tokens_of(source: &str) -> u64 {
449        metrics_verbatim(
450            crate::LANG::Rust,
451            source.as_bytes(),
452            crate::MetricsOptions::default().with_only(&[crate::Metric::Tokens]),
453        )
454        .tokens
455        .tokens_sum()
456    }
457
458    fn nested_parens(depth: usize) -> String {
459        format!(
460            "fn f() -> i32 {{ {}1{} }}\n",
461            "(".repeat(depth),
462            ")".repeat(depth)
463        )
464    }
465
466    /// The token count holds thousands of levels deep (#1052).
467    ///
468    /// The metric used to walk each leaf's ancestor chain to decide
469    /// comment membership. `Node::parent` is itself `O(depth)`, so that
470    /// cost `O(leaves × depth²)`: depth 1000 took ~19 s and depth 2000
471    /// over two minutes. The walker now propagates the flag down the
472    /// traversal in `O(1)` per node, and this runs in milliseconds.
473    ///
474    /// The count is asserted by formula rather than hand-counted: each
475    /// added paren pair contributes exactly two leaves, so the delta
476    /// between consecutive depths pins the metric without depending on
477    /// how the grammar tokenises the surrounding function.
478    ///
479    /// Counts only. The pre-#1052 implementation produced these *same*
480    /// counts, only quadratically, so this test cannot tell the two
481    /// apart — that job belongs to the `tokens/nested-paren` probe in
482    /// the benchmark harness (#1068), which fits an exponent across
483    /// three depths: `cargo bench -p big-code-analysis-bench --bench
484    /// scaling`. The wall-clock budget that used to sit here was
485    /// calibrated to one machine and this suite also runs under `cargo
486    /// llvm-cov` and on shared Windows / macOS runners; the equivalent
487    /// assertion in `cognitive` produced false failures in four
488    /// separate environments before it was retired.
489    #[test]
490    fn tokens_count_holds_at_depth() {
491        let shallow = tokens_of(&nested_parens(1));
492        assert_eq!(tokens_of(&nested_parens(2)), shallow + 2);
493        assert_eq!(tokens_of(&nested_parens(2000)), shallow + 2 * 1999);
494    }
495
496    /// A comment deep in the tree still contributes nothing.
497    ///
498    /// Honest scope: the comment's *own* subtree is only one level deep
499    /// (`line_comment` → marker / `doc_comment`), so the enclosing block
500    /// nesting exercises the walker's inheritance rather than any extra
501    /// level of comment-internal structure —
502    /// `rust_tokens_doc_comments_excluded` already covers the latter at
503    /// depth 1. What this adds is the anchored differential below: a
504    /// count that would not survive an `in_comment` wired to a constant.
505    #[test]
506    fn rust_tokens_comment_excluded_at_depth() {
507        let deep_block = format!(
508            "fn f() {{ {}let x = 1;{} }}\n",
509            "{ ".repeat(50),
510            " }".repeat(50)
511        );
512        let with_doc = deep_block.replace("let x = 1;", "/// doc\nlet x = 1;");
513        let baseline = tokens_of(&deep_block);
514        // Anchor the differential: without this, an `in_comment` wired
515        // to always-true would return 0 on both sides and pass.
516        assert_eq!(
517            baseline, 111,
518            // 4 (`fn f ( )`) + 2 (outer braces) + 100 (50 nested brace
519            // pairs) + 5 (`let x = 1 ;`).
520            "expected 111 tokens for the comment-free baseline"
521        );
522        assert_eq!(
523            tokens_of(&with_doc),
524            baseline,
525            "a doc comment 50 blocks deep must contribute no tokens, \
526             including its structured inner leaves"
527        );
528    }
529
530    /// Rust doc comments split into structured children whose leaves are
531    /// not themselves comment kinds (`//`, `outer_doc_comment_marker`,
532    /// `doc_comment`), so excluding only the comment node is not enough
533    /// — every leaf beneath it must be filtered too.
534    #[test]
535    fn rust_tokens_doc_comments_excluded() {
536        check_metrics::<RustParser>(
537            "/// outer doc\n/// more doc\nfn f() { let x = 1; }",
538            "foo.rs",
539            |m| {
540                // fn, f, (, ), {, let, x, =, 1, ;, } = 11.
541                assert_eq!(m.tokens.tokens_sum(), 11);
542            },
543        );
544        check_metrics::<RustParser>("fn f() { let x = 1; }", "foo.rs", |m| {
545            assert_eq!(m.tokens.tokens_sum(), 11);
546        });
547    }
548
549    // -- Per-language smoke tests --------------------------------------
550    //
551    // Lesson 1 (`docs/development/lessons_learned.md`): every supported
552    // language must have a positive test that asserts non-zero tokens
553    // on real source. Catches the silent-zero regression where a
554    // metric is registered but never fires. `check_metrics` takes a
555    // `fn` pointer so each test inlines its assertion directly.
556
557    #[test]
558    fn smoke_python() {
559        check_metrics::<PythonParser>("x = 1\n", "foo.py", |m| {
560            assert!(m.tokens.tokens_sum() > 0);
561        });
562    }
563
564    #[test]
565    fn smoke_rust() {
566        check_metrics::<RustParser>("fn f() { let x = 1; }", "foo.rs", |m| {
567            assert!(m.tokens.tokens_sum() > 0);
568        });
569    }
570
571    #[test]
572    fn smoke_cpp() {
573        check_metrics::<CppParser>("int x = 1;", "foo.cpp", |m| {
574            assert!(m.tokens.tokens_sum() > 0);
575        });
576    }
577
578    #[test]
579    fn smoke_java() {
580        check_metrics::<JavaParser>("class A { int x = 1; }", "A.java", |m| {
581            assert!(m.tokens.tokens_sum() > 0);
582        });
583    }
584
585    #[test]
586    fn smoke_csharp() {
587        check_metrics::<CsharpParser>("class A { int X = 1; }", "A.cs", |m| {
588            assert!(m.tokens.tokens_sum() > 0);
589        });
590    }
591
592    #[test]
593    fn smoke_javascript() {
594        check_metrics::<JavascriptParser>("let x = 1;", "foo.js", |m| {
595            assert!(m.tokens.tokens_sum() > 0);
596        });
597    }
598
599    #[test]
600    fn smoke_mozjs() {
601        check_metrics::<MozjsParser>("let x = 1;", "foo.js", |m| {
602            assert!(m.tokens.tokens_sum() > 0);
603        });
604    }
605
606    #[test]
607    fn smoke_typescript() {
608        check_metrics::<TypescriptParser>("const x: number = 1;", "foo.ts", |m| {
609            assert!(m.tokens.tokens_sum() > 0);
610        });
611    }
612
613    #[test]
614    fn smoke_tsx() {
615        check_metrics::<TsxParser>("const x: number = 1;", "foo.tsx", |m| {
616            assert!(m.tokens.tokens_sum() > 0);
617        });
618    }
619
620    #[test]
621    fn smoke_go() {
622        check_metrics::<GoParser>("package main\nfunc f() {}", "foo.go", |m| {
623            assert!(m.tokens.tokens_sum() > 0);
624        });
625    }
626
627    #[test]
628    fn smoke_kotlin() {
629        check_metrics::<KotlinParser>("fun f(): Int = 1", "foo.kt", |m| {
630            assert!(m.tokens.tokens_sum() > 0);
631        });
632    }
633
634    #[test]
635    fn smoke_lua() {
636        check_metrics::<LuaParser>("local x = 1", "foo.lua", |m| {
637            assert!(m.tokens.tokens_sum() > 0);
638        });
639    }
640
641    #[test]
642    fn smoke_bash() {
643        check_metrics::<BashParser>("x=1", "foo.sh", |m| {
644            assert!(m.tokens.tokens_sum() > 0);
645        });
646    }
647
648    #[test]
649    fn smoke_tcl() {
650        check_metrics::<TclParser>("set x 1", "foo.tcl", |m| {
651            assert!(m.tokens.tokens_sum() > 0);
652        });
653    }
654
655    #[test]
656    fn smoke_perl() {
657        check_metrics::<PerlParser>("my $x = 1;", "foo.pl", |m| {
658            assert!(m.tokens.tokens_sum() > 0);
659        });
660    }
661
662    #[test]
663    fn smoke_php() {
664        check_metrics::<PhpParser>("<?php $x = 1;", "foo.php", |m| {
665            assert!(m.tokens.tokens_sum() > 0);
666        });
667    }
668
669    #[test]
670    fn smoke_preproc() {
671        check_metrics::<PreprocParser>("#define FOO 1\n", "foo.h", |m| {
672            assert!(m.tokens.tokens_sum() > 0);
673        });
674    }
675
676    #[test]
677    fn smoke_ccomment() {
678        // Ccomment's grammar parses bare C source; non-comment text
679        // produces non-comment leaves.
680        check_metrics::<CcommentParser>("int x = 1;", "foo.c", |m| {
681            assert!(m.tokens.tokens_sum() > 0);
682        });
683    }
684
685    #[test]
686    fn smoke_c() {
687        check_metrics::<CParser>("int x = 1;\n", "foo.c", |m| {
688            assert!(m.tokens.tokens_sum() > 0);
689        });
690    }
691
692    #[test]
693    fn smoke_objc() {
694        check_metrics::<ObjcParser>("int x = 1;\n", "foo.m", |m| {
695            assert!(m.tokens.tokens_sum() > 0);
696        });
697    }
698
699    #[test]
700    fn smoke_elixir() {
701        check_metrics::<ElixirParser>("defmodule Foo do\n  :ok\nend\n", "foo.ex", |m| {
702            assert!(m.tokens.tokens_sum() > 0);
703        });
704    }
705
706    #[test]
707    fn smoke_ruby() {
708        check_metrics::<RubyParser>("def foo\n  a = 1\nend\n", "foo.rb", |m| {
709            assert!(m.tokens.tokens_sum() > 0);
710        });
711    }
712
713    #[test]
714    fn smoke_irules() {
715        check_metrics::<IrulesParser>("when X {\n    set x 1\n}\n", "foo.irule", |m| {
716            assert!(m.tokens.tokens_sum() > 0);
717        });
718    }
719}