Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions packages/rbtr/src/rbtr/languages/bash.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,29 @@ class system, or module structure.

deploy() { echo deploying; } → function "deploy", scope ""
function setup { ... } → function "setup", scope ""
alias ll="ls -l" → variable "ll", scope ""

No imports, classes, or methods are extracted.
No classes or methods are extracted.

An `alias` name parses as one `word` fused with its `=` (`ll=`), which no
query can split. The `name_extractor` strips the trailing `=`; no other
bash name ends in one.
"""

from __future__ import annotations

from rbtr.languages.hookspec import LanguageRegistration, hookimpl
from typing import TYPE_CHECKING

from rbtr.languages.hookspec import LanguageRegistration, hookimpl, resolve_name

if TYPE_CHECKING:
from tree_sitter import Node


def _strip_alias_eq(capture_name: str, node: Node, captures: dict[str, list[Node]]) -> str:
"""Default name, with the `=` the grammar fuses onto an alias removed."""
return resolve_name(capture_name, node, captures).rstrip("=")


# ── Query ────────────────────────────────────────────────────────────

Expand All @@ -38,6 +54,17 @@ class system, or module structure.
(program
(variable_assignment
name: (variable_name) @_var_name) @variable)

(program
(declaration_command
(variable_assignment
name: (variable_name) @_var_name)) @variable)

(command
name: (command_name (word) @_cmd)
argument: (concatenation (word) @_var_name)
(#eq? @_cmd "alias")
(#match? @_var_name "=")) @variable
"""

# ── Plugin ───────────────────────────────────────────────────────────
Expand Down Expand Up @@ -71,6 +98,7 @@ def rbtr_register_languages(self) -> list[LanguageRegistration]:
query=_QUERY,
# Bash: `#` comments above a function attach.
doc_comment_node_types=frozenset({"comment"}),
language_plugin_version=2,
name_extractor=_strip_alias_eq,
language_plugin_version=3,
),
]
124 changes: 105 additions & 19 deletions packages/rbtr/src/rbtr/languages/javascript.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"""JavaScript and TypeScript language plugins.

Registers two languages (one per tree-sitter grammar) sharing a
common import extractor. The only query difference is the class
name node type: JS uses `identifier`, TS uses `type_identifier`.
Registers three languages (one per tree-sitter grammar) sharing a
common import extractor and a shared core query (classes, functions,
generators, variables, imports, and methods — class/object members
including get/set accessors). TS additionally captures interfaces,
enums, type aliases, abstract classes, and interface/abstract method
signatures.

Extracted chunks::

Expand Down Expand Up @@ -37,17 +40,26 @@ class Service {} → class "Service", scope ""
# ── Queries ──────────────────────────────────────────────────────────

# Shared patterns — identical across JS and TS grammars.
# `method_definition` covers class and object-literal members, generators,
# and get/set accessors; inside a class it promotes to a method scoped to
# the class, elsewhere it is a scope-less method.
_SHARED = """\
(function_declaration
name: (identifier) @_fn_name) @function

(generator_function_declaration
name: (identifier) @_fn_name) @function

(import_statement
source: (string (string_fragment) @_import_module)) @import

(lexical_declaration
(variable_declarator
name: (identifier) @_fn_name
value: (arrow_function))) @function

(method_definition
name: (property_identifier) @_method_name) @method
"""

# Module-level const/let bindings with a non-function value become
Expand Down Expand Up @@ -114,15 +126,49 @@ class Service {} → class "Service", scope ""
+ _VARIABLES
)

_TS_QUERY = (
"""\
# TypeScript adds type-level declarations (interface, enum, type alias,
# abstract class) as classes, and class/interface members as methods.
# `method_definition` covers regular methods, constructors, and get/set
# accessors; `method_signature` covers interface methods;
# `abstract_method_signature` covers abstract declarations.
_TS_ONLY = """\
(class_declaration
name: (type_identifier) @_cls_name) @class

(abstract_class_declaration
name: (type_identifier) @_cls_name) @class

(interface_declaration
name: (type_identifier) @_cls_name) @class

(enum_declaration
name: (identifier) @_cls_name) @class

(enum_body
(property_identifier) @_var_name @variable)

(enum_body
(enum_assignment
name: (property_identifier) @_var_name) @variable)

(type_alias_declaration
name: (type_identifier) @_cls_name) @class

(internal_module
name: (identifier) @_cls_name) @class

(module
name: (identifier) @_cls_name) @class

(method_signature
name: (property_identifier) @_method_name) @method

(abstract_method_signature
name: (property_identifier) @_method_name) @method

"""
+ _SHARED
+ _VARIABLES
)

