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