radixcodemap 0.2.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.
- radixcodemap-0.2.0/LICENSE +21 -0
- radixcodemap-0.2.0/PKG-INFO +92 -0
- radixcodemap-0.2.0/README.md +63 -0
- radixcodemap-0.2.0/radix/__init__.py +0 -0
- radixcodemap-0.2.0/radix/__main__.py +46 -0
- radixcodemap-0.2.0/radix/cli.py +32 -0
- radixcodemap-0.2.0/radix/core.py +31 -0
- radixcodemap-0.2.0/radix/handlers/__init__.py +0 -0
- radixcodemap-0.2.0/radix/handlers/base.py +80 -0
- radixcodemap-0.2.0/radix/handlers/handler_go.py +126 -0
- radixcodemap-0.2.0/radix/handlers/handler_js.py +157 -0
- radixcodemap-0.2.0/radix/handlers/handler_py.py +89 -0
- radixcodemap-0.2.0/radix/handlers/registry.py +68 -0
- radixcodemap-0.2.0/radix/handlers/tree_utils.py +48 -0
- radixcodemap-0.2.0/radix/report.py +72 -0
- radixcodemap-0.2.0/radix/scanner.py +119 -0
- radixcodemap-0.2.0/radixcodemap.egg-info/PKG-INFO +92 -0
- radixcodemap-0.2.0/radixcodemap.egg-info/SOURCES.txt +23 -0
- radixcodemap-0.2.0/radixcodemap.egg-info/dependency_links.txt +1 -0
- radixcodemap-0.2.0/radixcodemap.egg-info/entry_points.txt +2 -0
- radixcodemap-0.2.0/radixcodemap.egg-info/requires.txt +5 -0
- radixcodemap-0.2.0/radixcodemap.egg-info/top_level.txt +1 -0
- radixcodemap-0.2.0/setup.cfg +4 -0
- radixcodemap-0.2.0/setup.py +30 -0
- radixcodemap-0.2.0/tests/test_integration.py +49 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jdotpy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: radixcodemap
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Summarize code repositories quickly and (soon) with multiple verbosity levels
|
|
5
|
+
Home-page: https://github.com/jdotpy/radix-map
|
|
6
|
+
Download-URL: https://github.com/jdotpy/radix-map/tarball/master
|
|
7
|
+
Author: KJ
|
|
8
|
+
Author-email: jdotpy@users.noreply.github.com
|
|
9
|
+
Keywords: tools
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: tree-sitter
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest; extra == "dev"
|
|
16
|
+
Requires-Dist: twine; extra == "dev"
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: author-email
|
|
19
|
+
Dynamic: classifier
|
|
20
|
+
Dynamic: description
|
|
21
|
+
Dynamic: description-content-type
|
|
22
|
+
Dynamic: download-url
|
|
23
|
+
Dynamic: home-page
|
|
24
|
+
Dynamic: keywords
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
Dynamic: provides-extra
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: summary
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Radix: Configurable SourceCode summarizer
|
|
33
|
+
|
|
34
|
+
Quickly summarize project structure with multiple verbosity levels (ok, so maybe there's only one option right now lol). More soon.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
1. **Clone the repository:**
|
|
41
|
+
```bash
|
|
42
|
+
git clone [https://github.com/jdotpy/radix-map.git](https://github.com/jdotpy/radix-map.git)
|
|
43
|
+
cd radix-map
|
|
44
|
+
pip install .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
2. **Install Dependencies based on code you plan on using:**
|
|
49
|
+
```bash
|
|
50
|
+
pip install tree-sitter-python
|
|
51
|
+
pip install tree-sitter-go
|
|
52
|
+
pip install tree-sitter-javascript
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## Use
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
radix map .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Example output:
|
|
64
|
+
```bash
|
|
65
|
+
#tests/test_integration.py
|
|
66
|
+
└── ƒ get_test_pairs()
|
|
67
|
+
|
|
68
|
+
#tests/snapshots/python_ex1.py
|
|
69
|
+
├── ƒ global_helper()
|
|
70
|
+
└── ○ class DataProcessor
|
|
71
|
+
├── ƒ __init__(self, source: str)
|
|
72
|
+
├── ƒ process(self)
|
|
73
|
+
└── ƒ _validate(self)
|
|
74
|
+
|
|
75
|
+
#radix/scanner.py
|
|
76
|
+
└── ○ class ProjectScanner
|
|
77
|
+
├── ƒ __init__(self, registry, max_bytes: int = 200_000, extra_ignored_dirs: Optional[Set[str]] = None)
|
|
78
|
+
├── ƒ is_visible(self, path: Path)
|
|
79
|
+
└── ƒ scan(self, target: str)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Supported Languages
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
| Language | Status | Package Requirement |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| Python | ✅ functions & classes | tree-sitter-python |
|
|
88
|
+
| Go | 🚧 | tree-sitter-go |
|
|
89
|
+
| JavaScript | 🚧 | tree-sitter-javascript |
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# Radix: Configurable SourceCode summarizer
|
|
4
|
+
|
|
5
|
+
Quickly summarize project structure with multiple verbosity levels (ok, so maybe there's only one option right now lol). More soon.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
1. **Clone the repository:**
|
|
12
|
+
```bash
|
|
13
|
+
git clone [https://github.com/jdotpy/radix-map.git](https://github.com/jdotpy/radix-map.git)
|
|
14
|
+
cd radix-map
|
|
15
|
+
pip install .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
2. **Install Dependencies based on code you plan on using:**
|
|
20
|
+
```bash
|
|
21
|
+
pip install tree-sitter-python
|
|
22
|
+
pip install tree-sitter-go
|
|
23
|
+
pip install tree-sitter-javascript
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## Use
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
radix map .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Example output:
|
|
35
|
+
```bash
|
|
36
|
+
#tests/test_integration.py
|
|
37
|
+
└── ƒ get_test_pairs()
|
|
38
|
+
|
|
39
|
+
#tests/snapshots/python_ex1.py
|
|
40
|
+
├── ƒ global_helper()
|
|
41
|
+
└── ○ class DataProcessor
|
|
42
|
+
├── ƒ __init__(self, source: str)
|
|
43
|
+
├── ƒ process(self)
|
|
44
|
+
└── ƒ _validate(self)
|
|
45
|
+
|
|
46
|
+
#radix/scanner.py
|
|
47
|
+
└── ○ class ProjectScanner
|
|
48
|
+
├── ƒ __init__(self, registry, max_bytes: int = 200_000, extra_ignored_dirs: Optional[Set[str]] = None)
|
|
49
|
+
├── ƒ is_visible(self, path: Path)
|
|
50
|
+
└── ƒ scan(self, target: str)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Supported Languages
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
| Language | Status | Package Requirement |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| Python | ✅ functions & classes | tree-sitter-python |
|
|
59
|
+
| Go | 🚧 | tree-sitter-go |
|
|
60
|
+
| JavaScript | 🚧 | tree-sitter-javascript |
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from . import core
|
|
2
|
+
from . import cli
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
def create_parser():
|
|
7
|
+
parser = argparse.ArgumentParser(prog="radix", description="Codebase Mapping Tool")
|
|
8
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# 'registry' subcommand
|
|
12
|
+
registry_parser = subparsers.add_parser("registry", help="Show registered file handlers and their libraries")
|
|
13
|
+
|
|
14
|
+
# 'map' subcommand
|
|
15
|
+
map_parser = subparsers.add_parser("map", help="Generate a structural map of the codebase")
|
|
16
|
+
|
|
17
|
+
# Positional argument
|
|
18
|
+
map_parser.add_argument("path", help="Path to file or directory to scan")
|
|
19
|
+
|
|
20
|
+
# Fine-grained Overrides (Your original flags)
|
|
21
|
+
group = map_parser.add_argument_group("Detail Overrides")
|
|
22
|
+
group.add_argument("--calls", action="store_true", help="Include internal function calls")
|
|
23
|
+
group.add_argument("--lines", action="store_true", help="Include type definitions/properties")
|
|
24
|
+
|
|
25
|
+
# Configuration
|
|
26
|
+
map_parser.add_argument("--max-size", type=int, default=200000,
|
|
27
|
+
help="Skip files larger than this size in bytes")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
return parser
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def entrypoint():
|
|
35
|
+
parser = create_parser()
|
|
36
|
+
args = parser.parse_args()
|
|
37
|
+
|
|
38
|
+
if args.command == "map":
|
|
39
|
+
cli.cli_map(args)
|
|
40
|
+
elif args.command == "registry":
|
|
41
|
+
cli.cli_registry(args)
|
|
42
|
+
else:
|
|
43
|
+
parser.print_help()
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
entrypoint()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from . import core
|
|
2
|
+
from . import report
|
|
3
|
+
from .handlers.registry import HandlerRegistry
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import importlib.util
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def cli_map(args):
|
|
10
|
+
scanner = core.default_scanner(args.path)
|
|
11
|
+
source = core.default_source(args.path)
|
|
12
|
+
reports_by_file = core.analyze_project(scanner, source, calls=args.calls, lines=args.lines)
|
|
13
|
+
report.display_txt(
|
|
14
|
+
reports_by_file,
|
|
15
|
+
sys.stdout,
|
|
16
|
+
lines=args.lines,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def cli_registry(args):
|
|
20
|
+
print('Supported languages:')
|
|
21
|
+
for file_type, info in HandlerRegistry.LIBRARIES.items():
|
|
22
|
+
spec = importlib.util.find_spec(info['lib'])
|
|
23
|
+
has_package = spec is not None
|
|
24
|
+
extra_info = []
|
|
25
|
+
if not has_package:
|
|
26
|
+
status = "Missing"
|
|
27
|
+
extra_info.append('\t')
|
|
28
|
+
extra_info.append(f'to install run `pip install {info["package_name"]}`')
|
|
29
|
+
else:
|
|
30
|
+
status = "Installed"
|
|
31
|
+
print('\t', file_type, '\t', status, *extra_info)
|
|
32
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from . import scanner
|
|
2
|
+
from .handlers.registry import HandlerRegistry
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
def default_scanner(path: str):
|
|
8
|
+
registry = HandlerRegistry()
|
|
9
|
+
s = scanner.ProjectScanner(registry)
|
|
10
|
+
return s
|
|
11
|
+
|
|
12
|
+
def default_source(path):
|
|
13
|
+
return scanner.DiskSource(path)
|
|
14
|
+
|
|
15
|
+
def analyze_project(scanner, source, calls=False, lines=False):
|
|
16
|
+
reports = {}
|
|
17
|
+
for file_path, relative_path, handler, read_func in scanner.scan(source):
|
|
18
|
+
try:
|
|
19
|
+
source_file = handler(file_path, read_func())
|
|
20
|
+
except Exception as e:
|
|
21
|
+
sys.stderr.write(f'Failed to parse file={file_path} with handler={handler.__name__}. Skipping. Error="{str(e)}"')
|
|
22
|
+
continue
|
|
23
|
+
|
|
24
|
+
file_report = {
|
|
25
|
+
"path": str(file_path),
|
|
26
|
+
"lines": source_file.get_line_count(),
|
|
27
|
+
"functions": list(source_file.iter_functions(include_calls=calls)),
|
|
28
|
+
"definitions": list(source_file.iter_definitions(include_methods=True, include_calls=calls)),
|
|
29
|
+
}
|
|
30
|
+
reports[relative_path] = file_report
|
|
31
|
+
return reports
|
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import List, Dict, Optional
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Variable:
|
|
7
|
+
name: str
|
|
8
|
+
type_hint: Optional[str] = None
|
|
9
|
+
value_snippet: Optional[str] = None # For constants like MAX_RETRIES = 5
|
|
10
|
+
|
|
11
|
+
def __str__(self):
|
|
12
|
+
type_prefix = ''
|
|
13
|
+
if self.type_hint:
|
|
14
|
+
type_prefix = f'{self.type_hint} '
|
|
15
|
+
value_suffix = ''
|
|
16
|
+
if self.value_snippet:
|
|
17
|
+
value_suffix = f' {self.value_snippet}'
|
|
18
|
+
return f'{type_prefix}{self.name}{value_suffix}'
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Function:
|
|
22
|
+
name: str
|
|
23
|
+
source_lines: tuple[int, int]
|
|
24
|
+
arguments: str = ""
|
|
25
|
+
return_type: str = ""
|
|
26
|
+
is_public: bool = False
|
|
27
|
+
calls: List[str] = field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
def __str__(self):
|
|
30
|
+
return_suffix = ''
|
|
31
|
+
if self.return_type:
|
|
32
|
+
return_suffix = ' ➜ {return_type}'
|
|
33
|
+
return f'ƒ {self.name}({self.arguments}){return_suffix}'
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Definition:
|
|
38
|
+
name: str
|
|
39
|
+
kind: str # "class", "struct", "interface"
|
|
40
|
+
source_lines: tuple[int, int]
|
|
41
|
+
properties: List[Variable] = field(default_factory=list)
|
|
42
|
+
methods: List[Function] = field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def __str__(self):
|
|
46
|
+
return f'{self.kind} {self.name}'
|
|
47
|
+
|
|
48
|
+
class SourceFile(ABC):
|
|
49
|
+
def __init__(self, path: str, code: bytes):
|
|
50
|
+
self.path = path
|
|
51
|
+
self.code = code
|
|
52
|
+
self._tree = self._parse()
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def get_line_count(self):
|
|
56
|
+
"""Fetch total lines in source file"""
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def _parse(self):
|
|
61
|
+
"""Initialize the Tree-sitter parser for the specific language."""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def iter_definitions(self) -> List[Definition]:
|
|
66
|
+
"""
|
|
67
|
+
Returns Classes or Structs.
|
|
68
|
+
Implementation should populate .properties and .methods.
|
|
69
|
+
"""
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def iter_functions(self) -> List[Function]:
|
|
74
|
+
"""Returns top-level functions (logic not bound to a class/struct)."""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def iter_globals(self) -> List[Variable]:
|
|
79
|
+
"""Returns top-level constants and global variables."""
|
|
80
|
+
pass
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from .base import Definition, Function, Variable, SourceFile
|
|
2
|
+
from tree_sitter import Language, Parser
|
|
3
|
+
import tree_sitter_go as tsgo
|
|
4
|
+
|
|
5
|
+
from .tree_utils import ts_get_captures, ts_line_info, one, q
|
|
6
|
+
|
|
7
|
+
class GoSourceFile(SourceFile):
|
|
8
|
+
lang = Language(tsgo.language())
|
|
9
|
+
|
|
10
|
+
def _parse(self):
|
|
11
|
+
self.parser = Parser(self.lang)
|
|
12
|
+
return self.parser.parse(self.code)
|
|
13
|
+
|
|
14
|
+
def _get_text(self, node):
|
|
15
|
+
if node is None:
|
|
16
|
+
return ''
|
|
17
|
+
return self.code[node.start_byte:node.end_byte].decode('utf-8')
|
|
18
|
+
|
|
19
|
+
def get_line_count(self):
|
|
20
|
+
"""Fetch total lines in source file"""
|
|
21
|
+
return ts_line_info(self._tree.root_node)['source_lines'][1]
|
|
22
|
+
|
|
23
|
+
def iter_definitions(self, include_calls=False, include_methods=False) -> list[Definition]:
|
|
24
|
+
"""Returns Structs and Interfaces, populating their methods."""
|
|
25
|
+
# Query for type declarations (structs and interfaces)
|
|
26
|
+
query = q(self.lang, """
|
|
27
|
+
(type_declaration
|
|
28
|
+
(type_spec
|
|
29
|
+
name: (type_identifier) @name
|
|
30
|
+
type: [
|
|
31
|
+
(struct_type)
|
|
32
|
+
(interface_type)
|
|
33
|
+
] @type_body)) @definition
|
|
34
|
+
""")
|
|
35
|
+
definitions = []
|
|
36
|
+
|
|
37
|
+
all_methods = self._get_all_methods()
|
|
38
|
+
|
|
39
|
+
for _, captures in ts_get_captures(query, self._tree.root_node):
|
|
40
|
+
node = one(captures.get('definition'))
|
|
41
|
+
name_node = node.child_by_field_name("name") or node.named_child(0).child_by_field_name("name")
|
|
42
|
+
|
|
43
|
+
if not name_node:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
type_name = self._get_text(name_node)
|
|
47
|
+
type_body = node.named_child(0).child_by_field_name("type")
|
|
48
|
+
kind = "struct" if type_body.type == "struct_type" else "interface"
|
|
49
|
+
|
|
50
|
+
defn = Definition(name=type_name, kind=kind, **ts_line_info(node))
|
|
51
|
+
defn.methods = [m for m in all_methods if m._receiver_type == type_name]
|
|
52
|
+
definitions.append(defn)
|
|
53
|
+
|
|
54
|
+
return definitions
|
|
55
|
+
|
|
56
|
+
def iter_functions(self, include_calls=False) -> list[Function]:
|
|
57
|
+
"""Returns top-level functions (those without receivers)."""
|
|
58
|
+
query = q(self.lang, """
|
|
59
|
+
(function_declaration
|
|
60
|
+
name: (identifier) @name
|
|
61
|
+
parameters: (parameter_list) @params) @func
|
|
62
|
+
""")
|
|
63
|
+
|
|
64
|
+
functions = []
|
|
65
|
+
for _, captures in ts_get_captures(query, self._tree.root_node):
|
|
66
|
+
func_node = captures.get('func')
|
|
67
|
+
name = self._get_text(one(captures.get('name')))
|
|
68
|
+
params = self._get_text(one(captures.get('params'))).strip("()")
|
|
69
|
+
functions.append(Function(name=name, arguments=params, **ts_line_info(func_node)))
|
|
70
|
+
|
|
71
|
+
return functions
|
|
72
|
+
|
|
73
|
+
def iter_globals(self) -> list[Variable]:
|
|
74
|
+
"""Returns top-level var and const declarations."""
|
|
75
|
+
query = q(self.lang, """
|
|
76
|
+
(var_declaration
|
|
77
|
+
(var_spec name: (identifier) @name)) @var
|
|
78
|
+
(const_declaration
|
|
79
|
+
(const_spec name: (identifier) @name)) @const
|
|
80
|
+
""")
|
|
81
|
+
|
|
82
|
+
variables = []
|
|
83
|
+
|
|
84
|
+
for _, captures in ts_get_captures(query, self._tree.root_node):
|
|
85
|
+
# Simplification: grabbing the first identifier in the spec
|
|
86
|
+
node = captures.get('var') or captures.get('const')
|
|
87
|
+
name_node = node.named_child(0).child_by_field_name("name")
|
|
88
|
+
if name_node:
|
|
89
|
+
variables.append(Variable(name=self._get_text(name_node)))
|
|
90
|
+
|
|
91
|
+
return variables
|
|
92
|
+
|
|
93
|
+
def _get_all_methods(self) -> list:
|
|
94
|
+
"""Helper to find all method_declarations and identify their receiver type."""
|
|
95
|
+
query = q(self.lang, """
|
|
96
|
+
(method_declaration
|
|
97
|
+
receiver: (parameter_list
|
|
98
|
+
(parameter_declaration
|
|
99
|
+
type: [
|
|
100
|
+
(pointer_type (type_identifier) @recv)
|
|
101
|
+
(type_identifier) @recv
|
|
102
|
+
]
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
name: (field_identifier) @name
|
|
106
|
+
parameters: (parameter_list) @params
|
|
107
|
+
) @method
|
|
108
|
+
""")
|
|
109
|
+
|
|
110
|
+
methods = []
|
|
111
|
+
for _, captures in ts_get_captures(query, self._tree.root_node):
|
|
112
|
+
method_node = one(captures.get('method'))
|
|
113
|
+
name_node = one(captures.get('name'))
|
|
114
|
+
recv_node = one(captures.get('recv'))
|
|
115
|
+
param_node = one(captures.get('params'))
|
|
116
|
+
|
|
117
|
+
func = Function(
|
|
118
|
+
name=self._get_text(name_node),
|
|
119
|
+
arguments=self._get_text(param_node).strip("()"),
|
|
120
|
+
**ts_line_info(method_node)
|
|
121
|
+
)
|
|
122
|
+
# Temporary attribute to help iter_definitions link them
|
|
123
|
+
func._receiver_type = self._get_text(recv_node)
|
|
124
|
+
methods.append(func)
|
|
125
|
+
|
|
126
|
+
return methods
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from .base import Variable, Function, Definition, SourceFile
|
|
2
|
+
from tree_sitter import Language, Parser
|
|
3
|
+
import tree_sitter_javascript as tsjavascript
|
|
4
|
+
from .tree_utils import ts_get_captures, ts_line_info, one, q
|
|
5
|
+
|
|
6
|
+
def get_child_by_type(node, node_type):
|
|
7
|
+
for child in node.children:
|
|
8
|
+
if child.type == node_type:
|
|
9
|
+
return child
|
|
10
|
+
return None
|
|
11
|
+
|
|
12
|
+
class JsSourceFile(SourceFile):
|
|
13
|
+
lang = Language(tsjavascript.language())
|
|
14
|
+
|
|
15
|
+
def _parse(self):
|
|
16
|
+
self.parser = Parser(self.lang)
|
|
17
|
+
self._tree = self.parser.parse(self.code)
|
|
18
|
+
return self._tree
|
|
19
|
+
|
|
20
|
+
def _get_text(self, node):
|
|
21
|
+
if node is None:
|
|
22
|
+
return ''
|
|
23
|
+
return self.code[node.start_byte:node.end_byte].decode('utf-8')
|
|
24
|
+
|
|
25
|
+
def get_line_count(self):
|
|
26
|
+
"""Fetch total lines in source file"""
|
|
27
|
+
return ts_line_info(self._tree.root_node)['source_lines'][1]
|
|
28
|
+
|
|
29
|
+
def iter_functions(self, include_calls=False) -> list[Function]:
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
Use case 1: Arrow functions:
|
|
33
|
+
|
|
34
|
+
query:
|
|
35
|
+
(lexical_declaration
|
|
36
|
+
(variable_declarator
|
|
37
|
+
value: (arrow_function)
|
|
38
|
+
)
|
|
39
|
+
) @arrowfunc1
|
|
40
|
+
example:
|
|
41
|
+
const foobar = () => {
|
|
42
|
+
console.log('im an arrow function')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Use case 2: actual functions
|
|
46
|
+
|
|
47
|
+
query:
|
|
48
|
+
(function_declaration) @func
|
|
49
|
+
example:
|
|
50
|
+
function applyDiscount(p) {
|
|
51
|
+
return p * 0.9;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Use case 3: anon functions
|
|
55
|
+
|
|
56
|
+
query:
|
|
57
|
+
(lexical_declaration
|
|
58
|
+
(variable_declarator
|
|
59
|
+
value: (function_expression)
|
|
60
|
+
)
|
|
61
|
+
) @anonFunc
|
|
62
|
+
example:
|
|
63
|
+
const anonymousExpress = function(a, b) {
|
|
64
|
+
return a + b;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
query = q(self.lang, """
|
|
71
|
+
(program [
|
|
72
|
+
;; 1. Standard or Exported Declaration
|
|
73
|
+
(function_declaration
|
|
74
|
+
name: (identifier) @name
|
|
75
|
+
parameters: (formal_parameters) @params) @anchor
|
|
76
|
+
(export_statement
|
|
77
|
+
(function_declaration
|
|
78
|
+
name: (identifier) @name
|
|
79
|
+
parameters: (formal_parameters) @params)) @anchor
|
|
80
|
+
|
|
81
|
+
;; 2. Lexical Assignments (Arrow or Expression)
|
|
82
|
+
(lexical_declaration
|
|
83
|
+
(variable_declarator
|
|
84
|
+
name: (identifier) @name
|
|
85
|
+
value: [
|
|
86
|
+
(function_expression parameters: (formal_parameters) @params)
|
|
87
|
+
(arrow_function parameters: (formal_parameters) @params)
|
|
88
|
+
]
|
|
89
|
+
)
|
|
90
|
+
) @anchor
|
|
91
|
+
])
|
|
92
|
+
""")
|
|
93
|
+
functions = []
|
|
94
|
+
for _, captures in ts_get_captures(query, self._tree.root_node):
|
|
95
|
+
|
|
96
|
+
name_node = one(captures.get('name', [None]))
|
|
97
|
+
param_node = one(captures.get('params', [None]))
|
|
98
|
+
anchor_node = one(captures.get('anchor', [None]))
|
|
99
|
+
|
|
100
|
+
if name_node and param_node:
|
|
101
|
+
name = self._get_text(name_node)
|
|
102
|
+
args = self._get_text(param_node).strip("()")
|
|
103
|
+
|
|
104
|
+
f = Function(name=name, arguments=args, **ts_line_info(anchor_node))
|
|
105
|
+
if include_calls and anchor_node:
|
|
106
|
+
f.calls = self._extract_calls(anchor_node)
|
|
107
|
+
functions.append(f)
|
|
108
|
+
|
|
109
|
+
return functions
|
|
110
|
+
|
|
111
|
+
def iter_definitions(self, include_methods=False, include_calls=False) -> list[Definition]:
|
|
112
|
+
query = q(self.lang, """
|
|
113
|
+
(class_declaration
|
|
114
|
+
name: (identifier) @name
|
|
115
|
+
body: (class_body) @body) @class
|
|
116
|
+
""")
|
|
117
|
+
|
|
118
|
+
definitions = []
|
|
119
|
+
|
|
120
|
+
for _, captures in ts_get_captures(query, self._tree.root_node):
|
|
121
|
+
name_node = one(captures.get('name'))
|
|
122
|
+
class_node = one(captures.get('class'))
|
|
123
|
+
class_name = self._get_text(name_node)
|
|
124
|
+
defn = Definition(name=class_name, kind="class", **ts_line_info(class_node))
|
|
125
|
+
|
|
126
|
+
if not include_methods:
|
|
127
|
+
definitions.append(defn)
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
body_node = class_node.child_by_field_name("body")
|
|
131
|
+
# In JS class_body, we look for method_definition
|
|
132
|
+
for child in body_node.children:
|
|
133
|
+
if child.type == "method_definition":
|
|
134
|
+
name_node = child.child_by_field_name("name")
|
|
135
|
+
param_node = child.child_by_field_name("parameters")
|
|
136
|
+
|
|
137
|
+
method = Function(
|
|
138
|
+
name=self._get_text(name_node),
|
|
139
|
+
arguments=self._get_text(param_node).strip("()"),
|
|
140
|
+
**ts_line_info()
|
|
141
|
+
)
|
|
142
|
+
if include_calls:
|
|
143
|
+
method.calls = self._extract_calls(child)
|
|
144
|
+
defn.methods.append(method)
|
|
145
|
+
|
|
146
|
+
definitions.append(defn)
|
|
147
|
+
return definitions
|
|
148
|
+
|
|
149
|
+
def iter_globals(self):
|
|
150
|
+
query = q(self.lang, """
|
|
151
|
+
(program [
|
|
152
|
+
(lexical_declaration (variable_declarator name: (identifier) @name))
|
|
153
|
+
(variable_declaration (variable_declarator name: (identifier) @name))
|
|
154
|
+
] @global)
|
|
155
|
+
""")
|
|
156
|
+
captures = query.captures(self._tree.root_node)
|
|
157
|
+
return [self._get_text(node.child_by_field_name("name")) for node in captures.get('global', [])]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import tree_sitter_python as tspython
|
|
2
|
+
from tree_sitter import Language, Parser
|
|
3
|
+
from .base import SourceFile, Function, Variable, Definition
|
|
4
|
+
from .tree_utils import ts_get_captures,ts_line_info, one, q
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def extract_decorated_function(node):
|
|
8
|
+
for child in node.children:
|
|
9
|
+
if child.type == 'function_definition':
|
|
10
|
+
return child
|
|
11
|
+
return None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PythonSourceFile(SourceFile):
|
|
15
|
+
lang = Language(tspython.language())
|
|
16
|
+
|
|
17
|
+
def _parse(self):
|
|
18
|
+
self.parser = Parser(self.lang)
|
|
19
|
+
return self.parser.parse(self.code)
|
|
20
|
+
|
|
21
|
+
def _get_text(self, node):
|
|
22
|
+
if node is None:
|
|
23
|
+
return ''
|
|
24
|
+
return self.code[node.start_byte:node.end_byte].decode('utf-8')
|
|
25
|
+
|
|
26
|
+
def get_line_count(self):
|
|
27
|
+
"""Fetch total lines in source file"""
|
|
28
|
+
return ts_line_info(self._tree.root_node)['source_lines'][1]
|
|
29
|
+
|
|
30
|
+
def iter_functions(self, include_calls=False) -> list[Function]:
|
|
31
|
+
query = q(self.lang, """
|
|
32
|
+
(module (function_definition
|
|
33
|
+
name: (identifier) @name
|
|
34
|
+
parameters: (parameters) @params) @func)
|
|
35
|
+
""")
|
|
36
|
+
functions = []
|
|
37
|
+
for _, captures in ts_get_captures(query, self._tree.root_node):
|
|
38
|
+
func_node = one(captures.get('func'))
|
|
39
|
+
name_node = one(captures.get("name"))
|
|
40
|
+
param_node = one(captures.get("params"))
|
|
41
|
+
|
|
42
|
+
if name_node:
|
|
43
|
+
name = self._get_text(name_node)
|
|
44
|
+
params = self._get_text(param_node).strip("()")
|
|
45
|
+
|
|
46
|
+
f = Function(name=name, arguments=params, **ts_line_info(func_node))
|
|
47
|
+
if include_calls:
|
|
48
|
+
f.calls = self._extract_calls(func_node)
|
|
49
|
+
functions.append(f)
|
|
50
|
+
|
|
51
|
+
return functions
|
|
52
|
+
|
|
53
|
+
def iter_definitions(self, include_methods=False, include_calls=False) -> list[Definition]:
|
|
54
|
+
# Query for the class and its internal methods
|
|
55
|
+
query = q(self.lang, """
|
|
56
|
+
(class_definition
|
|
57
|
+
name: (identifier) @name
|
|
58
|
+
body: (block) @body) @class
|
|
59
|
+
""")
|
|
60
|
+
definitions = []
|
|
61
|
+
for _, captures in ts_get_captures(query, self._tree.root_node):
|
|
62
|
+
class_name = self._get_text(captures.get('name')[0])
|
|
63
|
+
body_node = one(captures.get('body'))
|
|
64
|
+
class_node = one(captures.get('class'))
|
|
65
|
+
|
|
66
|
+
# Initialize the Definition
|
|
67
|
+
defn = Definition(name=class_name, kind="class", **ts_line_info(class_node))
|
|
68
|
+
|
|
69
|
+
if not include_methods:
|
|
70
|
+
continue
|
|
71
|
+
# Walk the class body to find function_definitions (Methods)
|
|
72
|
+
for child in body_node.children:
|
|
73
|
+
entry = child
|
|
74
|
+
if entry.type == "decorated_definition":
|
|
75
|
+
decorated_function = extract_decorated_function(entry)
|
|
76
|
+
if decorated_function is not None:
|
|
77
|
+
entry = decorated_function
|
|
78
|
+
if entry.type == "function_definition":
|
|
79
|
+
method_name = self._get_text(entry.child_by_field_name("name"))
|
|
80
|
+
params = self._get_text(entry.child_by_field_name("parameters")).strip("()")
|
|
81
|
+
method = Function(name=method_name, arguments=params, **ts_line_info(child))
|
|
82
|
+
if include_calls:
|
|
83
|
+
method.calls = self._extract_calls(entry)
|
|
84
|
+
defn.methods.append(method)
|
|
85
|
+
definitions.append(defn)
|
|
86
|
+
return definitions
|
|
87
|
+
|
|
88
|
+
def iter_globals(self):
|
|
89
|
+
raise NotImplemented()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from typing import Dict, Callable, Type
|
|
2
|
+
from .base import SourceFile
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
def _load_python():
|
|
7
|
+
from .handler_py import PythonSourceFile
|
|
8
|
+
return PythonSourceFile
|
|
9
|
+
|
|
10
|
+
def _load_go():
|
|
11
|
+
from .handler_go import GoSourceFile
|
|
12
|
+
return GoSourceFile
|
|
13
|
+
|
|
14
|
+
def _load_js():
|
|
15
|
+
from .handler_js import JsSourceFile
|
|
16
|
+
return JsSourceFile
|
|
17
|
+
|
|
18
|
+
class HandlerRegistry:
|
|
19
|
+
LIBRARIES = {
|
|
20
|
+
"py": {
|
|
21
|
+
'package_name': 'tree-sitter-python',
|
|
22
|
+
'lib': 'tree_sitter_python',
|
|
23
|
+
'loader': _load_python,
|
|
24
|
+
},
|
|
25
|
+
"go": {
|
|
26
|
+
'package_name': 'tree-sitter-go',
|
|
27
|
+
'lib': 'tree_sitter_go',
|
|
28
|
+
'loader': _load_go,
|
|
29
|
+
},
|
|
30
|
+
"js": {
|
|
31
|
+
'package_name': 'tree-sitter-javascript',
|
|
32
|
+
'lib': 'tree_sitter_javascript',
|
|
33
|
+
'loader': _load_js,
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
# Maps extension -> A function that returns the Class
|
|
39
|
+
self._loaders = {
|
|
40
|
+
".py": self.LIBRARIES['py']['loader'],
|
|
41
|
+
".go": self.LIBRARIES['go']['loader'],
|
|
42
|
+
".js": self.LIBRARIES['js']['loader'],
|
|
43
|
+
}
|
|
44
|
+
self._handlers: Dict[str, Type[SourceFile]] = {}
|
|
45
|
+
self._errors = {}
|
|
46
|
+
|
|
47
|
+
def get_handler_class(self, extension: str) -> Type[SourceFile]:
|
|
48
|
+
ext = extension.lower()
|
|
49
|
+
if not self.has_handler(ext):
|
|
50
|
+
raise ValueError(f"Unsupported extension: {ext}")
|
|
51
|
+
|
|
52
|
+
# Check cache so we don't re-import every time
|
|
53
|
+
if ext not in self._handlers:
|
|
54
|
+
loader_func = self._loaders[ext]
|
|
55
|
+
try:
|
|
56
|
+
self._handlers[ext] = loader_func()
|
|
57
|
+
except ModuleNotFoundError as e:
|
|
58
|
+
sys.stderr.write(f"Skipping files of type {ext} due to import failure for required module. Error: {str(e)}\n")
|
|
59
|
+
self._errors[ext] = e
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
return self._handlers[ext]
|
|
63
|
+
|
|
64
|
+
def has_handler(self, extension: str) -> bool:
|
|
65
|
+
ext = extension.lower()
|
|
66
|
+
if ext in self._errors:
|
|
67
|
+
return False
|
|
68
|
+
return ext in self._loaders or ext in self._handlers
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from tree_sitter import QueryCursor, Query
|
|
3
|
+
except (ImportError, AttributeError):
|
|
4
|
+
QueryCursor = None
|
|
5
|
+
Query = None
|
|
6
|
+
|
|
7
|
+
def ts_line_info(node):
|
|
8
|
+
if isinstance(node, list) and len(node) == 1:
|
|
9
|
+
node = node[0]
|
|
10
|
+
return {'source_lines': (node.start_point[0], node.end_point[0])}
|
|
11
|
+
|
|
12
|
+
def ts_get_captures(query, root_node):
|
|
13
|
+
"""
|
|
14
|
+
A cross-version generator for tree-sitter captures.
|
|
15
|
+
"""
|
|
16
|
+
# Version A: Modern API (0.21.0+)
|
|
17
|
+
if QueryCursor is not None:
|
|
18
|
+
cursor = QueryCursor(query)
|
|
19
|
+
for index, captures in cursor.matches(root_node):
|
|
20
|
+
yield index, captures
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
# Version B: Older versions dont have cursor and allow query.captures(root_node) which returns a list
|
|
24
|
+
if hasattr(query, 'captures'):
|
|
25
|
+
results = query.matches(root_node)
|
|
26
|
+
for index, captures in results:
|
|
27
|
+
yield index, captures
|
|
28
|
+
else:
|
|
29
|
+
raise RuntimeError("No compatible tree-sitter capture method found.")
|
|
30
|
+
|
|
31
|
+
def one(source):
|
|
32
|
+
""" helper that extracts the first element or child for a tree node or None """
|
|
33
|
+
if source is None:
|
|
34
|
+
return None
|
|
35
|
+
if hasattr(source, "children"):
|
|
36
|
+
source = source.children
|
|
37
|
+
if isinstance(source, list):
|
|
38
|
+
if len(source) >= 1:
|
|
39
|
+
return source[0]
|
|
40
|
+
else:
|
|
41
|
+
return None
|
|
42
|
+
raise ValueError("Unsupported type sent to one()")
|
|
43
|
+
|
|
44
|
+
def q(lang, text):
|
|
45
|
+
if Query is not None:
|
|
46
|
+
return Query(lang, text)
|
|
47
|
+
return lang.query(text)
|
|
48
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
def as_sorted_dict(d):
|
|
2
|
+
return dict(sorted(d.items()))
|
|
3
|
+
|
|
4
|
+
def line_count_pad_size(source):
|
|
5
|
+
m = 0
|
|
6
|
+
for report in source.values():
|
|
7
|
+
m = max(m, report['lines'])
|
|
8
|
+
return (len(str(m)) * 2) + len('[:]')
|
|
9
|
+
|
|
10
|
+
def get_line_range_str(item=None, size=None, default=''):
|
|
11
|
+
if size == 0:
|
|
12
|
+
lines = (0, 0)
|
|
13
|
+
elif size is not None:
|
|
14
|
+
lines = (1, size)
|
|
15
|
+
elif hasattr(item, 'source_lines'):
|
|
16
|
+
lines = (item.source_lines[0], item.source_lines[1])
|
|
17
|
+
else:
|
|
18
|
+
return default
|
|
19
|
+
|
|
20
|
+
range_text = f"[{lines[0]}:{lines[1]}]"
|
|
21
|
+
return f"{range_text}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def display_txt(reports_by_file, output, lines=False):
|
|
25
|
+
if lines:
|
|
26
|
+
max_gutter = line_count_pad_size(reports_by_file) + 1 # for padding
|
|
27
|
+
gutter_spacing = " " * max_gutter
|
|
28
|
+
else:
|
|
29
|
+
max_gutter = 0
|
|
30
|
+
gutter_spacing = ''
|
|
31
|
+
line_range = ''
|
|
32
|
+
|
|
33
|
+
for file_path, report in as_sorted_dict(reports_by_file).items():
|
|
34
|
+
if lines:
|
|
35
|
+
file_lines = get_line_range_str(size=report['lines']).ljust(max_gutter)
|
|
36
|
+
else:
|
|
37
|
+
file_lines = ''
|
|
38
|
+
output.write(f"\n{file_lines}\033[1m # {file_path}\033[0m\n")
|
|
39
|
+
|
|
40
|
+
all_top_level = ([(f, 'func') for f in sorted(report.get('functions', []), key=lambda x: x.name)] +
|
|
41
|
+
[(d, 'def') for d in sorted(report.get('definitions', []), key=lambda x: x.name)])
|
|
42
|
+
|
|
43
|
+
for i, (item, kind) in enumerate(all_top_level):
|
|
44
|
+
is_last_top = (i == len(all_top_level) - 1)
|
|
45
|
+
marker = "└── " if is_last_top else "├── "
|
|
46
|
+
pipe = " " if is_last_top else "│ "
|
|
47
|
+
|
|
48
|
+
if lines:
|
|
49
|
+
line_range = get_line_range_str(item, default=gutter_spacing).ljust(max_gutter)
|
|
50
|
+
|
|
51
|
+
if kind == 'func':
|
|
52
|
+
output.write(f"{line_range} {marker} {item}\n")
|
|
53
|
+
# Render Calls
|
|
54
|
+
for j, call in enumerate(sorted(item.calls)):
|
|
55
|
+
is_last_call = (j == len(item.calls) - 1)
|
|
56
|
+
c_marker = "└── ➜ " if is_last_call else "├── ➜ "
|
|
57
|
+
output.write(f"{gutter_spacing} {pipe}{c_marker}{call}\n")
|
|
58
|
+
|
|
59
|
+
elif kind == 'def':
|
|
60
|
+
output.write(f"{line_range} {marker}○ {item}\n")
|
|
61
|
+
# Render Methods
|
|
62
|
+
methods = item.methods
|
|
63
|
+
for j, method in enumerate(methods):
|
|
64
|
+
is_last_method = (j == len(methods) - 1)
|
|
65
|
+
m_marker = "└── " if is_last_method else "├── "
|
|
66
|
+
m_pipe = " " if is_last_method else "│ "
|
|
67
|
+
|
|
68
|
+
if lines:
|
|
69
|
+
m_line_range = get_line_range_str(method, default=gutter_spacing).ljust(max_gutter)
|
|
70
|
+
else:
|
|
71
|
+
m_line_range = ''
|
|
72
|
+
output.write(f"{m_line_range} {pipe}{m_marker}{method}\n")
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Generator, Tuple, Optional, Set
|
|
4
|
+
|
|
5
|
+
import zipfile
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Callable
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class FileEntry:
|
|
11
|
+
full_path: Path # The logical path within the project
|
|
12
|
+
rel_path: Path
|
|
13
|
+
size: int # For your max_bytes check
|
|
14
|
+
reader: Callable[[], bytes] # The "make_reader" logic
|
|
15
|
+
|
|
16
|
+
class DiskSource:
|
|
17
|
+
def __init__(self, root_path: Path):
|
|
18
|
+
self.path = Path(root_path)
|
|
19
|
+
self.is_single_file = self.path.is_file()
|
|
20
|
+
self.root = self.path.parent if self.is_single_file else self.path
|
|
21
|
+
|
|
22
|
+
def walk(self) -> Generator[FileEntry, None, None]:
|
|
23
|
+
if self.is_single_file:
|
|
24
|
+
yield FileEntry(
|
|
25
|
+
full_path=self.path,
|
|
26
|
+
rel_path=Path(self.path.name),
|
|
27
|
+
size=self.path.stat().st_size,
|
|
28
|
+
reader=self.path.read_bytes
|
|
29
|
+
)
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
for root, dirs, files in os.walk(self.root):
|
|
33
|
+
dirs[:] = [d for d in dirs if not d.startswith(".") and d not in ProjectScanner.DEFAULT_IGNORE_LIST]
|
|
34
|
+
for f in files:
|
|
35
|
+
p = Path(root) / f
|
|
36
|
+
yield FileEntry(
|
|
37
|
+
full_path=p,
|
|
38
|
+
rel_path=p.relative_to(self.root),
|
|
39
|
+
size=p.stat().st_size,
|
|
40
|
+
reader=p.read_bytes
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
class ZipSource:
|
|
44
|
+
def __init__(self, buffer):
|
|
45
|
+
self.zip = zipfile.ZipFile(buffer)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_path(cls, path):
|
|
49
|
+
# Open file is seekable
|
|
50
|
+
return cls(open(path, 'rb'))
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_stream(cls, stream):
|
|
54
|
+
# Pipes aren't seekable, so we must read the whole thing into memory
|
|
55
|
+
# to allow ZipFile to jump to the Central Directory at the end.
|
|
56
|
+
seekable_buffer = io.BytesIO(stream.read())
|
|
57
|
+
return cls(seekable_buffer)
|
|
58
|
+
|
|
59
|
+
def walk(self) -> Generator[FileEntry, None, None]:
|
|
60
|
+
for info in self.zip.infolist():
|
|
61
|
+
if info.is_dir():
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
rel_path = Path(info.filename)
|
|
65
|
+
yield FileEntry(
|
|
66
|
+
full_path=rel_path,
|
|
67
|
+
rel_path=rel_path,
|
|
68
|
+
size=info.file_size,
|
|
69
|
+
reader=lambda name=info.filename: self.zip.read(name)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ProjectScanner:
|
|
74
|
+
DEFAULT_IGNORE_LIST = {
|
|
75
|
+
"node_modules", "bower_components", "vendor",
|
|
76
|
+
"dist", "build", "out", "venv", "env", "target"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def __init__(self, registry, max_bytes: int = 200_000, extra_ignored_dirs: Optional[Set[str]] = None):
|
|
80
|
+
self.registry = registry
|
|
81
|
+
self.max_bytes = max_bytes
|
|
82
|
+
self.ignored_segments = self.DEFAULT_IGNORE_LIST
|
|
83
|
+
if extra_ignored_dirs:
|
|
84
|
+
self.ignored_segments.update(extra_ignored_dirs)
|
|
85
|
+
|
|
86
|
+
def is_visible(self, entry: FileEntry) -> bool:
|
|
87
|
+
if entry.size > self.max_bytes:
|
|
88
|
+
return False
|
|
89
|
+
if not self.registry.has_handler(entry.rel_path.suffix):
|
|
90
|
+
return False
|
|
91
|
+
for part in entry.rel_path.parts:
|
|
92
|
+
if part.startswith(".") or part in self.ignored_segments:
|
|
93
|
+
return False
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
def make_reader(self, path):
|
|
97
|
+
def reader():
|
|
98
|
+
with open(path, 'rb') as f:
|
|
99
|
+
content = f.read()
|
|
100
|
+
return content
|
|
101
|
+
return reader
|
|
102
|
+
|
|
103
|
+
def scan(self, source) -> Generator[Tuple[Path, Path, type, Callable], None, None]:
|
|
104
|
+
"""
|
|
105
|
+
Yields (Full_Path, Relative_Path, Handler, Reader)
|
|
106
|
+
"""
|
|
107
|
+
for entry in source.walk():
|
|
108
|
+
# Check visibility based on the relative path (to catch ignored folders)
|
|
109
|
+
if not self.is_visible(entry):
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
handler_class = self.registry.get_handler_class(entry.rel_path.suffix)
|
|
113
|
+
if handler_class:
|
|
114
|
+
yield (
|
|
115
|
+
entry.full_path,
|
|
116
|
+
entry.rel_path,
|
|
117
|
+
handler_class,
|
|
118
|
+
entry.reader
|
|
119
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: radixcodemap
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Summarize code repositories quickly and (soon) with multiple verbosity levels
|
|
5
|
+
Home-page: https://github.com/jdotpy/radix-map
|
|
6
|
+
Download-URL: https://github.com/jdotpy/radix-map/tarball/master
|
|
7
|
+
Author: KJ
|
|
8
|
+
Author-email: jdotpy@users.noreply.github.com
|
|
9
|
+
Keywords: tools
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: tree-sitter
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest; extra == "dev"
|
|
16
|
+
Requires-Dist: twine; extra == "dev"
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: author-email
|
|
19
|
+
Dynamic: classifier
|
|
20
|
+
Dynamic: description
|
|
21
|
+
Dynamic: description-content-type
|
|
22
|
+
Dynamic: download-url
|
|
23
|
+
Dynamic: home-page
|
|
24
|
+
Dynamic: keywords
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
Dynamic: provides-extra
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: summary
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Radix: Configurable SourceCode summarizer
|
|
33
|
+
|
|
34
|
+
Quickly summarize project structure with multiple verbosity levels (ok, so maybe there's only one option right now lol). More soon.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
1. **Clone the repository:**
|
|
41
|
+
```bash
|
|
42
|
+
git clone [https://github.com/jdotpy/radix-map.git](https://github.com/jdotpy/radix-map.git)
|
|
43
|
+
cd radix-map
|
|
44
|
+
pip install .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
2. **Install Dependencies based on code you plan on using:**
|
|
49
|
+
```bash
|
|
50
|
+
pip install tree-sitter-python
|
|
51
|
+
pip install tree-sitter-go
|
|
52
|
+
pip install tree-sitter-javascript
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## Use
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
radix map .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Example output:
|
|
64
|
+
```bash
|
|
65
|
+
#tests/test_integration.py
|
|
66
|
+
└── ƒ get_test_pairs()
|
|
67
|
+
|
|
68
|
+
#tests/snapshots/python_ex1.py
|
|
69
|
+
├── ƒ global_helper()
|
|
70
|
+
└── ○ class DataProcessor
|
|
71
|
+
├── ƒ __init__(self, source: str)
|
|
72
|
+
├── ƒ process(self)
|
|
73
|
+
└── ƒ _validate(self)
|
|
74
|
+
|
|
75
|
+
#radix/scanner.py
|
|
76
|
+
└── ○ class ProjectScanner
|
|
77
|
+
├── ƒ __init__(self, registry, max_bytes: int = 200_000, extra_ignored_dirs: Optional[Set[str]] = None)
|
|
78
|
+
├── ƒ is_visible(self, path: Path)
|
|
79
|
+
└── ƒ scan(self, target: str)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Supported Languages
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
| Language | Status | Package Requirement |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| Python | ✅ functions & classes | tree-sitter-python |
|
|
88
|
+
| Go | 🚧 | tree-sitter-go |
|
|
89
|
+
| JavaScript | 🚧 | tree-sitter-javascript |
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
radix/__init__.py
|
|
5
|
+
radix/__main__.py
|
|
6
|
+
radix/cli.py
|
|
7
|
+
radix/core.py
|
|
8
|
+
radix/report.py
|
|
9
|
+
radix/scanner.py
|
|
10
|
+
radix/handlers/__init__.py
|
|
11
|
+
radix/handlers/base.py
|
|
12
|
+
radix/handlers/handler_go.py
|
|
13
|
+
radix/handlers/handler_js.py
|
|
14
|
+
radix/handlers/handler_py.py
|
|
15
|
+
radix/handlers/registry.py
|
|
16
|
+
radix/handlers/tree_utils.py
|
|
17
|
+
radixcodemap.egg-info/PKG-INFO
|
|
18
|
+
radixcodemap.egg-info/SOURCES.txt
|
|
19
|
+
radixcodemap.egg-info/dependency_links.txt
|
|
20
|
+
radixcodemap.egg-info/entry_points.txt
|
|
21
|
+
radixcodemap.egg-info/requires.txt
|
|
22
|
+
radixcodemap.egg-info/top_level.txt
|
|
23
|
+
tests/test_integration.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
radix
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r") as f:
|
|
4
|
+
long_description = f.read()
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name = 'radixcodemap',
|
|
8
|
+
entry_points={
|
|
9
|
+
'console_scripts': [
|
|
10
|
+
'radix=radix.__main__:entrypoint',
|
|
11
|
+
],
|
|
12
|
+
},
|
|
13
|
+
packages=['radix', 'radix.handlers'],
|
|
14
|
+
version = '0.2.0',
|
|
15
|
+
description = 'Summarize code repositories quickly and (soon) with multiple verbosity levels',
|
|
16
|
+
long_description=long_description,
|
|
17
|
+
long_description_content_type="text/markdown",
|
|
18
|
+
author = 'KJ',
|
|
19
|
+
author_email = 'jdotpy@users.noreply.github.com',
|
|
20
|
+
url = 'https://github.com/jdotpy/radix-map',
|
|
21
|
+
download_url = 'https://github.com/jdotpy/radix-map/tarball/master',
|
|
22
|
+
keywords = ['tools'],
|
|
23
|
+
install_requires=['tree-sitter'],
|
|
24
|
+
extras_require={
|
|
25
|
+
'dev': ['pytest', 'twine'],
|
|
26
|
+
},
|
|
27
|
+
classifiers = [
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
],
|
|
30
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from radix.core import analyze_project
|
|
4
|
+
from radix.report import display_txt
|
|
5
|
+
from radix.handlers.registry import HandlerRegistry
|
|
6
|
+
from .utils import MockScanner, MockSource
|
|
7
|
+
|
|
8
|
+
import io
|
|
9
|
+
|
|
10
|
+
# Setup paths
|
|
11
|
+
TEST_DATA_DIR = Path(__file__).parent / "snapshots"
|
|
12
|
+
SNAPSHOT_SUFFIX = '.snapshot'
|
|
13
|
+
|
|
14
|
+
def get_test_pairs():
|
|
15
|
+
"""
|
|
16
|
+
Finds all source files and pairs them with their corresponding .snapshot output.
|
|
17
|
+
Returns a list of tuples: (source_path, expected_output_path)
|
|
18
|
+
"""
|
|
19
|
+
pairs = []
|
|
20
|
+
for snapshot_file in TEST_DATA_DIR.glob("*" + SNAPSHOT_SUFFIX):
|
|
21
|
+
# Matches 'python_ex1.py' with 'python_ex1.py.snapshot'
|
|
22
|
+
source_file = TEST_DATA_DIR / str(snapshot_file)[:-len(SNAPSHOT_SUFFIX)]
|
|
23
|
+
if source_file.exists():
|
|
24
|
+
pairs.append((source_file, snapshot_file))
|
|
25
|
+
return pairs
|
|
26
|
+
|
|
27
|
+
@pytest.mark.parametrize("source_path, expected_path", get_test_pairs())
|
|
28
|
+
def test_report_generation(source_path, expected_path):
|
|
29
|
+
source_code = source_path.read_bytes()
|
|
30
|
+
expected_output = expected_path.read_text()
|
|
31
|
+
|
|
32
|
+
virtual_path = source_path.name
|
|
33
|
+
handler = HandlerRegistry().get_handler_class(source_path.suffix)
|
|
34
|
+
|
|
35
|
+
source = MockSource([])
|
|
36
|
+
scanner = MockScanner(None, [
|
|
37
|
+
(
|
|
38
|
+
virtual_path,
|
|
39
|
+
virtual_path,
|
|
40
|
+
handler,
|
|
41
|
+
lambda: source_code,
|
|
42
|
+
)
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
test_output = io.StringIO()
|
|
46
|
+
reports_by_file = analyze_project(scanner, source)
|
|
47
|
+
display_txt(reports_by_file, test_output)
|
|
48
|
+
result = test_output.getvalue()
|
|
49
|
+
assert result == expected_output, f"Snapshot comparison failed for {source_path.name}"
|