_TS_QUERY = _TS_ONLY + _SHARED + _VARIABLES

# ── Import extractor (shared by JS and TS) ───────────────────────────

Expand Down Expand Up @@ -196,17 +242,19 @@ def extract_import_meta(node: Node, captures: dict[str, list[Node]]) -> ImportMe
class JavaScriptPlugin:
"""JavaScript and TypeScript language support.

Registers two languages with separate grammars but a shared
import extractor. TypeScript requires `grammar_entry=
"language_typescript"` because the `tree_sitter_typescript`
package exposes `language_typescript()` instead of the
standard `language()`.
Registers three languages with separate grammars but a shared
import extractor. The `tree_sitter_typescript` package ships two
grammars: `language_typescript()` for `.ts` and `language_tsx()`
for `.tsx`. They are registered separately because the TypeScript
grammar cannot parse JSX (a `.tsx` file parses with errors under
`language_typescript`, cleanly under `language_tsx`).

Example registration output::

[
LanguageRegistration(id="javascript", ...),
LanguageRegistration(id="typescript", grammar_entry="language_typescript", ...),
LanguageRegistration(id="tsx", grammar_entry="language_tsx", ...),
]
"""

Expand All @@ -228,24 +276,62 @@ def rbtr_register_languages(self) -> list[LanguageRegistration]:
import_targets=frozenset({"javascript", "css"}),
source_roots=("", "src"),
test_suffix=".test",
language_plugin_version=3,
language_plugin_version=4,
),
LanguageRegistration(
id="typescript",
extensions=frozenset({".ts", ".tsx"}),
extensions=frozenset({".ts"}),
grammar_module="tree_sitter_typescript",
grammar_entry="language_typescript",
query=_TS_QUERY,
import_extractor=extract_import_meta,
scope_types=frozenset(
{"class_declaration", "function_declaration", "internal_module"}
{
"class_declaration",
"abstract_class_declaration",
"interface_declaration",
"function_declaration",
"internal_module",
"module",
"enum_declaration",
}
),
class_scope_types=frozenset(
{"class_declaration", "abstract_class_declaration", "interface_declaration"}
),
class_scope_types=frozenset({"class_declaration"}),
doc_comment_node_types=frozenset({"comment"}),
index_files=frozenset({"index.ts", "index.js"}),
import_targets=frozenset({"typescript", "javascript", "css"}),
import_targets=frozenset({"typescript", "tsx", "javascript", "css"}),
source_roots=("", "src"),
test_suffix=".test",
language_plugin_version=4,
),
LanguageRegistration(
id="tsx",
extensions=frozenset({".tsx"}),
grammar_module="tree_sitter_typescript",
grammar_entry="language_tsx",
query=_TS_QUERY,
import_extractor=extract_import_meta,
scope_types=frozenset(
{
"class_declaration",
"abstract_class_declaration",
"interface_declaration",
"function_declaration",
"internal_module",
"module",
"enum_declaration",
}
),
class_scope_types=frozenset(
{"class_declaration", "abstract_class_declaration", "interface_declaration"}
),
doc_comment_node_types=frozenset({"comment"}),
index_files=frozenset({"index.tsx", "index.ts", "index.js"}),
import_targets=frozenset({"tsx", "typescript", "javascript", "css"}),
source_roots=("", "src"),
test_suffix=".test",
language_plugin_version=3,
language_plugin_version=1,
),
]
60 changes: 46 additions & 14 deletions packages/rbtr/src/rbtr/languages/ruby.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
"""Ruby language plugin.

Provides symbol extraction (methods, classes, modules) and
structured import metadata from `require` / `require_relative`.
Provides symbol extraction (methods, classes, modules, constants,
and the RSpec `describe`/`context`/`it` DSL) and structured import
metadata from `require` / `require_relative`. Constants scope to
their enclosing class/module. RSpec groups (`describe`/`context`/
`feature`) are classes and examples (`it`/`specify`/`example`) are
functions, named by their description string.

Extracted chunks::

Expand All @@ -23,7 +27,12 @@ def bar ... end → method "bar", scope "Foo"
from typing import TYPE_CHECKING

from rbtr.index.models import ImportMeta
from rbtr.languages.hookspec import LanguageRegistration, build_import_from_captures, hookimpl
from rbtr.languages.hookspec import (
LanguageRegistration,
build_import_from_captures,
hookimpl,
parse_path_relative,
)

if TYPE_CHECKING:
from tree_sitter import Node
Expand Down Expand Up @@ -55,17 +64,29 @@ def bar ... end → method "bar", scope "Foo"
(string) @_import_module)
(#eq? @_call_name "require_relative")) @import

(program
(assignment
left: (constant) @_var_name) @variable)
(assignment
left: (constant) @_var_name) @variable

(program
(assignment
left: (left_assignment_list (constant) @_var_name)) @variable)
(assignment
left: (left_assignment_list (constant) @_var_name)) @variable

(program
(assignment
left: (left_assignment_list (rest_assignment (constant) @_var_name))) @variable)
(assignment
left: (left_assignment_list (rest_assignment (constant) @_var_name))) @variable

(call
method: (identifier) @_call_name
arguments: (argument_list . (string (string_content) @_cls_name))
(#any-of? @_call_name "describe" "context" "feature" "shared_examples" "shared_context")) @class

(call
method: (identifier) @_call_name
arguments: (argument_list . (constant) @_cls_name)
(#any-of? @_call_name "describe" "context" "feature")) @class

(call
method: (identifier) @_call_name
arguments: (argument_list . (string (string_content) @_fn_name))
(#any-of? @_call_name "it" "specify" "example" "scenario")) @function
"""

