From bd91c20ebf833dbbaa6bdeb92739b6dfd6e87829 Mon Sep 17 00:00:00 2001 From: "Robert C. Martin" Date: Wed, 30 Sep 2026 08:38:14 -0500 Subject: [PATCH] Route function extraction through a Language type. Each language implements functions. An unknown language returns nothing. --- src/crapper/languages/__init__.py | 33 +++++++++++++++++------------ src/crapper/languages/clojure.py | 6 ++++++ src/crapper/languages/golang.py | 6 ++++++ src/crapper/languages/java.py | 6 ++++++ src/crapper/languages/language.py | 16 ++++++++++++++ src/crapper/languages/python.py | 6 ++++++ src/crapper/languages/rust.py | 6 ++++++ src/crapper/languages/typescript.py | 6 ++++++ tests/test_languages.py | 23 ++++++++++++++++++++ 9 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 src/crapper/languages/language.py create mode 100644 tests/test_languages.py diff --git a/src/crapper/languages/__init__.py b/src/crapper/languages/__init__.py index fd5bc69..f9725ed 100644 --- a/src/crapper/languages/__init__.py +++ b/src/crapper/languages/__init__.py @@ -1,22 +1,27 @@ """Per-language function extraction.""" -from crapper.languages import clojure, golang, java, python, rust, typescript +from crapper.languages.clojure import Clojure +from crapper.languages.golang import Go +from crapper.languages.java import Java +from crapper.languages.language import Unknown +from crapper.languages.python import Python +from crapper.languages.rust import Rust +from crapper.languages.typescript import TypeScript from crapper.model import Function +_LANGUAGES = { + "clojure": Clojure(), + "java": Java(), + "go": Go(), + "typescript": TypeScript(), + "python": Python(), + "rust": Rust(), +} +_UNKNOWN = Unknown() + def functions_in_file( language: str, source: str, path: str, project_root: str ) -> list[Function]: - if language == "clojure": - return clojure.functions_in_source(source, path, project_root) - if language == "java": - return java.functions_in_source(source, path) - if language == "go": - return golang.functions_in_source(source, path, project_root) - if language == "typescript": - return typescript.functions_in_source(source, path, project_root) - if language == "python": - return python.functions_in_source(source, path, project_root) - if language == "rust": - return rust.functions_in_source(source, path, project_root) - return [] + chosen = _LANGUAGES.get(language, _UNKNOWN) + return chosen.functions(source, path, project_root) diff --git a/src/crapper/languages/clojure.py b/src/crapper/languages/clojure.py index 9efe05a..1ee6f33 100644 --- a/src/crapper/languages/clojure.py +++ b/src/crapper/languages/clojure.py @@ -7,6 +7,7 @@ variants, `and`, `or`, `loop`, `catch`, and each clause of `cond`, `condp`, import re +from crapper.languages.language import Language from crapper.model import Function _DECISION = re.compile( @@ -285,3 +286,8 @@ def functions_in_source( ) for item in extract_functions(source) ] + + +class Clojure(Language): + def functions(self, source: str, path: str, project_root: str) -> list[Function]: + return functions_in_source(source, path, project_root) diff --git a/src/crapper/languages/golang.py b/src/crapper/languages/golang.py index dd2517a..ae43d3f 100644 --- a/src/crapper/languages/golang.py +++ b/src/crapper/languages/golang.py @@ -19,6 +19,7 @@ from crapper.languages.treesitter import ( parse, start_line, ) +from crapper.languages.language import Language from crapper.model import Function _DECISIONS = { @@ -149,3 +150,8 @@ def functions_in_source( ) ) return found + + +class Go(Language): + def functions(self, source: str, path: str, project_root: str) -> list[Function]: + return functions_in_source(source, path, project_root) diff --git a/src/crapper/languages/java.py b/src/crapper/languages/java.py index 908a608..0b8e7e7 100644 --- a/src/crapper/languages/java.py +++ b/src/crapper/languages/java.py @@ -17,6 +17,7 @@ from crapper.languages.treesitter import ( parse, start_line, ) +from crapper.languages.language import Language from crapper.model import Function _PACKAGE = re.compile(r"(?m)^\s*package\s+([a-zA-Z_][\w.]*)\s*;") @@ -114,3 +115,8 @@ def functions_in_source(source: str, path: str) -> list[Function]: visit(tree.root_node) return found + + +class Java(Language): + def functions(self, source: str, path: str, project_root: str) -> list[Function]: + return functions_in_source(source, path) diff --git a/src/crapper/languages/language.py b/src/crapper/languages/language.py new file mode 100644 index 0000000..f57b49c --- /dev/null +++ b/src/crapper/languages/language.py @@ -0,0 +1,16 @@ +"""The operation every language implements.""" + +from abc import ABC, abstractmethod + +from crapper.model import Function + + +class Language(ABC): + @abstractmethod + def functions(self, source: str, path: str, project_root: str) -> list[Function]: + """Functions and methods defined in one source file.""" + + +class Unknown(Language): + def functions(self, source: str, path: str, project_root: str) -> list[Function]: + return [] diff --git a/src/crapper/languages/python.py b/src/crapper/languages/python.py index cb9dbcc..793fd31 100644 --- a/src/crapper/languages/python.py +++ b/src/crapper/languages/python.py @@ -16,6 +16,7 @@ from crapper.languages.treesitter import ( parse, start_line, ) +from crapper.languages.language import Language from crapper.model import Function _DECISIONS = { @@ -131,3 +132,8 @@ def functions_in_source( ) ) return found + + +class Python(Language): + def functions(self, source: str, path: str, project_root: str) -> list[Function]: + return functions_in_source(source, path, project_root) diff --git a/src/crapper/languages/rust.py b/src/crapper/languages/rust.py index 4ba18b0..f368b76 100644 --- a/src/crapper/languages/rust.py +++ b/src/crapper/languages/rust.py @@ -20,6 +20,7 @@ from crapper.languages.treesitter import ( parse, start_line, ) +from crapper.languages.language import Language from crapper.model import Function _DECISIONS = { @@ -186,3 +187,8 @@ def functions_in_source( if recorded is not None: found.append(recorded) return found + + +class Rust(Language): + def functions(self, source: str, path: str, project_root: str) -> list[Function]: + return functions_in_source(source, path, project_root) diff --git a/src/crapper/languages/typescript.py b/src/crapper/languages/typescript.py index 9cb1b58..4daf0f0 100644 --- a/src/crapper/languages/typescript.py +++ b/src/crapper/languages/typescript.py @@ -17,6 +17,7 @@ from crapper.languages.treesitter import ( parse, start_line, ) +from crapper.languages.language import Language from crapper.model import Function _DECISIONS = { @@ -178,3 +179,8 @@ def functions_in_source( elif node.type == "lexical_declaration": _append_arrows(found, data, node, module, path) return found + + +class TypeScript(Language): + def functions(self, source: str, path: str, project_root: str) -> list[Function]: + return functions_in_source(source, path, project_root) diff --git a/tests/test_languages.py b/tests/test_languages.py new file mode 100644 index 0000000..05f73b1 --- /dev/null +++ b/tests/test_languages.py @@ -0,0 +1,23 @@ +import pytest + +from crapper.languages import functions_in_file + +_SOURCES = [ + ("clojure", "(defn choose [x] x)\n", "src/demo/core.clj", "choose"), + ("java", "class Board { void place() { if (true) return; } }\n", "Board.java", "place"), + ("go", "package demo\nfunc Place() int { return 1 }\n", "board.go", "Place"), + ("typescript", "export function view() { return 1 }\n", "src/ui.ts", "view"), + ("python", "def run():\n return 1\n", "src/app.py", "run"), + ("rust", "pub fn open() {}\n", "src/lib.rs", "open"), +] + + +@pytest.mark.parametrize(("language", "source", "path", "name"), _SOURCES) +def test_each_language_answers_the_same_message(language, source, path, name): + found = functions_in_file(language, source, path, "/proj") + assert found[0].name == name + assert found[0].language == language + + +def test_unknown_language_has_no_functions(): + assert functions_in_file("ruby", "def x\nend\n", "a.rb", "/proj") == []