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}