bubbles-lint 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.
- bubbles_lint-0.1.0/LICENSE +21 -0
- bubbles_lint-0.1.0/PKG-INFO +211 -0
- bubbles_lint-0.1.0/README.md +166 -0
- bubbles_lint-0.1.0/assets/bubbles-lint-logo.png +0 -0
- bubbles_lint-0.1.0/bubbles_lint/__init__.py +3 -0
- bubbles_lint-0.1.0/bubbles_lint/cli.py +43 -0
- bubbles_lint-0.1.0/bubbles_lint/config.py +79 -0
- bubbles_lint-0.1.0/bubbles_lint/models.py +76 -0
- bubbles_lint-0.1.0/bubbles_lint/report.py +52 -0
- bubbles_lint-0.1.0/bubbles_lint/rules/__init__.py +1 -0
- bubbles_lint-0.1.0/bubbles_lint/rules/ai_smells.py +113 -0
- bubbles_lint-0.1.0/bubbles_lint/rules/boundaries.py +94 -0
- bubbles_lint-0.1.0/bubbles_lint/rules/imports.py +14 -0
- bubbles_lint-0.1.0/bubbles_lint/rules/leaks.py +216 -0
- bubbles_lint-0.1.0/bubbles_lint/rules/registry.py +13 -0
- bubbles_lint-0.1.0/bubbles_lint/rules/size.py +111 -0
- bubbles_lint-0.1.0/bubbles_lint/scanner.py +88 -0
- bubbles_lint-0.1.0/pyproject.toml +43 -0
- bubbles_lint-0.1.0/tests/test_rules.py +171 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Syed (Sadat) Nazrul
|
|
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,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bubbles-lint
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python-first architectural linter for small, composable, Unix-like code.
|
|
5
|
+
Author: Bubbles Lint contributors
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Syed (Sadat) Nazrul
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Keywords: architecture,linter,python,static-analysis,unix-philosophy
|
|
29
|
+
Classifier: Development Status :: 3 - Alpha
|
|
30
|
+
Classifier: Environment :: Console
|
|
31
|
+
Classifier: Intended Audience :: Developers
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
40
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
41
|
+
Requires-Python: >=3.9
|
|
42
|
+
Provides-Extra: dev
|
|
43
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
<p align="center">
|
|
47
|
+
<img src="assets/bubbles-lint-logo.png" alt="Bubbles Lint logo" width="220">
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
Bubbles Lint is an architectural linter for Python code. It is inspired by Unix and Linux software design principles: do one thing well, keep modules small, compose simple parts, and make boundaries easy to inspect.
|
|
51
|
+
|
|
52
|
+
Think of it as Ruff for architecture. It does not compete with Ruff, Black, Flake8, or Pylint. Bubbles Lint looks for software shape problems: code that is too large, too coupled, too magical, or too monolithic.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install bubbles-lint
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For local development from this repository:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install -e ".[dev]"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
Scan the current repository:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bubbles-lint scan .
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Emit JSON for CI systems:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bubbles-lint scan . --json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Bubbles Lint exits with status `1` when findings are present and `0` when the scan is clean.
|
|
81
|
+
|
|
82
|
+
## Philosophy
|
|
83
|
+
|
|
84
|
+
Bubbles Lint encourages codebases where:
|
|
85
|
+
|
|
86
|
+
- modules have one clear responsibility
|
|
87
|
+
- functions and classes stay small enough to replace
|
|
88
|
+
- dependencies flow through explicit interfaces
|
|
89
|
+
- configuration is loaded at the edge
|
|
90
|
+
- side effects are isolated from pure logic
|
|
91
|
+
- text and data move through well-defined boundaries
|
|
92
|
+
- components are easy to inspect, test, and swap
|
|
93
|
+
|
|
94
|
+
The goal is not style enforcement. The goal is software design discipline, especially in Python codebases that have grown quickly with help from AI coding assistants.
|
|
95
|
+
|
|
96
|
+
## Rules
|
|
97
|
+
|
|
98
|
+
### Bubble Burst
|
|
99
|
+
|
|
100
|
+
Flags code that has grown too large:
|
|
101
|
+
|
|
102
|
+
- files over `max_file_lines`
|
|
103
|
+
- functions over `max_function_lines`
|
|
104
|
+
- classes with more than `max_class_methods`
|
|
105
|
+
- functions with more than `max_function_params`
|
|
106
|
+
|
|
107
|
+
### Bubble Leak
|
|
108
|
+
|
|
109
|
+
Flags hidden coupling and mixed side effects:
|
|
110
|
+
|
|
111
|
+
- global mutable state
|
|
112
|
+
- direct environment variable reads outside config modules
|
|
113
|
+
- functions mixing too many side-effect categories, such as filesystem, network, database, subprocess, logging, and rendering
|
|
114
|
+
|
|
115
|
+
### Bubble Boundary
|
|
116
|
+
|
|
117
|
+
Flags dependency boundary problems:
|
|
118
|
+
|
|
119
|
+
- circular imports where practical
|
|
120
|
+
- modules importing too many dependencies
|
|
121
|
+
- imports from private modules like `from package._internal import thing`
|
|
122
|
+
|
|
123
|
+
### AI Smells
|
|
124
|
+
|
|
125
|
+
Flags common broad abstractions produced in rushed or generated code:
|
|
126
|
+
|
|
127
|
+
- oversized `utils.py`, `helpers.py`, `manager.py`, and `service.py` modules
|
|
128
|
+
- broad classes ending in `Manager`, `Service`, or `Handler`
|
|
129
|
+
- deeply nested control flow
|
|
130
|
+
|
|
131
|
+
## Configuration
|
|
132
|
+
|
|
133
|
+
Configure Bubbles Lint in `pyproject.toml`:
|
|
134
|
+
|
|
135
|
+
```toml
|
|
136
|
+
[tool.bubbles-lint]
|
|
137
|
+
max_file_lines = 500
|
|
138
|
+
max_function_lines = 50
|
|
139
|
+
max_class_methods = 10
|
|
140
|
+
max_function_params = 5
|
|
141
|
+
max_imports_per_module = 20
|
|
142
|
+
max_nesting_depth = 4
|
|
143
|
+
allow_private_imports = false
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Additional knobs:
|
|
147
|
+
|
|
148
|
+
```toml
|
|
149
|
+
[tool.bubbles-lint]
|
|
150
|
+
max_side_effect_kinds = 3
|
|
151
|
+
max_ai_module_lines = 200
|
|
152
|
+
max_ai_class_dependencies = 8
|
|
153
|
+
excludes = ["generated"]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Bubbles Lint always ignores `.venv`, `venv`, `.git`, `__pycache__`, `build`, and `dist`.
|
|
157
|
+
|
|
158
|
+
Existing `[tool.bubbles]` configs are still read for compatibility, but new projects should use `[tool.bubbles-lint]`.
|
|
159
|
+
|
|
160
|
+
## Human Output
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
Bubble: Bubble Burst
|
|
164
|
+
|
|
165
|
+
src/payment_service.py:1
|
|
166
|
+
warning: File has 1437 lines; recommended maximum is 500.
|
|
167
|
+
|
|
168
|
+
Suggestion:
|
|
169
|
+
Split this module into smaller bubbles with one responsibility each.
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## JSON Output
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"findings": [
|
|
177
|
+
{
|
|
178
|
+
"rule": "bubble-burst/file-too-large",
|
|
179
|
+
"severity": "warning",
|
|
180
|
+
"path": "src/payment_service.py",
|
|
181
|
+
"line": 1,
|
|
182
|
+
"message": "File has 1437 lines; recommended maximum is 500.",
|
|
183
|
+
"suggestion": "Split this module into smaller bubbles with one responsibility each."
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
"files_scanned": 1
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## CI
|
|
191
|
+
|
|
192
|
+
Example GitHub Actions step:
|
|
193
|
+
|
|
194
|
+
```yaml
|
|
195
|
+
- name: Install Bubbles Lint
|
|
196
|
+
run: pip install .
|
|
197
|
+
|
|
198
|
+
- name: Scan architecture
|
|
199
|
+
run: bubbles-lint scan . --json
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Development
|
|
203
|
+
|
|
204
|
+
Run tests:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
pip install -e ".[dev]"
|
|
208
|
+
pytest
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The rule engine is intentionally small. New rules implement a `check(context)` method and return `Finding` objects. Parsing, configuration, scanning, reporting, and CLI code are separated so the tool can grow without becoming the kind of monolith it warns about.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/bubbles-lint-logo.png" alt="Bubbles Lint logo" width="220">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
Bubbles Lint is an architectural linter for Python code. It is inspired by Unix and Linux software design principles: do one thing well, keep modules small, compose simple parts, and make boundaries easy to inspect.
|
|
6
|
+
|
|
7
|
+
Think of it as Ruff for architecture. It does not compete with Ruff, Black, Flake8, or Pylint. Bubbles Lint looks for software shape problems: code that is too large, too coupled, too magical, or too monolithic.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install bubbles-lint
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For local development from this repository:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install -e ".[dev]"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Scan the current repository:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bubbles-lint scan .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Emit JSON for CI systems:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bubbles-lint scan . --json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Bubbles Lint exits with status `1` when findings are present and `0` when the scan is clean.
|
|
36
|
+
|
|
37
|
+
## Philosophy
|
|
38
|
+
|
|
39
|
+
Bubbles Lint encourages codebases where:
|
|
40
|
+
|
|
41
|
+
- modules have one clear responsibility
|
|
42
|
+
- functions and classes stay small enough to replace
|
|
43
|
+
- dependencies flow through explicit interfaces
|
|
44
|
+
- configuration is loaded at the edge
|
|
45
|
+
- side effects are isolated from pure logic
|
|
46
|
+
- text and data move through well-defined boundaries
|
|
47
|
+
- components are easy to inspect, test, and swap
|
|
48
|
+
|
|
49
|
+
The goal is not style enforcement. The goal is software design discipline, especially in Python codebases that have grown quickly with help from AI coding assistants.
|
|
50
|
+
|
|
51
|
+
## Rules
|
|
52
|
+
|
|
53
|
+
### Bubble Burst
|
|
54
|
+
|
|
55
|
+
Flags code that has grown too large:
|
|
56
|
+
|
|
57
|
+
- files over `max_file_lines`
|
|
58
|
+
- functions over `max_function_lines`
|
|
59
|
+
- classes with more than `max_class_methods`
|
|
60
|
+
- functions with more than `max_function_params`
|
|
61
|
+
|
|
62
|
+
### Bubble Leak
|
|
63
|
+
|
|
64
|
+
Flags hidden coupling and mixed side effects:
|
|
65
|
+
|
|
66
|
+
- global mutable state
|
|
67
|
+
- direct environment variable reads outside config modules
|
|
68
|
+
- functions mixing too many side-effect categories, such as filesystem, network, database, subprocess, logging, and rendering
|
|
69
|
+
|
|
70
|
+
### Bubble Boundary
|
|
71
|
+
|
|
72
|
+
Flags dependency boundary problems:
|
|
73
|
+
|
|
74
|
+
- circular imports where practical
|
|
75
|
+
- modules importing too many dependencies
|
|
76
|
+
- imports from private modules like `from package._internal import thing`
|
|
77
|
+
|
|
78
|
+
### AI Smells
|
|
79
|
+
|
|
80
|
+
Flags common broad abstractions produced in rushed or generated code:
|
|
81
|
+
|
|
82
|
+
- oversized `utils.py`, `helpers.py`, `manager.py`, and `service.py` modules
|
|
83
|
+
- broad classes ending in `Manager`, `Service`, or `Handler`
|
|
84
|
+
- deeply nested control flow
|
|
85
|
+
|
|
86
|
+
## Configuration
|
|
87
|
+
|
|
88
|
+
Configure Bubbles Lint in `pyproject.toml`:
|
|
89
|
+
|
|
90
|
+
```toml
|
|
91
|
+
[tool.bubbles-lint]
|
|
92
|
+
max_file_lines = 500
|
|
93
|
+
max_function_lines = 50
|
|
94
|
+
max_class_methods = 10
|
|
95
|
+
max_function_params = 5
|
|
96
|
+
max_imports_per_module = 20
|
|
97
|
+
max_nesting_depth = 4
|
|
98
|
+
allow_private_imports = false
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Additional knobs:
|
|
102
|
+
|
|
103
|
+
```toml
|
|
104
|
+
[tool.bubbles-lint]
|
|
105
|
+
max_side_effect_kinds = 3
|
|
106
|
+
max_ai_module_lines = 200
|
|
107
|
+
max_ai_class_dependencies = 8
|
|
108
|
+
excludes = ["generated"]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Bubbles Lint always ignores `.venv`, `venv`, `.git`, `__pycache__`, `build`, and `dist`.
|
|
112
|
+
|
|
113
|
+
Existing `[tool.bubbles]` configs are still read for compatibility, but new projects should use `[tool.bubbles-lint]`.
|
|
114
|
+
|
|
115
|
+
## Human Output
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
Bubble: Bubble Burst
|
|
119
|
+
|
|
120
|
+
src/payment_service.py:1
|
|
121
|
+
warning: File has 1437 lines; recommended maximum is 500.
|
|
122
|
+
|
|
123
|
+
Suggestion:
|
|
124
|
+
Split this module into smaller bubbles with one responsibility each.
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## JSON Output
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"findings": [
|
|
132
|
+
{
|
|
133
|
+
"rule": "bubble-burst/file-too-large",
|
|
134
|
+
"severity": "warning",
|
|
135
|
+
"path": "src/payment_service.py",
|
|
136
|
+
"line": 1,
|
|
137
|
+
"message": "File has 1437 lines; recommended maximum is 500.",
|
|
138
|
+
"suggestion": "Split this module into smaller bubbles with one responsibility each."
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
"files_scanned": 1
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## CI
|
|
146
|
+
|
|
147
|
+
Example GitHub Actions step:
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
- name: Install Bubbles Lint
|
|
151
|
+
run: pip install .
|
|
152
|
+
|
|
153
|
+
- name: Scan architecture
|
|
154
|
+
run: bubbles-lint scan . --json
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
Run tests:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
pip install -e ".[dev]"
|
|
163
|
+
pytest
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The rule engine is intentionally small. New rules implement a `check(context)` method and return `Finding` objects. Parsing, configuration, scanning, reporting, and CLI code are separated so the tool can grow without becoming the kind of monolith it warns about.
|
|
Binary file
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from bubbles_lint.config import load_config
|
|
8
|
+
from bubbles_lint.report import format_human, format_json
|
|
9
|
+
from bubbles_lint.scanner import scan_path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(argv: list[str] | None = None) -> int:
|
|
13
|
+
parser = build_parser()
|
|
14
|
+
args = parser.parse_args(argv)
|
|
15
|
+
|
|
16
|
+
if args.command == "scan":
|
|
17
|
+
target = Path(args.path)
|
|
18
|
+
config = load_config(target)
|
|
19
|
+
result = scan_path(target, config=config)
|
|
20
|
+
output = format_json(result) if args.json else format_human(result)
|
|
21
|
+
print(output)
|
|
22
|
+
return 1 if result.has_findings else 0
|
|
23
|
+
|
|
24
|
+
parser.print_help()
|
|
25
|
+
return 2
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
29
|
+
parser = argparse.ArgumentParser(
|
|
30
|
+
prog="bubbles-lint",
|
|
31
|
+
description="Architectural linting for small, composable Python code.",
|
|
32
|
+
)
|
|
33
|
+
subcommands = parser.add_subparsers(dest="command")
|
|
34
|
+
|
|
35
|
+
scan = subcommands.add_parser("scan", help="Scan Python files for architecture findings.")
|
|
36
|
+
scan.add_argument("path", nargs="?", default=".", help="File or directory to scan.")
|
|
37
|
+
scan.add_argument("--json", action="store_true", help="Emit machine-readable JSON output.")
|
|
38
|
+
|
|
39
|
+
return parser
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
sys.exit(main())
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tomllib
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
DEFAULT_EXCLUDES = frozenset({
|
|
10
|
+
".git",
|
|
11
|
+
".venv",
|
|
12
|
+
"__pycache__",
|
|
13
|
+
"build",
|
|
14
|
+
"dist",
|
|
15
|
+
"venv",
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Config:
|
|
21
|
+
max_file_lines: int = 500
|
|
22
|
+
max_function_lines: int = 50
|
|
23
|
+
max_class_methods: int = 10
|
|
24
|
+
max_function_params: int = 5
|
|
25
|
+
max_imports_per_module: int = 20
|
|
26
|
+
max_nesting_depth: int = 4
|
|
27
|
+
max_side_effect_kinds: int = 3
|
|
28
|
+
max_ai_module_lines: int = 200
|
|
29
|
+
max_ai_class_dependencies: int = 8
|
|
30
|
+
allow_private_imports: bool = False
|
|
31
|
+
excludes: frozenset[str] = DEFAULT_EXCLUDES
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_config(start: Path) -> Config:
|
|
35
|
+
pyproject = find_pyproject(start)
|
|
36
|
+
if pyproject is None:
|
|
37
|
+
return Config()
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
41
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
42
|
+
return Config()
|
|
43
|
+
|
|
44
|
+
tool_config = data.get("tool", {})
|
|
45
|
+
if not isinstance(tool_config, dict):
|
|
46
|
+
return Config()
|
|
47
|
+
|
|
48
|
+
values = tool_config.get("bubbles-lint", tool_config.get("bubbles", {}))
|
|
49
|
+
if not isinstance(values, dict):
|
|
50
|
+
return Config()
|
|
51
|
+
|
|
52
|
+
return build_config(values)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def find_pyproject(start: Path) -> Path | None:
|
|
56
|
+
current = start.resolve()
|
|
57
|
+
if current.is_file():
|
|
58
|
+
current = current.parent
|
|
59
|
+
|
|
60
|
+
for directory in (current, *current.parents):
|
|
61
|
+
pyproject = directory / "pyproject.toml"
|
|
62
|
+
if pyproject.exists():
|
|
63
|
+
return pyproject
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def build_config(values: dict[str, Any]) -> Config:
|
|
68
|
+
defaults = Config()
|
|
69
|
+
kwargs: dict[str, Any] = {}
|
|
70
|
+
for field_name in defaults.__dataclass_fields__:
|
|
71
|
+
if field_name not in values:
|
|
72
|
+
continue
|
|
73
|
+
value = values[field_name]
|
|
74
|
+
if field_name == "excludes":
|
|
75
|
+
if isinstance(value, list) and all(isinstance(item, str) for item in value):
|
|
76
|
+
kwargs[field_name] = DEFAULT_EXCLUDES | frozenset(value)
|
|
77
|
+
continue
|
|
78
|
+
kwargs[field_name] = value
|
|
79
|
+
return Config(**kwargs)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from bubbles_lint.config import Config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Severity(str, Enum):
|
|
12
|
+
INFO = "info"
|
|
13
|
+
WARNING = "warning"
|
|
14
|
+
ERROR = "error"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class Finding:
|
|
19
|
+
rule: str
|
|
20
|
+
severity: Severity
|
|
21
|
+
path: Path
|
|
22
|
+
line: int
|
|
23
|
+
message: str
|
|
24
|
+
suggestion: str
|
|
25
|
+
|
|
26
|
+
def to_json(self) -> dict[str, object]:
|
|
27
|
+
return {
|
|
28
|
+
"rule": self.rule,
|
|
29
|
+
"severity": self.severity.value,
|
|
30
|
+
"path": self.path.as_posix(),
|
|
31
|
+
"line": self.line,
|
|
32
|
+
"message": self.message,
|
|
33
|
+
"suggestion": self.suggestion,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class ModuleContext:
|
|
39
|
+
path: Path
|
|
40
|
+
root: Path
|
|
41
|
+
source: str
|
|
42
|
+
tree: object
|
|
43
|
+
line_count: int
|
|
44
|
+
config: Config
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def relative_path(self) -> Path:
|
|
48
|
+
try:
|
|
49
|
+
return self.path.relative_to(self.root)
|
|
50
|
+
except ValueError:
|
|
51
|
+
return self.path
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Rule(Protocol):
|
|
55
|
+
id: str
|
|
56
|
+
title: str
|
|
57
|
+
|
|
58
|
+
def check(self, context: ModuleContext) -> list[Finding]:
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class ScanResult:
|
|
64
|
+
findings: list[Finding] = field(default_factory=list)
|
|
65
|
+
files_scanned: int = 0
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def has_errors(self) -> bool:
|
|
69
|
+
return any(finding.severity is Severity.ERROR for finding in self.findings)
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def has_findings(self) -> bool:
|
|
73
|
+
return bool(self.findings)
|
|
74
|
+
|
|
75
|
+
def extend(self, findings: list[Finding]) -> None:
|
|
76
|
+
self.findings.extend(findings)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from types import MappingProxyType
|
|
6
|
+
|
|
7
|
+
from bubbles_lint.models import Finding, ScanResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
RULE_TITLES = MappingProxyType({
|
|
11
|
+
"ai-smells": "AI Smells",
|
|
12
|
+
"bubble-boundary": "Bubble Boundary",
|
|
13
|
+
"bubble-burst": "Bubble Burst",
|
|
14
|
+
"bubble-leak": "Bubble Leak",
|
|
15
|
+
"parser": "Parser",
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def format_json(result: ScanResult) -> str:
|
|
20
|
+
return json.dumps(
|
|
21
|
+
{
|
|
22
|
+
"findings": [finding.to_json() for finding in result.findings],
|
|
23
|
+
"files_scanned": result.files_scanned,
|
|
24
|
+
},
|
|
25
|
+
indent=2,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def format_human(result: ScanResult) -> str:
|
|
30
|
+
if not result.findings:
|
|
31
|
+
return f"No bubbles burst. Scanned {result.files_scanned} Python file(s)."
|
|
32
|
+
|
|
33
|
+
grouped: dict[str, list[Finding]] = defaultdict(list)
|
|
34
|
+
for finding in result.findings:
|
|
35
|
+
grouped[_family(finding.rule)].append(finding)
|
|
36
|
+
|
|
37
|
+
chunks: list[str] = []
|
|
38
|
+
for family in sorted(grouped):
|
|
39
|
+
chunks.append(f"Bubble: {RULE_TITLES.get(family, family)}")
|
|
40
|
+
for finding in grouped[family]:
|
|
41
|
+
chunks.append("")
|
|
42
|
+
chunks.append(f"{finding.path}:{finding.line}")
|
|
43
|
+
chunks.append(f"{finding.severity.value}: {finding.message}")
|
|
44
|
+
chunks.append("")
|
|
45
|
+
chunks.append("Suggestion:")
|
|
46
|
+
chunks.append(finding.suggestion)
|
|
47
|
+
|
|
48
|
+
return "\n".join(chunks)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _family(rule: str) -> str:
|
|
52
|
+
return rule.split("/", 1)[0]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Architecture rules shipped with Bubbles Lint."""
|