Skip to main content

testing_conventions/
one_function.rs

1//! The `unit one-function-per-file` check: a source file holds at most one module-scope
2//! function whose body runs longer than the configured threshold.
3
4use std::path::{Path, PathBuf};
5
6use anyhow::{anyhow, Context, Result};
7use oxc::allocator::Allocator;
8use oxc::ast::ast::{
9    Declaration, ExportDefaultDeclarationKind, Expression, Statement, VariableDeclaration,
10};
11use oxc::parser::Parser;
12use oxc::span::{GetSpan, SourceType};
13use rustpython_ast::Ranged;
14use rustpython_parser::ast::{self, Constant, Expr, Stmt};
15use rustpython_parser::text_size::TextSize;
16use rustpython_parser::Parse;
17use syn::spanned::Spanned;
18
19pub use crate::violation::Violation;
20
21use crate::colocated_test::Language;
22
23/// The rule id reported for a function sharing its file with another over the threshold.
24const RULE: &str = "one-function-per-file";
25
26/// A module-scope function: its name, declaration line, and body code-line count.
27#[derive(Debug, PartialEq, Eq)]
28struct Function {
29    name: String,
30    line: usize,
31    body_lines: usize,
32}
33
34/// A violation for every module-scope function under `root` past the first whose body runs
35/// longer than `max_lines`, sorted by `(file, line)`, and the number of files scanned to find
36/// them. The first over-threshold function in a file holds it; each later one is a violation
37/// naming both.
38pub fn find_violations(
39    root: impl AsRef<Path>,
40    language: Language,
41    max_lines: u32,
42) -> Result<(Vec<Violation>, usize)> {
43    let root = root.as_ref();
44    let files = source_files(root, language)?;
45
46    let mut violations = Vec::new();
47    for file in &files {
48        let source = std::fs::read_to_string(file)
49            .with_context(|| format!("reading source file `{}`", file.display()))?;
50        let mut over = functions(&source, file, language)?
51            .into_iter()
52            .filter(|function| function.body_lines > max_lines as usize);
53        let Some(holder) = over.next() else {
54            continue;
55        };
56        for extra in over {
57            violations.push(Violation {
58                file: file.clone(),
59                line: extra.line,
60                rule: RULE,
61                message: format!(
62                    "`{}` runs {} lines, and `{}` already holds this file; \
63                     move it to its own module",
64                    extra.name, extra.body_lines, holder.name
65                ),
66            });
67        }
68    }
69    Ok((violations, files.len()))
70}
71
72/// Every file under `root` the rule judges, sorted: the language's source files, minus the
73/// test and support files and the suite tiers under `<package root>/tests/`.
74fn source_files(root: &Path, language: Language) -> Result<Vec<PathBuf>> {
75    let mut files = Vec::new();
76    if language == Language::Rust {
77        crate::colocated_test::collect_rust_source_files(root, &mut files)?;
78        files.sort();
79        return Ok(files);
80    }
81    crate::colocated_test::collect_files(root, language, &mut files)?;
82    let manifest = match language {
83        Language::Python => "pyproject.toml",
84        _ => "package.json",
85    };
86    if let Some(tests) = crate::tiers::suite_tests_dir(root, manifest) {
87        files.retain(|file| !file.starts_with(&tests));
88    }
89    files.retain(|file| !language.is_test(file) && !language.is_support(file));
90    files.sort();
91    Ok(files)
92}
93
94/// The module-scope functions `source` (the file at `path`) declares, in source order.
95fn functions(source: &str, path: &Path, language: Language) -> Result<Vec<Function>> {
96    match language {
97        Language::Python => python_functions(source, path),
98        Language::TypeScript => typescript_functions(source, path),
99        Language::Rust => rust_functions(source, path),
100    }
101}
102
103/// The module-level `def` / `async def` statements of a Python module.
104fn python_functions(source: &str, path: &Path) -> Result<Vec<Function>> {
105    let suite = ast::Suite::parse(source, &path.to_string_lossy())
106        .map_err(|err| anyhow!("parsing `{}`: {err}", path.display()))?;
107    let lines: Vec<&str> = source.lines().collect();
108    let mut found = Vec::new();
109    for statement in &suite {
110        let (name, body, range) = match statement {
111            Stmt::FunctionDef(node) => (&node.name, &node.body, node.range),
112            Stmt::AsyncFunctionDef(node) => (&node.name, &node.body, node.range),
113            _ => continue,
114        };
115        found.push(Function {
116            name: name.to_string(),
117            line: line_of(source, range.start()),
118            body_lines: python_body_lines(source, &lines, body),
119        });
120    }
121    Ok(found)
122}
123
124/// The code lines of a Python function body, the docstring excluded.
125fn python_body_lines(source: &str, lines: &[&str], body: &[Stmt]) -> usize {
126    let start = body.iter().position(|statement| !is_docstring(statement));
127    let Some(start) = start else {
128        return 0;
129    };
130    let first = line_of(source, body[start].range().start());
131    let last = line_of(source, body[body.len() - 1].range().end());
132    code_lines(lines, first, last, Comment::Hash)
133}
134
135/// `true` for a bare string-literal statement — a docstring wherever it leads a body.
136fn is_docstring(statement: &Stmt) -> bool {
137    let Stmt::Expr(node) = statement else {
138        return false;
139    };
140    matches!(
141        node.value.as_ref(),
142        Expr::Constant(constant) if matches!(constant.value, Constant::Str(_))
143    )
144}
145
146/// The module-scope functions of a TypeScript module: `function` declarations and
147/// bindings initialized with an arrow or function expression, `export` or not.
148fn typescript_functions(source: &str, path: &Path) -> Result<Vec<Function>> {
149    let allocator = Allocator::default();
150    let source_type = SourceType::from_path(path)
151        .map_err(|err| anyhow!("reading the source type of `{}`: {err}", path.display()))?;
152    let parsed = Parser::new(&allocator, source, source_type).parse();
153    if parsed.panicked || !parsed.diagnostics.is_empty() {
154        return Err(anyhow!("parsing `{}`", path.display()));
155    }
156    let lines: Vec<&str> = source.lines().collect();
157    let mut found = Vec::new();
158    for statement in &parsed.program.body {
159        push_ts_statement(source, &lines, statement, &mut found);
160    }
161    Ok(found)
162}
163
164/// Record whatever module-scope functions `statement` declares. Only the four statement
165/// kinds that can carry one at module scope contribute; everything else is skipped.
166fn push_ts_statement(source: &str, lines: &[&str], statement: &Statement, out: &mut Vec<Function>) {
167    match statement {
168        Statement::FunctionDeclaration(node) => push_ts_function(source, lines, node, out),
169        Statement::VariableDeclaration(node) => push_ts_bindings(source, lines, node, out),
170        Statement::ExportNamedDeclaration(node) => {
171            push_ts_declaration(source, lines, node.declaration.as_ref(), out);
172        }
173        Statement::ExportDefaultDeclaration(node) => {
174            push_ts_default(source, lines, &node.declaration, out);
175        }
176        _ => {}
177    }
178}
179
180/// Record whatever module-scope functions an `export` clause's declaration carries. An
181/// `export { … }` re-export declares nothing of its own.
182fn push_ts_declaration(
183    source: &str,
184    lines: &[&str],
185    declaration: Option<&Declaration>,
186    out: &mut Vec<Function>,
187) {
188    match declaration {
189        Some(Declaration::FunctionDeclaration(inner)) => {
190            push_ts_function(source, lines, inner, out);
191        }
192        Some(Declaration::VariableDeclaration(inner)) => {
193            push_ts_bindings(source, lines, inner, out);
194        }
195        _ => {}
196    }
197}
198
199/// Record the function an `export default` carries. It carries one only when the default is a
200/// declaration rather than an expression.
201fn push_ts_default(
202    source: &str,
203    lines: &[&str],
204    declaration: &ExportDefaultDeclarationKind,
205    out: &mut Vec<Function>,
206) {
207    if let ExportDefaultDeclarationKind::FunctionDeclaration(inner) = declaration {
208        push_ts_function(source, lines, inner, out);
209    }
210}
211
212/// Record a TypeScript `function` declaration; a bodyless overload signature is skipped.
213fn push_ts_function(
214    source: &str,
215    lines: &[&str],
216    node: &oxc::ast::ast::Function,
217    out: &mut Vec<Function>,
218) {
219    let Some(body) = &node.body else {
220        return;
221    };
222    let name = node
223        .id
224        .as_ref()
225        .map(|id| id.name.to_string())
226        .unwrap_or_else(|| "default".to_string());
227    out.push(Function {
228        name,
229        line: line_of(source, TextSize::from(node.span.start)),
230        body_lines: ts_body_lines(source, lines, body),
231    });
232}
233
234/// Record every `const` / `let` / `var` binding initialized with an arrow or function
235/// expression.
236fn push_ts_bindings(
237    source: &str,
238    lines: &[&str],
239    node: &VariableDeclaration,
240    out: &mut Vec<Function>,
241) {
242    for declarator in &node.declarations {
243        let body = match &declarator.init {
244            Some(Expression::ArrowFunctionExpression(arrow)) => Some(arrow.body.as_ref()),
245            Some(Expression::FunctionExpression(function)) => function.body.as_deref(),
246            _ => None,
247        };
248        let Some(body) = body else {
249            continue;
250        };
251        let Some(name) = declarator.id.get_identifier_name() else {
252            continue;
253        };
254        out.push(Function {
255            name: name.to_string(),
256            line: line_of(source, TextSize::from(declarator.span.start)),
257            body_lines: ts_body_lines(source, lines, body),
258        });
259    }
260}
261
262/// The code lines of a TypeScript function body.
263fn ts_body_lines(source: &str, lines: &[&str], body: &oxc::ast::ast::FunctionBody) -> usize {
264    let (Some(first), Some(last)) = (body.statements.first(), body.statements.last()) else {
265        return 0;
266    };
267    code_lines(
268        lines,
269        line_of(source, TextSize::from(first.span().start)),
270        line_of(source, TextSize::from(last.span().end)),
271        Comment::Slash,
272    )
273}
274
275/// The `fn` items at the top level of a Rust file.
276fn rust_functions(source: &str, path: &Path) -> Result<Vec<Function>> {
277    let ast =
278        syn::parse_file(source).map_err(|err| anyhow!("parsing `{}`: {err}", path.display()))?;
279    let lines: Vec<&str> = source.lines().collect();
280    let mut found = Vec::new();
281    for item in &ast.items {
282        let syn::Item::Fn(node) = item else {
283            continue;
284        };
285        found.push(Function {
286            name: node.sig.ident.to_string(),
287            line: node.sig.ident.span().start().line,
288            body_lines: rust_body_lines(&lines, &node.block),
289        });
290    }
291    Ok(found)
292}
293
294/// The code lines of a Rust function body — the block's statements, never the braces.
295fn rust_body_lines(lines: &[&str], block: &syn::Block) -> usize {
296    let (Some(first), Some(last)) = (block.stmts.first(), block.stmts.last()) else {
297        return 0;
298    };
299    code_lines(
300        lines,
301        first.span().start().line,
302        last.span().end().line,
303        Comment::Slash,
304    )
305}
306
307/// How a language spells a comment, for the body-line count.
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309enum Comment {
310    /// Python: `#`.
311    Hash,
312    /// TypeScript and Rust: `//`, and the `/* … */` block form.
313    Slash,
314}
315
316/// The number of lines in the inclusive 1-based range `first..=last` of `lines` that carry
317/// code — blank and comment lines don't count.
318fn code_lines(lines: &[&str], first: usize, last: usize, comment: Comment) -> usize {
319    lines
320        .iter()
321        .skip(first.saturating_sub(1))
322        .take(last.saturating_sub(first) + 1)
323        .filter(|line| {
324            let trimmed = line.trim();
325            !trimmed.is_empty() && !is_comment(trimmed, comment)
326        })
327        .count()
328}
329
330/// `true` when a trimmed line carries only a comment. The `* ` form catches a `/* … */`
331/// block's interior, which a Rust dereference never matches: no space after the star.
332fn is_comment(trimmed: &str, comment: Comment) -> bool {
333    match comment {
334        Comment::Hash => trimmed.starts_with('#'),
335        Comment::Slash => {
336            trimmed.starts_with("//")
337                || trimmed.starts_with("/*")
338                || trimmed == "*"
339                || trimmed.starts_with("* ")
340                || trimmed.starts_with("*/")
341        }
342    }
343}
344
345/// The 1-based line containing byte `offset` in `source`.
346fn line_of(source: &str, offset: TextSize) -> usize {
347    let offset = (u32::from(offset) as usize).min(source.len());
348    source.as_bytes()[..offset]
349        .iter()
350        .filter(|&&byte| byte == b'\n')
351        .count()
352        + 1
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    /// The `(name, body_lines)` pairs of a Python module's module-scope functions.
360    fn python(source: &str) -> Vec<(String, usize)> {
361        python_functions(source, Path::new("widget.py"))
362            .expect("the snippet parses")
363            .into_iter()
364            .map(|function| (function.name, function.body_lines))
365            .collect()
366    }
367
368    /// The `(name, body_lines)` pairs of a TypeScript module's module-scope functions.
369    fn typescript(source: &str) -> Vec<(String, usize)> {
370        typescript_functions(source, Path::new("widget.ts"))
371            .expect("the snippet parses")
372            .into_iter()
373            .map(|function| (function.name, function.body_lines))
374            .collect()
375    }
376
377    /// The `(name, body_lines)` pairs of a Rust file's top-level `fn` items.
378    fn rust(source: &str) -> Vec<(String, usize)> {
379        rust_functions(source, Path::new("widget.rs"))
380            .expect("the snippet parses")
381            .into_iter()
382            .map(|function| (function.name, function.body_lines))
383            .collect()
384    }
385
386    #[test]
387    fn python_counts_module_level_defs_and_their_body_lines() {
388        let found = python(
389            "def alpha(value):\n    total = value + 1\n    return total\n\n\
390             async def beta(value):\n    return value\n",
391        );
392        assert_eq!(
393            found,
394            vec![("alpha".to_string(), 2), ("beta".to_string(), 1)]
395        );
396    }
397
398    #[test]
399    fn python_skips_methods_and_nested_functions() {
400        let found = python(
401            "class Widget:\n    def grow(self):\n        self.size += 1\n        return self.size\n\n\
402             def build(values):\n    def inner(value):\n        return value * 2\n\n    return inner\n",
403        );
404        assert_eq!(found, vec![("build".to_string(), 3)]);
405    }
406
407    #[test]
408    fn python_excludes_the_docstring_blank_lines_and_comments() {
409        let found = python(
410            "def described(value):\n    \"\"\"Return the value unchanged.\"\"\"\n\
411             \x20   # the identity is the whole contract\n\n    return value\n",
412        );
413        assert_eq!(found, vec![("described".to_string(), 1)]);
414    }
415
416    #[test]
417    fn python_reports_a_decorated_function_by_name() {
418        let found = python("@cache\ndef alpha(value):\n    return value\n");
419        assert_eq!(found, vec![("alpha".to_string(), 1)]);
420    }
421
422    #[test]
423    fn python_counts_an_empty_body_as_no_lines() {
424        let found = python("def stub():\n    \"\"\"Nothing yet.\"\"\"\n");
425        assert_eq!(found, vec![("stub".to_string(), 0)]);
426    }
427
428    #[test]
429    fn typescript_counts_declarations_and_function_bound_bindings() {
430        let found = typescript(
431            "export function alpha(value: number): number {\n  const total = value + 1;\n  return total;\n}\n\
432             const beta = (value: number): number => value * 2;\n\
433             export const gamma = function (value: number): number {\n  return value;\n};\n",
434        );
435        assert_eq!(
436            found,
437            vec![
438                ("alpha".to_string(), 2),
439                ("beta".to_string(), 1),
440                ("gamma".to_string(), 1),
441            ]
442        );
443    }
444
445    #[test]
446    fn typescript_skips_methods_nested_arrows_and_non_function_bindings() {
447        let found = typescript(
448            "const SIZE = 3;\n\
449             export class Widget {\n  grow(amount: number): number {\n    return amount;\n  }\n}\n\
450             export function build(values: number[]): number[] {\n  const inner = (v: number) => v * 2;\n  return values.map(inner);\n}\n",
451        );
452        assert_eq!(found, vec![("build".to_string(), 2)]);
453    }
454
455    #[test]
456    fn typescript_counts_an_export_default_function() {
457        let found = typescript(
458            "export default function alpha(value: number): number {\n  const total = value + 1;\n  return total;\n}\n",
459        );
460        assert_eq!(found, vec![("alpha".to_string(), 2)]);
461    }
462
463    #[test]
464    fn typescript_skips_an_overload_signature() {
465        let found = typescript(
466            "export function alpha(value: number): number;\n\
467             export function alpha(value: string): string;\n\
468             export function alpha(value: unknown): unknown {\n  const echoed = value;\n  return echoed;\n}\n",
469        );
470        assert_eq!(found, vec![("alpha".to_string(), 2)]);
471    }
472
473    #[test]
474    fn typescript_excludes_comment_lines_from_the_body() {
475        let found = typescript(
476            "export function described(value: number): number {\n  // the identity is the whole contract\n\n  return value;\n}\n",
477        );
478        assert_eq!(found, vec![("described".to_string(), 1)]);
479    }
480
481    #[test]
482    fn rust_counts_top_level_items_only() {
483        let found = rust(
484            "pub struct Widget;\n\
485             impl Widget {\n    pub fn grow(&self) -> u8 {\n        1\n    }\n}\n\
486             pub fn build(values: &[u8]) -> u8 {\n    fn inner(v: u8) -> u8 {\n        v * 2\n    }\n    inner(values[0])\n}\n",
487        );
488        assert_eq!(found, vec![("build".to_string(), 4)]);
489    }
490
491    #[test]
492    fn rust_skips_functions_in_an_inline_test_module() {
493        let found = rust(
494            "pub fn ratio(a: u8, b: u8) -> u8 {\n    (a + b) / 2\n}\n\
495             #[cfg(test)]\nmod tests {\n    #[test]\n    fn halves() {\n        let x = 1;\n        assert_eq!(x, 1);\n    }\n}\n",
496        );
497        assert_eq!(found, vec![("ratio".to_string(), 1)]);
498    }
499
500    #[test]
501    fn rust_excludes_doc_comments_and_body_comments() {
502        let found = rust(
503            "/// Return the value unchanged.\npub fn described(value: u8) -> u8 {\n    // the identity is the whole contract\n\n    value\n}\n",
504        );
505        assert_eq!(found, vec![("described".to_string(), 1)]);
506    }
507
508    #[test]
509    fn rust_counts_an_empty_body_as_no_lines() {
510        let found = rust("pub fn stub() {}\n");
511        assert_eq!(found, vec![("stub".to_string(), 0)]);
512    }
513
514    #[test]
515    fn typescript_counts_a_plain_function_declaration() {
516        let found = typescript(
517            "function alpha(value: number): number {\n  const total = value + 1;\n  return total;\n}\n",
518        );
519        assert_eq!(found, vec![("alpha".to_string(), 2)]);
520    }
521
522    #[test]
523    fn typescript_skips_non_function_defaults_and_imports() {
524        let found = typescript(
525            "import { x } from './x';\nexport default class Widget {}\n\
526             const beta = (value: number): number => value;\n",
527        );
528        assert_eq!(found, vec![("beta".to_string(), 1)]);
529    }
530
531    #[test]
532    fn typescript_skips_a_destructured_function_binding() {
533        let found = typescript("const { a } = () => {};\n");
534        assert!(found.is_empty(), "got: {found:?}");
535    }
536
537    #[test]
538    fn typescript_counts_an_empty_arrow_body_as_no_lines() {
539        let found = typescript("const stub = () => {};\n");
540        assert_eq!(found, vec![("stub".to_string(), 0)]);
541    }
542
543    #[test]
544    fn typescript_parse_error_is_reported() {
545        let err = typescript_functions("const x = ;\n", Path::new("bad.ts")).unwrap_err();
546        assert!(err.to_string().contains("parsing"), "got: {err}");
547    }
548
549    fn unique_tmp(slug: &str) -> PathBuf {
550        use std::sync::atomic::{AtomicU64, Ordering};
551        static COUNTER: AtomicU64 = AtomicU64::new(0);
552        std::env::temp_dir().join(format!(
553            "tc-one-function-{slug}-{}-{}",
554            std::process::id(),
555            COUNTER.fetch_add(1, Ordering::Relaxed)
556        ))
557    }
558
559    #[test]
560    fn suite_tier_files_are_not_judged() {
561        let root = unique_tmp("suite");
562        std::fs::create_dir_all(root.join("tests")).unwrap();
563        std::fs::write(root.join("pyproject.toml"), "[project]\nname = \"w\"\n").unwrap();
564        let two_functions = "def alpha():\n    return 1\n\ndef beta():\n    return 2\n";
565        std::fs::write(root.join("widget.py"), two_functions).unwrap();
566        std::fs::write(root.join("tests").join("helper.py"), two_functions).unwrap();
567        let (found, _) = find_violations(&root, Language::Python, 0).expect("the tree scans");
568        assert_eq!(found.len(), 1, "got: {found:?}");
569        assert!(found[0].file.ends_with("widget.py"), "got: {found:?}");
570    }
571
572    #[test]
573    fn a_root_without_a_manifest_judges_every_source_file() {
574        let root = unique_tmp("no-manifest");
575        std::fs::create_dir_all(&root).unwrap();
576        let two_functions = "def alpha():\n    return 1\n\ndef beta():\n    return 2\n";
577        std::fs::write(root.join("widget.py"), two_functions).unwrap();
578        let (found, _) = find_violations(&root, Language::Python, 0).expect("the tree scans");
579        assert_eq!(found.len(), 1, "got: {found:?}");
580    }
581
582    #[test]
583    fn a_typescript_suite_tier_is_not_judged() {
584        let root = unique_tmp("ts-suite");
585        std::fs::create_dir_all(root.join("tests")).unwrap();
586        std::fs::write(root.join("package.json"), "{ \"name\": \"w\" }\n").unwrap();
587        let two = "const alpha = () => {\n  return 1;\n};\nconst beta = () => {\n  return 2;\n};\n";
588        std::fs::write(root.join("widget.ts"), two).unwrap();
589        std::fs::write(root.join("tests").join("helper.ts"), two).unwrap();
590        let (found, _) = find_violations(&root, Language::TypeScript, 0).expect("the tree scans");
591        assert_eq!(found.len(), 1, "got: {found:?}");
592        assert!(found[0].file.ends_with("widget.ts"), "got: {found:?}");
593    }
594
595    #[test]
596    fn a_rust_root_collects_only_rust_sources() {
597        let root = unique_tmp("rust-root");
598        std::fs::create_dir_all(&root).unwrap();
599        std::fs::write(root.join("widget.rs"), "pub fn one() -> u8 {\n    1\n}\n").unwrap();
600        let (found, _) = find_violations(&root, Language::Rust, 0).expect("the tree scans");
601        assert!(found.is_empty(), "got: {found:?}");
602    }
603
604    #[test]
605    fn an_unreadable_source_names_the_file() {
606        let root = unique_tmp("unreadable");
607        std::fs::create_dir_all(&root).unwrap();
608        std::fs::write(root.join("widget.rs"), [0xFF, 0xFE]).unwrap();
609        let err = find_violations(&root, Language::Rust, 0).unwrap_err();
610        assert!(
611            err.to_string().contains("reading source file"),
612            "got: {err}"
613        );
614    }
615
616    #[test]
617    fn an_unparsable_python_source_names_the_file() {
618        let err = python_functions("def broken(:\n", Path::new("widget.py")).unwrap_err();
619        assert!(err.to_string().contains("parsing"), "got: {err}");
620    }
621
622    #[test]
623    fn an_extension_without_a_source_type_is_an_error() {
624        let err = typescript_functions("", Path::new("widget.txt")).unwrap_err();
625        assert!(
626            err.to_string().contains("reading the source type"),
627            "got: {err}"
628        );
629    }
630
631    #[test]
632    fn an_unnamed_default_export_is_reported_as_default() {
633        let found =
634            typescript("export default function (value: number): number {\n  return value;\n}\n");
635        assert_eq!(found, vec![("default".to_string(), 1)]);
636    }
637
638    #[test]
639    fn an_unparsable_rust_source_names_the_file() {
640        let err = rust_functions("fn broken( {\n", Path::new("widget.rs")).unwrap_err();
641        assert!(err.to_string().contains("parsing"), "got: {err}");
642    }
643
644    #[test]
645    fn code_lines_skips_blank_and_comment_lines() {
646        let lines = vec![
647            "let a = 1;",
648            "",
649            "// note",
650            "/* block",
651            " * inner",
652            " */",
653            "a",
654        ];
655        assert_eq!(code_lines(&lines, 1, 7, Comment::Slash), 2);
656    }
657
658    #[test]
659    fn is_comment_keeps_a_rust_dereference_as_code() {
660        assert!(!is_comment("*counter += 1;", Comment::Slash));
661        assert!(is_comment("* inner", Comment::Slash));
662        assert!(is_comment("# note", Comment::Hash));
663        assert!(!is_comment("value = 1", Comment::Hash));
664    }
665
666    #[test]
667    fn line_of_counts_newlines_before_the_offset() {
668        let source = "a\nb\nc";
669        assert_eq!(line_of(source, TextSize::from(0)), 1);
670        assert_eq!(line_of(source, TextSize::from(2)), 2);
671        assert_eq!(line_of(source, TextSize::from(4)), 3);
672    }
673}