# ── Import extractor ─────────────────────────────────────────────────
Expand All @@ -85,11 +106,22 @@ def extract_import_meta(node: Node, captures: dict[str, list[Node]]) -> ImportMe

`require_relative "helpers"`:
module="helpers", dots="1"

`require_relative "./config"`:
module="config", dots="1"

`require_relative "../lib/utils"`:
module="lib/utils", dots="2"
"""
meta = build_import_from_captures(node, captures)
method = node.child_by_field_name("method")
if method and method.text == b"require_relative":
meta.dots = "1"
# `require_relative` is always relative to the current file. Strip any
# `./`/`../` prefix into `dots` (a bare path is the current dir, dots=1)
# so the resolver doesn't see a leftover `./` it can't match.
dots, cleaned = parse_path_relative(meta.module)
meta.module = cleaned
meta.dots = str(dots or 1)
return meta


Expand All @@ -115,6 +147,6 @@ def rbtr_register_languages(self) -> list[LanguageRegistration]:
doc_comment_node_types=frozenset({"comment"}),
source_roots=("", "lib"),
test_prefix="test_",
language_plugin_version=3,
language_plugin_version=4,
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
"bash.sh::source ./lib/colours.sh -> lib/colours.sh::RED [imports]",
"bash.sh::source ./lib/colours.sh -> lib/colours.sh::RESET [imports]"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
"html.html::<link rel=\"stylesheet\" href=\"styles.css\"> -> styles.css::.greeter [imports]",
"html.html::<script src=\"greeter.js\"></script> -> greeter.js::greet [imports]"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
"javascript.js::import \"./styles.css\"; -> styles.css::.greeter [imports]",
"javascript.js::import { LOCALE } from \"./config.js\"; -> config.js::LOCALE [imports]"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
"python.py::from .config import LOCALE -> config.py::LOCALE [imports]"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
"rst.rst:::class:`Greeter` -> helpers.py::Greeter [documents]",
"rst.rst:::doc:`config` -> config.rst::Config [documents]",
"rst.rst:::func:`format_greeting` -> helpers.py::format_greeting [documents]",
"rst.rst::`the guide <guide.rst>`_ -> guide.rst::Guide [documents]"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
"ruby.rb::require_relative \"./config\" -> config.rb::DEFAULT_LOCALE [imports]"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
"tsx.tsx::import { INITIAL_LABEL } from \"./labels\"; -> labels.ts::INITIAL_LABEL [imports]"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
"typescript.ts::import type { Formatter } from \"./types\"; -> types.ts::Formatter [imports]",
"typescript.ts::import { LOCALE } from \"./config\"; -> config.ts::LOCALE [imports]"
]
Loading