codemap-bash 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codemap_bash-0.1.0/.gitignore +43 -0
- codemap_bash-0.1.0/PKG-INFO +69 -0
- codemap_bash-0.1.0/README.md +48 -0
- codemap_bash-0.1.0/pyproject.toml +37 -0
- codemap_bash-0.1.0/src/codemap_bash/__init__.py +8 -0
- codemap_bash-0.1.0/src/codemap_bash/indexer.py +218 -0
- codemap_bash-0.1.0/tests/__init__.py +0 -0
- codemap_bash-0.1.0/tests/test_indexer.py +168 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Build artifacts
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
*.egg
|
|
11
|
+
.eggs/
|
|
12
|
+
|
|
13
|
+
# Test / coverage
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
.coverage.*
|
|
17
|
+
htmlcov/
|
|
18
|
+
coverage.xml
|
|
19
|
+
.tox/
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
.ruff_cache/
|
|
22
|
+
.benchmarks/
|
|
23
|
+
|
|
24
|
+
# Virtualenv
|
|
25
|
+
.venv/
|
|
26
|
+
venv/
|
|
27
|
+
env/
|
|
28
|
+
|
|
29
|
+
# uv / pdm lockfiles (commit uv.lock once we settle)
|
|
30
|
+
# uv.lock
|
|
31
|
+
|
|
32
|
+
# IDE
|
|
33
|
+
.idea/
|
|
34
|
+
.vscode/
|
|
35
|
+
*.swp
|
|
36
|
+
*.swo
|
|
37
|
+
|
|
38
|
+
# OS
|
|
39
|
+
.DS_Store
|
|
40
|
+
Thumbs.db
|
|
41
|
+
|
|
42
|
+
# CodeMap own index when dogfooding
|
|
43
|
+
.codemap/
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codemap-bash
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bash / shell-script indexer plugin for CodeMap
|
|
5
|
+
Project-URL: Homepage, https://github.com/qxbyte/codemap
|
|
6
|
+
Author: CodeMap Contributors
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: bash,codemap,indexer,shell,tree-sitter
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Unix Shell
|
|
12
|
+
Classifier: Topic :: Software Development
|
|
13
|
+
Classifier: Topic :: System :: Shells
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Requires-Dist: codemap-core<0.2,>=0.1.0
|
|
16
|
+
Requires-Dist: tree-sitter-bash>=0.25
|
|
17
|
+
Requires-Dist: tree-sitter>=0.25
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# codemap-bash
|
|
23
|
+
|
|
24
|
+
> A Bash / shell-script indexer for [CodeMap](https://github.com/qxbyte/codemap),
|
|
25
|
+
> shipped as an independent PyPI package.
|
|
26
|
+
|
|
27
|
+
## What it captures
|
|
28
|
+
|
|
29
|
+
Backed by `tree-sitter-bash`:
|
|
30
|
+
|
|
31
|
+
| AST node | Symbol kind |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `function_definition` | `function` |
|
|
34
|
+
| Top-level `variable_assignment` | `variable` |
|
|
35
|
+
| `declaration_command` (`readonly`/`declare`/`export`/`local`) at top level | `variable` (with `extra.bash_kind=<keyword>`) |
|
|
36
|
+
|
|
37
|
+
Function bodies are **not** walked for inner assignments — anything
|
|
38
|
+
declared inside a function is local state, not a script-level symbol.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install codemap-bash
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## SymbolID encoding
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
scip-bash . . . scripts/deploy.sh/greet().
|
|
50
|
+
scip-bash . . . scripts/deploy.sh/MAX_RETRIES.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## File patterns
|
|
54
|
+
|
|
55
|
+
* `*.sh`, `*.bash`, `*.bats`
|
|
56
|
+
* Files without an extension whose shebang starts with `#!/bin/bash` or
|
|
57
|
+
`#!/usr/bin/env bash` are matched via `supports()`.
|
|
58
|
+
|
|
59
|
+
## Limits
|
|
60
|
+
|
|
61
|
+
* `source` / `.` includes are not turned into edges.
|
|
62
|
+
* Aliases (`alias ll='ls -la'`) are not captured.
|
|
63
|
+
* `getopts` argument schemas aren't structured.
|
|
64
|
+
* POSIX sh / Zsh / Fish dialects parse, but constructs unique to them
|
|
65
|
+
may degrade to syntax-error diagnostics.
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# codemap-bash
|
|
2
|
+
|
|
3
|
+
> A Bash / shell-script indexer for [CodeMap](https://github.com/qxbyte/codemap),
|
|
4
|
+
> shipped as an independent PyPI package.
|
|
5
|
+
|
|
6
|
+
## What it captures
|
|
7
|
+
|
|
8
|
+
Backed by `tree-sitter-bash`:
|
|
9
|
+
|
|
10
|
+
| AST node | Symbol kind |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `function_definition` | `function` |
|
|
13
|
+
| Top-level `variable_assignment` | `variable` |
|
|
14
|
+
| `declaration_command` (`readonly`/`declare`/`export`/`local`) at top level | `variable` (with `extra.bash_kind=<keyword>`) |
|
|
15
|
+
|
|
16
|
+
Function bodies are **not** walked for inner assignments — anything
|
|
17
|
+
declared inside a function is local state, not a script-level symbol.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install codemap-bash
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## SymbolID encoding
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
scip-bash . . . scripts/deploy.sh/greet().
|
|
29
|
+
scip-bash . . . scripts/deploy.sh/MAX_RETRIES.
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## File patterns
|
|
33
|
+
|
|
34
|
+
* `*.sh`, `*.bash`, `*.bats`
|
|
35
|
+
* Files without an extension whose shebang starts with `#!/bin/bash` or
|
|
36
|
+
`#!/usr/bin/env bash` are matched via `supports()`.
|
|
37
|
+
|
|
38
|
+
## Limits
|
|
39
|
+
|
|
40
|
+
* `source` / `.` includes are not turned into edges.
|
|
41
|
+
* Aliases (`alias ll='ls -la'`) are not captured.
|
|
42
|
+
* `getopts` argument schemas aren't structured.
|
|
43
|
+
* POSIX sh / Zsh / Fish dialects parse, but constructs unique to them
|
|
44
|
+
may degrade to syntax-error diagnostics.
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
MIT.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codemap-bash"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Bash / shell-script indexer plugin for CodeMap"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "CodeMap Contributors" }]
|
|
13
|
+
keywords = ["codemap", "bash", "shell", "indexer", "tree-sitter"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Unix Shell",
|
|
18
|
+
"Topic :: Software Development",
|
|
19
|
+
"Topic :: System :: Shells",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"codemap-core>=0.1.0,<0.2",
|
|
23
|
+
"tree-sitter>=0.25",
|
|
24
|
+
"tree-sitter-bash>=0.25",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = ["pytest>=8.0"]
|
|
29
|
+
|
|
30
|
+
[project.entry-points."codemap.indexers"]
|
|
31
|
+
bash = "codemap_bash:BashIndexer"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/qxbyte/codemap"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/codemap_bash"]
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Bash indexer built on tree-sitter-bash.
|
|
2
|
+
|
|
3
|
+
Only script-level symbols are emitted: function definitions, top-level
|
|
4
|
+
variable assignments, and top-level ``readonly`` / ``declare`` /
|
|
5
|
+
``export`` / ``local`` declaration commands. Variables defined inside a
|
|
6
|
+
function body are intentionally not surfaced — they are private state,
|
|
7
|
+
not a stable interface.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path, PurePosixPath
|
|
13
|
+
from typing import ClassVar
|
|
14
|
+
|
|
15
|
+
import tree_sitter
|
|
16
|
+
import tree_sitter_bash
|
|
17
|
+
|
|
18
|
+
from codemap.core.models import Diagnostic, Edge, IndexResult, Range, Symbol
|
|
19
|
+
from codemap.core.symbol import Descriptor, DescriptorKind, SymbolID
|
|
20
|
+
from codemap.indexers.base import IndexContext
|
|
21
|
+
|
|
22
|
+
SCHEME = "scip-bash"
|
|
23
|
+
LANG = "bash"
|
|
24
|
+
|
|
25
|
+
_BASH_LANG = tree_sitter.Language(tree_sitter_bash.language())
|
|
26
|
+
_DECLARATION_KEYWORDS = frozenset({"readonly", "declare", "export", "local", "typeset"})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BashIndexer:
|
|
30
|
+
name: ClassVar[str] = "bash"
|
|
31
|
+
version: ClassVar[str] = "0.1.0"
|
|
32
|
+
file_patterns: ClassVar[list[str]] = ["*.sh", "*.bash", "*.bats"]
|
|
33
|
+
languages: ClassVar[list[str]] = [LANG]
|
|
34
|
+
|
|
35
|
+
def supports(self, path: Path) -> bool:
|
|
36
|
+
if path.suffix in {".sh", ".bash", ".bats"}:
|
|
37
|
+
return True
|
|
38
|
+
# Extensionless files with a bash shebang are also accepted.
|
|
39
|
+
if path.suffix == "" and path.is_file():
|
|
40
|
+
try:
|
|
41
|
+
with path.open("rb") as f:
|
|
42
|
+
head = f.read(64)
|
|
43
|
+
return _looks_like_bash_shebang(head)
|
|
44
|
+
except OSError:
|
|
45
|
+
return False
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
def index_file(
|
|
49
|
+
self,
|
|
50
|
+
path: Path,
|
|
51
|
+
source: bytes,
|
|
52
|
+
ctx: IndexContext,
|
|
53
|
+
) -> IndexResult:
|
|
54
|
+
try:
|
|
55
|
+
source.decode("utf-8")
|
|
56
|
+
except UnicodeDecodeError as exc:
|
|
57
|
+
return IndexResult(
|
|
58
|
+
diagnostics=[
|
|
59
|
+
Diagnostic(
|
|
60
|
+
severity="error",
|
|
61
|
+
file=ctx.relative_path,
|
|
62
|
+
code="SH002",
|
|
63
|
+
message=f"not valid UTF-8: {exc}",
|
|
64
|
+
producer=self.name,
|
|
65
|
+
)
|
|
66
|
+
]
|
|
67
|
+
)
|
|
68
|
+
parser = tree_sitter.Parser(_BASH_LANG)
|
|
69
|
+
tree = parser.parse(source)
|
|
70
|
+
visitor = _Visitor(ctx.relative_path)
|
|
71
|
+
visitor.visit_root(tree.root_node)
|
|
72
|
+
diagnostics = list(visitor.diagnostics)
|
|
73
|
+
if tree.root_node.has_error:
|
|
74
|
+
diagnostics.append(
|
|
75
|
+
Diagnostic(
|
|
76
|
+
severity="warning",
|
|
77
|
+
file=ctx.relative_path,
|
|
78
|
+
range=Range(start_line=1, end_line=1),
|
|
79
|
+
code="SH001",
|
|
80
|
+
message="tree-sitter reported parse errors; symbols may be incomplete",
|
|
81
|
+
producer=self.name,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
return IndexResult(
|
|
85
|
+
symbols=visitor.symbols,
|
|
86
|
+
edges=visitor.edges,
|
|
87
|
+
diagnostics=diagnostics,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _Visitor:
|
|
92
|
+
def __init__(self, relative_path: PurePosixPath) -> None:
|
|
93
|
+
self.relative_path = relative_path
|
|
94
|
+
self.symbols: list[Symbol] = []
|
|
95
|
+
self.edges: list[Edge] = []
|
|
96
|
+
self.diagnostics: list[Diagnostic] = []
|
|
97
|
+
|
|
98
|
+
def visit_root(self, root: tree_sitter.Node) -> None:
|
|
99
|
+
# We only walk the top-level statement list — function bodies are
|
|
100
|
+
# opaque from the symbol-table perspective.
|
|
101
|
+
for child in root.children:
|
|
102
|
+
self._visit_top_level(child)
|
|
103
|
+
|
|
104
|
+
def _visit_top_level(self, node: tree_sitter.Node) -> None:
|
|
105
|
+
kind = node.type
|
|
106
|
+
if kind == "function_definition":
|
|
107
|
+
self._emit_function(node)
|
|
108
|
+
return
|
|
109
|
+
if kind == "variable_assignment":
|
|
110
|
+
self._emit_variable(node, bash_kind=None)
|
|
111
|
+
return
|
|
112
|
+
if kind == "declaration_command":
|
|
113
|
+
self._emit_declaration(node)
|
|
114
|
+
|
|
115
|
+
def _emit_function(self, node: tree_sitter.Node) -> None:
|
|
116
|
+
name = _function_name(node)
|
|
117
|
+
if name is None:
|
|
118
|
+
return
|
|
119
|
+
sid = self._make_id(name, kind=DescriptorKind.METHOD)
|
|
120
|
+
self.symbols.append(
|
|
121
|
+
Symbol(
|
|
122
|
+
id=sid,
|
|
123
|
+
kind="function",
|
|
124
|
+
language=LANG,
|
|
125
|
+
file=self.relative_path,
|
|
126
|
+
range=_node_range(node),
|
|
127
|
+
signature=f"{name}()",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _emit_variable(
|
|
132
|
+
self,
|
|
133
|
+
node: tree_sitter.Node,
|
|
134
|
+
*,
|
|
135
|
+
bash_kind: str | None,
|
|
136
|
+
) -> None:
|
|
137
|
+
name = _variable_name(node)
|
|
138
|
+
if name is None:
|
|
139
|
+
return
|
|
140
|
+
sid = self._make_id(name, kind=DescriptorKind.TERM)
|
|
141
|
+
extra: dict[str, str] = {}
|
|
142
|
+
if bash_kind is not None:
|
|
143
|
+
extra["bash_kind"] = bash_kind
|
|
144
|
+
self.symbols.append(
|
|
145
|
+
Symbol(
|
|
146
|
+
id=sid,
|
|
147
|
+
kind="variable",
|
|
148
|
+
language=LANG,
|
|
149
|
+
file=self.relative_path,
|
|
150
|
+
range=_node_range(node),
|
|
151
|
+
extra=extra,
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _emit_declaration(self, node: tree_sitter.Node) -> None:
|
|
156
|
+
keyword: str | None = None
|
|
157
|
+
for child in node.children:
|
|
158
|
+
if child.type in _DECLARATION_KEYWORDS:
|
|
159
|
+
keyword = child.type
|
|
160
|
+
break
|
|
161
|
+
for child in node.children:
|
|
162
|
+
if child.type == "variable_assignment":
|
|
163
|
+
self._emit_variable(child, bash_kind=keyword)
|
|
164
|
+
|
|
165
|
+
def _make_id(self, name: str, *, kind: DescriptorKind) -> SymbolID:
|
|
166
|
+
descriptors = list(_path_namespaces(self.relative_path))
|
|
167
|
+
descriptors.append(Descriptor(name=name, kind=kind))
|
|
168
|
+
return SymbolID(scheme=SCHEME, descriptors=tuple(descriptors))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
# Pure helpers
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _path_namespaces(path: PurePosixPath) -> list[Descriptor]:
|
|
177
|
+
return [Descriptor(name=part, kind=DescriptorKind.NAMESPACE) for part in path.parts]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _node_range(node: tree_sitter.Node) -> Range:
|
|
181
|
+
sr, sc = node.start_point
|
|
182
|
+
er, ec = node.end_point
|
|
183
|
+
return Range(
|
|
184
|
+
start_line=sr + 1,
|
|
185
|
+
start_col=sc,
|
|
186
|
+
end_line=max(er + 1, sr + 1),
|
|
187
|
+
end_col=ec,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _node_text(node: tree_sitter.Node) -> str:
|
|
192
|
+
return node.text.decode("utf-8") if node.text is not None else ""
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _function_name(node: tree_sitter.Node) -> str | None:
|
|
196
|
+
"""``function greet() { ... }`` and ``helper() { ... }`` both expose the
|
|
197
|
+
function name as a ``word`` child (sitting either after the optional
|
|
198
|
+
``function`` keyword or as the first content child).
|
|
199
|
+
"""
|
|
200
|
+
for child in node.children:
|
|
201
|
+
if child.type == "word":
|
|
202
|
+
return _node_text(child)
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _variable_name(node: tree_sitter.Node) -> str | None:
|
|
207
|
+
"""For ``variable_assignment``: the ``variable_name`` child."""
|
|
208
|
+
for child in node.children:
|
|
209
|
+
if child.type == "variable_name":
|
|
210
|
+
return _node_text(child)
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
_BASH_SHEBANGS = (b"#!/bin/bash", b"#!/usr/bin/env bash", b"#!/bin/sh", b"#!/usr/bin/env sh")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _looks_like_bash_shebang(head: bytes) -> bool:
|
|
218
|
+
return any(head.startswith(sb) for sb in _BASH_SHEBANGS)
|
|
File without changes
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Unit tests for the Bash indexer plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import textwrap
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
|
|
8
|
+
from codemap_bash import BashIndexer
|
|
9
|
+
from codemap_bash.indexer import SCHEME
|
|
10
|
+
|
|
11
|
+
from codemap.core.models import IndexResult
|
|
12
|
+
from codemap.indexers.base import IndexContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _index(source: str, *, path: str = "scripts/deploy.sh") -> IndexResult:
|
|
16
|
+
code = textwrap.dedent(source).lstrip("\n")
|
|
17
|
+
return BashIndexer().index_file(
|
|
18
|
+
Path(path),
|
|
19
|
+
code.encode("utf-8"),
|
|
20
|
+
IndexContext(
|
|
21
|
+
project_root=Path("/tmp/proj"),
|
|
22
|
+
relative_path=PurePosixPath(path),
|
|
23
|
+
language="bash",
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_indexer_metadata() -> None:
|
|
29
|
+
ix = BashIndexer()
|
|
30
|
+
assert ix.name == "bash"
|
|
31
|
+
assert ix.languages == ["bash"]
|
|
32
|
+
assert ix.supports(Path("a.sh"))
|
|
33
|
+
assert ix.supports(Path("a.bash"))
|
|
34
|
+
assert ix.supports(Path("a.bats"))
|
|
35
|
+
assert not ix.supports(Path("a.py"))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_scheme_is_consistent() -> None:
|
|
39
|
+
r = _index(
|
|
40
|
+
"""
|
|
41
|
+
FOO=1
|
|
42
|
+
function f() { echo hi; }
|
|
43
|
+
"""
|
|
44
|
+
)
|
|
45
|
+
for s in r.symbols:
|
|
46
|
+
assert str(s.id).startswith(f"{SCHEME} ")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_function_definition_with_function_keyword() -> None:
|
|
50
|
+
r = _index(
|
|
51
|
+
"""
|
|
52
|
+
function greet() {
|
|
53
|
+
echo "hi $1"
|
|
54
|
+
}
|
|
55
|
+
"""
|
|
56
|
+
)
|
|
57
|
+
funcs = [s for s in r.symbols if s.kind == "function"]
|
|
58
|
+
assert len(funcs) == 1
|
|
59
|
+
assert "greet()." in str(funcs[0].id)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_function_definition_without_keyword() -> None:
|
|
63
|
+
r = _index(
|
|
64
|
+
"""
|
|
65
|
+
helper() {
|
|
66
|
+
local x=$1
|
|
67
|
+
return 0
|
|
68
|
+
}
|
|
69
|
+
"""
|
|
70
|
+
)
|
|
71
|
+
funcs = [s for s in r.symbols if s.kind == "function"]
|
|
72
|
+
assert len(funcs) == 1
|
|
73
|
+
assert "helper()." in str(funcs[0].id)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_top_level_variable_assignment() -> None:
|
|
77
|
+
r = _index("COUNT=10")
|
|
78
|
+
vars_ = [s for s in r.symbols if s.kind == "variable"]
|
|
79
|
+
assert len(vars_) == 1
|
|
80
|
+
assert "COUNT." in str(vars_[0].id)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_readonly_declaration_tagged() -> None:
|
|
84
|
+
r = _index("readonly MAX_RETRIES=3")
|
|
85
|
+
vars_ = [s for s in r.symbols if s.kind == "variable"]
|
|
86
|
+
assert len(vars_) == 1
|
|
87
|
+
assert vars_[0].extra.get("bash_kind") == "readonly"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_export_declaration_tagged() -> None:
|
|
91
|
+
r = _index('export PATH_VAR="/usr/local/bin"')
|
|
92
|
+
vars_ = [s for s in r.symbols if s.kind == "variable"]
|
|
93
|
+
assert vars_[0].extra.get("bash_kind") == "export"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_declare_declaration_tagged() -> None:
|
|
97
|
+
r = _index("declare -i COUNTER=0")
|
|
98
|
+
vars_ = [s for s in r.symbols if s.kind == "variable"]
|
|
99
|
+
assert vars_[0].extra.get("bash_kind") == "declare"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_function_internals_not_indexed() -> None:
|
|
103
|
+
"""Variables declared inside a function body are local state, not
|
|
104
|
+
script-level symbols."""
|
|
105
|
+
r = _index(
|
|
106
|
+
"""
|
|
107
|
+
helper() {
|
|
108
|
+
INNER_VAR=42
|
|
109
|
+
local INNER_LOCAL=7
|
|
110
|
+
}
|
|
111
|
+
"""
|
|
112
|
+
)
|
|
113
|
+
vars_ = [s for s in r.symbols if s.kind == "variable"]
|
|
114
|
+
assert vars_ == []
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_multiple_top_level_symbols() -> None:
|
|
118
|
+
r = _index(
|
|
119
|
+
"""
|
|
120
|
+
FOO=1
|
|
121
|
+
readonly BAR=2
|
|
122
|
+
function f() { echo hi; }
|
|
123
|
+
g() { :; }
|
|
124
|
+
"""
|
|
125
|
+
)
|
|
126
|
+
funcs = [s for s in r.symbols if s.kind == "function"]
|
|
127
|
+
vars_ = [s for s in r.symbols if s.kind == "variable"]
|
|
128
|
+
assert {s.id.descriptors[-1].name for s in funcs} == {"f", "g"}
|
|
129
|
+
assert {s.id.descriptors[-1].name for s in vars_} == {"FOO", "BAR"}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_symbol_id_uses_path_namespaces() -> None:
|
|
133
|
+
r = _index("FOO=1", path="scripts/utils/setup.sh")
|
|
134
|
+
var = next(s for s in r.symbols if s.kind == "variable")
|
|
135
|
+
assert str(var.id) == "scip-bash . . . scripts/utils/setup.sh/FOO."
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_empty_file_yields_no_symbols() -> None:
|
|
139
|
+
r = _index("")
|
|
140
|
+
assert r.symbols == []
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_invalid_utf8_yields_diagnostic() -> None:
|
|
144
|
+
ix = BashIndexer()
|
|
145
|
+
r = ix.index_file(
|
|
146
|
+
Path("bad.sh"),
|
|
147
|
+
b"\xff\xfe echo",
|
|
148
|
+
IndexContext(
|
|
149
|
+
project_root=Path("/tmp/proj"),
|
|
150
|
+
relative_path=PurePosixPath("bad.sh"),
|
|
151
|
+
language="bash",
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
assert r.symbols == []
|
|
155
|
+
assert r.diagnostics[0].code == "SH002"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_command_invocations_ignored() -> None:
|
|
159
|
+
"""Commands like `cp foo bar` are not symbols."""
|
|
160
|
+
r = _index(
|
|
161
|
+
"""
|
|
162
|
+
FOO=1
|
|
163
|
+
cp source dest
|
|
164
|
+
echo done
|
|
165
|
+
"""
|
|
166
|
+
)
|
|
167
|
+
# Only FOO should appear; commands aren't symbols.
|
|
168
|
+
assert {s.id.descriptors[-1].name for s in r.symbols if s.kind == "variable"} == {"FOO"}
|