python-oop-analyzer 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.
- python_oop_analyzer-0.1.0/.github/workflows/ci.yml +62 -0
- python_oop_analyzer-0.1.0/.github/workflows/publish.yml +65 -0
- python_oop_analyzer-0.1.0/.gitignore +86 -0
- python_oop_analyzer-0.1.0/.pre-commit-config.yaml +24 -0
- python_oop_analyzer-0.1.0/CHANGELOG.md +34 -0
- python_oop_analyzer-0.1.0/LICENSE +21 -0
- python_oop_analyzer-0.1.0/PKG-INFO +383 -0
- python_oop_analyzer-0.1.0/README.md +348 -0
- python_oop_analyzer-0.1.0/examples/README.md +127 -0
- python_oop_analyzer-0.1.0/examples/boolean_flag_example.py +253 -0
- python_oop_analyzer-0.1.0/examples/coupling_example.py +174 -0
- python_oop_analyzer-0.1.0/examples/dictionary_usage_example.py +258 -0
- python_oop_analyzer-0.1.0/examples/encapsulation_example.py +136 -0
- python_oop_analyzer-0.1.0/examples/functions_to_objects_example.py +226 -0
- python_oop_analyzer-0.1.0/examples/null_object_example.py +182 -0
- python_oop_analyzer-0.1.0/examples/polymorphism_example.py +215 -0
- python_oop_analyzer-0.1.0/examples/reference_exposure_example.py +242 -0
- python_oop_analyzer-0.1.0/examples/type_code_example.py +258 -0
- python_oop_analyzer-0.1.0/oop_analyzer/__init__.py +12 -0
- python_oop_analyzer-0.1.0/oop_analyzer/analyzer.py +373 -0
- python_oop_analyzer-0.1.0/oop_analyzer/cli.py +160 -0
- python_oop_analyzer-0.1.0/oop_analyzer/config.py +155 -0
- python_oop_analyzer-0.1.0/oop_analyzer/formatters/__init__.py +31 -0
- python_oop_analyzer-0.1.0/oop_analyzer/formatters/base.py +101 -0
- python_oop_analyzer-0.1.0/oop_analyzer/formatters/html_formatter.py +222 -0
- python_oop_analyzer-0.1.0/oop_analyzer/formatters/json_formatter.py +37 -0
- python_oop_analyzer-0.1.0/oop_analyzer/formatters/xml_formatter.py +113 -0
- python_oop_analyzer-0.1.0/oop_analyzer/py.typed +0 -0
- python_oop_analyzer-0.1.0/oop_analyzer/rules/__init__.py +56 -0
- python_oop_analyzer-0.1.0/oop_analyzer/rules/base.py +186 -0
- python_oop_analyzer-0.1.0/oop_analyzer/rules/boolean_flag.py +391 -0
- python_oop_analyzer-0.1.0/oop_analyzer/rules/coupling.py +616 -0
- python_oop_analyzer-0.1.0/oop_analyzer/rules/dictionary_usage.py +526 -0
- python_oop_analyzer-0.1.0/oop_analyzer/rules/encapsulation.py +291 -0
- python_oop_analyzer-0.1.0/oop_analyzer/rules/functions_to_objects.py +331 -0
- python_oop_analyzer-0.1.0/oop_analyzer/rules/null_object.py +472 -0
- python_oop_analyzer-0.1.0/oop_analyzer/rules/polymorphism.py +428 -0
- python_oop_analyzer-0.1.0/oop_analyzer/rules/reference_exposure.py +348 -0
- python_oop_analyzer-0.1.0/oop_analyzer/rules/type_code.py +450 -0
- python_oop_analyzer-0.1.0/oop_analyzer/safety.py +163 -0
- python_oop_analyzer-0.1.0/pyproject.toml +130 -0
- python_oop_analyzer-0.1.0/tests/__init__.py +1 -0
- python_oop_analyzer-0.1.0/tests/conftest.py +275 -0
- python_oop_analyzer-0.1.0/tests/test_analyzer.py +336 -0
- python_oop_analyzer-0.1.0/tests/test_config.py +176 -0
- python_oop_analyzer-0.1.0/tests/test_formatters.py +193 -0
- python_oop_analyzer-0.1.0/tests/test_rules/__init__.py +1 -0
- python_oop_analyzer-0.1.0/tests/test_rules/test_boolean_flag.py +219 -0
- python_oop_analyzer-0.1.0/tests/test_rules/test_coupling.py +208 -0
- python_oop_analyzer-0.1.0/tests/test_rules/test_dictionary_usage.py +283 -0
- python_oop_analyzer-0.1.0/tests/test_rules/test_encapsulation.py +167 -0
- python_oop_analyzer-0.1.0/tests/test_rules/test_functions_to_objects.py +240 -0
- python_oop_analyzer-0.1.0/tests/test_rules/test_null_object.py +205 -0
- python_oop_analyzer-0.1.0/tests/test_rules/test_polymorphism.py +244 -0
- python_oop_analyzer-0.1.0/tests/test_rules/test_reference_exposure.py +231 -0
- python_oop_analyzer-0.1.0/tests/test_rules/test_type_code.py +216 -0
- python_oop_analyzer-0.1.0/tests/test_safety.py +166 -0
- python_oop_analyzer-0.1.0/uv.lock +905 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install uv
|
|
25
|
+
uses: astral-sh/setup-uv@v3
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: uv sync --all-extras
|
|
29
|
+
|
|
30
|
+
- name: Run linter
|
|
31
|
+
run: uv run ruff check .
|
|
32
|
+
|
|
33
|
+
- name: Run formatter check
|
|
34
|
+
run: uv run ruff format --check .
|
|
35
|
+
|
|
36
|
+
- name: Run tests
|
|
37
|
+
run: uv run pytest tests/ -v --cov=oop_analyzer --cov-report=xml
|
|
38
|
+
|
|
39
|
+
- name: Upload coverage to Codecov
|
|
40
|
+
uses: codecov/codecov-action@v4
|
|
41
|
+
with:
|
|
42
|
+
file: ./coverage.xml
|
|
43
|
+
fail_ci_if_error: false
|
|
44
|
+
|
|
45
|
+
type-check:
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
|
|
50
|
+
- name: Set up Python
|
|
51
|
+
uses: actions/setup-python@v5
|
|
52
|
+
with:
|
|
53
|
+
python-version: "3.12"
|
|
54
|
+
|
|
55
|
+
- name: Install uv
|
|
56
|
+
uses: astral-sh/setup-uv@v3
|
|
57
|
+
|
|
58
|
+
- name: Install dependencies
|
|
59
|
+
run: uv sync --all-extras
|
|
60
|
+
|
|
61
|
+
- name: Run mypy
|
|
62
|
+
run: uv run mypy oop_analyzer --ignore-missing-imports
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- name: Set up Python
|
|
14
|
+
uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
|
|
18
|
+
- name: Install build tools
|
|
19
|
+
run: pip install build twine
|
|
20
|
+
|
|
21
|
+
- name: Build package
|
|
22
|
+
run: python -m build
|
|
23
|
+
|
|
24
|
+
- name: Check package
|
|
25
|
+
run: twine check dist/*
|
|
26
|
+
|
|
27
|
+
- name: Upload artifacts
|
|
28
|
+
uses: actions/upload-artifact@v4
|
|
29
|
+
with:
|
|
30
|
+
name: dist
|
|
31
|
+
path: dist/
|
|
32
|
+
|
|
33
|
+
publish-testpypi:
|
|
34
|
+
needs: build
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
environment: testpypi
|
|
37
|
+
permissions:
|
|
38
|
+
id-token: write
|
|
39
|
+
steps:
|
|
40
|
+
- name: Download artifacts
|
|
41
|
+
uses: actions/download-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: dist
|
|
44
|
+
path: dist/
|
|
45
|
+
|
|
46
|
+
- name: Publish to TestPyPI
|
|
47
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
48
|
+
with:
|
|
49
|
+
repository-url: https://test.pypi.org/legacy/
|
|
50
|
+
|
|
51
|
+
publish-pypi:
|
|
52
|
+
needs: [build, publish-testpypi]
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
environment: pypi
|
|
55
|
+
permissions:
|
|
56
|
+
id-token: write
|
|
57
|
+
steps:
|
|
58
|
+
- name: Download artifacts
|
|
59
|
+
uses: actions/download-artifact@v4
|
|
60
|
+
with:
|
|
61
|
+
name: dist
|
|
62
|
+
path: dist/
|
|
63
|
+
|
|
64
|
+
- name: Publish to PyPI
|
|
65
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
|
|
27
|
+
# PyInstaller
|
|
28
|
+
*.manifest
|
|
29
|
+
*.spec
|
|
30
|
+
|
|
31
|
+
# Installer logs
|
|
32
|
+
pip-log.txt
|
|
33
|
+
pip-delete-this-directory.txt
|
|
34
|
+
|
|
35
|
+
# Unit test / coverage reports
|
|
36
|
+
htmlcov/
|
|
37
|
+
.tox/
|
|
38
|
+
.nox/
|
|
39
|
+
.coverage
|
|
40
|
+
.coverage.*
|
|
41
|
+
.cache
|
|
42
|
+
nosetests.xml
|
|
43
|
+
coverage.xml
|
|
44
|
+
*.cover
|
|
45
|
+
*.py,cover
|
|
46
|
+
.hypothesis/
|
|
47
|
+
.pytest_cache/
|
|
48
|
+
|
|
49
|
+
# Translations
|
|
50
|
+
*.mo
|
|
51
|
+
*.pot
|
|
52
|
+
|
|
53
|
+
# Environments
|
|
54
|
+
.env
|
|
55
|
+
.venv
|
|
56
|
+
env/
|
|
57
|
+
venv/
|
|
58
|
+
ENV/
|
|
59
|
+
env.bak/
|
|
60
|
+
venv.bak/
|
|
61
|
+
|
|
62
|
+
# IDEs
|
|
63
|
+
.idea/
|
|
64
|
+
.vscode/
|
|
65
|
+
*.swp
|
|
66
|
+
*.swo
|
|
67
|
+
*~
|
|
68
|
+
|
|
69
|
+
# mypy
|
|
70
|
+
.mypy_cache/
|
|
71
|
+
.dmypy.json
|
|
72
|
+
dmypy.json
|
|
73
|
+
|
|
74
|
+
# ruff
|
|
75
|
+
.ruff_cache/
|
|
76
|
+
|
|
77
|
+
# OS
|
|
78
|
+
.DS_Store
|
|
79
|
+
Thumbs.db
|
|
80
|
+
|
|
81
|
+
# Project specific
|
|
82
|
+
*.html
|
|
83
|
+
!examples/*.py
|
|
84
|
+
check.py
|
|
85
|
+
report.json
|
|
86
|
+
report.xml
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v4.6.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: trailing-whitespace
|
|
6
|
+
- id: end-of-file-fixer
|
|
7
|
+
- id: check-yaml
|
|
8
|
+
- id: check-toml
|
|
9
|
+
- id: check-added-large-files
|
|
10
|
+
- id: check-merge-conflict
|
|
11
|
+
|
|
12
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
13
|
+
rev: v0.4.4
|
|
14
|
+
hooks:
|
|
15
|
+
- id: ruff
|
|
16
|
+
args: [--fix]
|
|
17
|
+
- id: ruff-format
|
|
18
|
+
|
|
19
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
20
|
+
rev: v1.10.0
|
|
21
|
+
hooks:
|
|
22
|
+
- id: mypy
|
|
23
|
+
additional_dependencies: []
|
|
24
|
+
args: [--ignore-missing-imports]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-02-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release of OOP Analyzer
|
|
14
|
+
- **Encapsulation Rule**: Detects direct property access violations (Tell Don't Ask principle)
|
|
15
|
+
- Skips module attribute access (e.g., `json.JSONEncoder`)
|
|
16
|
+
- Skips class inheritance bases
|
|
17
|
+
- **Coupling Rule**: Measures coupling and builds dependency graphs
|
|
18
|
+
- Differentiates stdlib (info) vs external dependencies (warning)
|
|
19
|
+
- Shows import locations in reports
|
|
20
|
+
- **Null Object Rule**: Finds None usage replaceable by Null Object pattern
|
|
21
|
+
- Detects Optional type hints in parameters
|
|
22
|
+
- **Polymorphism Rule**: Detects if/elif chains replaceable by polymorphism
|
|
23
|
+
- **Functions to Objects Rule**: Identifies functions that could be objects
|
|
24
|
+
- **Type Code Rule**: Detects type code conditionals (State/Strategy pattern candidates)
|
|
25
|
+
- **Reference Exposure Rule**: Finds methods exposing internal mutable state
|
|
26
|
+
- **Dictionary Usage Rule**: Detects dictionaries that should be dataclasses/Pydantic models
|
|
27
|
+
- **Boolean Flag Rule**: Detects boolean flag parameters causing behavior branching
|
|
28
|
+
- CLI with JSON, XML, and HTML output formats
|
|
29
|
+
- Configuration via `.oop-analyzer.toml` or `pyproject.toml`
|
|
30
|
+
- Comprehensive test suite (200+ tests)
|
|
31
|
+
- Example files for each rule
|
|
32
|
+
|
|
33
|
+
[Unreleased]: https://github.com/agustindorda/oop-analyzer/compare/v0.1.0...HEAD
|
|
34
|
+
[0.1.0]: https://github.com/agustindorda/oop-analyzer/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agustin Dorda
|
|
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,383 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-oop-analyzer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A static analysis tool for Python OOP best practices
|
|
5
|
+
Project-URL: Homepage, https://github.com/angdmz/oop-analyzer
|
|
6
|
+
Project-URL: Documentation, https://github.com/angdmz/oop-analyzer#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/angdmz/oop-analyzer.git
|
|
8
|
+
Project-URL: Issues, https://github.com/angdmz/oop-analyzer/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/angdmz/oop-analyzer/blob/main/CHANGELOG.md
|
|
10
|
+
Author: Agustin Dorda
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: best-practices,clean-code,code-quality,linter,oop,python,static-analysis
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
|
+
Classifier: Topic :: Software Development :: Testing
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build>=1.2.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: mypy>=1.10.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pre-commit>=3.7.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: twine>=5.0.0; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# OOP Analyzer
|
|
37
|
+
|
|
38
|
+
A static analysis tool for Python that checks adherence to Object-Oriented Programming best practices. **Safe by design** - analyzes code using AST parsing only, never executes any code.
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- **Encapsulation Rule**: Detects direct property access violations (Tell Don't Ask principle)
|
|
43
|
+
- **Coupling Rule**: Measures coupling, builds dependency graphs, differentiates stdlib (soft warning) vs external dependencies (warning)
|
|
44
|
+
- **Null Object Rule**: Finds None usage and Optional type hints that could introduce nulls
|
|
45
|
+
- **Polymorphism Rule**: Detects if/elif chains replaceable by polymorphism
|
|
46
|
+
- **Functions to Objects Rule**: Identifies functions that could be better represented as objects
|
|
47
|
+
- **Type Code Rule**: Detects conditionals checking constants/enums that should use State/Strategy pattern
|
|
48
|
+
- **Reference Exposure Rule**: Finds methods returning internal mutable state that breaks encapsulation
|
|
49
|
+
- **Dictionary Usage Rule**: Detects dictionaries that should be dataclasses/Pydantic models (allows API boundaries)
|
|
50
|
+
- **Boolean Flag Rule**: Detects boolean flag parameters causing behavior branching
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
### From PyPI (recommended)
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install oop-analyzer
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### From source
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Clone the repository
|
|
64
|
+
git clone https://github.com/agustindorda/oop-analyzer.git
|
|
65
|
+
cd oop-analyzer
|
|
66
|
+
|
|
67
|
+
# Install with pip
|
|
68
|
+
pip install .
|
|
69
|
+
|
|
70
|
+
# Or install in development mode with dev dependencies
|
|
71
|
+
pip install -e ".[dev]"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Using uv
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uv add oop-analyzer
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
### Command Line
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Analyze a single file
|
|
86
|
+
oop-analyzer path/to/file.py
|
|
87
|
+
|
|
88
|
+
# Analyze a directory
|
|
89
|
+
oop-analyzer path/to/project/
|
|
90
|
+
|
|
91
|
+
# Analyze a module
|
|
92
|
+
oop-analyzer path/to/module/
|
|
93
|
+
|
|
94
|
+
# Specify output format (json, xml, html)
|
|
95
|
+
oop-analyzer path/to/file.py -f html -o report.html
|
|
96
|
+
|
|
97
|
+
# Enable only specific rules
|
|
98
|
+
oop-analyzer path/to/file.py --rules encapsulation coupling
|
|
99
|
+
|
|
100
|
+
# Disable specific rules
|
|
101
|
+
oop-analyzer path/to/file.py --disable-rules functions_to_objects
|
|
102
|
+
|
|
103
|
+
# List available rules
|
|
104
|
+
oop-analyzer --list-rules
|
|
105
|
+
|
|
106
|
+
# Generate default config file
|
|
107
|
+
oop-analyzer --init-config oop-analyzer.json
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Python API
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from oop_analyzer import OOPAnalyzer, AnalyzerConfig
|
|
114
|
+
|
|
115
|
+
# Default configuration (all rules enabled)
|
|
116
|
+
analyzer = OOPAnalyzer()
|
|
117
|
+
|
|
118
|
+
# Analyze source code
|
|
119
|
+
report = analyzer.analyze_source('''
|
|
120
|
+
def process(user):
|
|
121
|
+
print(user.name) # Encapsulation violation
|
|
122
|
+
''')
|
|
123
|
+
|
|
124
|
+
# Analyze a file
|
|
125
|
+
report = analyzer.analyze_file("path/to/file.py")
|
|
126
|
+
|
|
127
|
+
# Analyze a directory or module
|
|
128
|
+
report = analyzer.analyze("path/to/project/")
|
|
129
|
+
|
|
130
|
+
# Get formatted output
|
|
131
|
+
json_output = analyzer.format_report(report, "json")
|
|
132
|
+
html_output = analyzer.format_report(report, "html")
|
|
133
|
+
xml_output = analyzer.format_report(report, "xml")
|
|
134
|
+
|
|
135
|
+
# Custom configuration
|
|
136
|
+
config = AnalyzerConfig()
|
|
137
|
+
config.enable_only("encapsulation", "null_object")
|
|
138
|
+
config.output_format = "html"
|
|
139
|
+
|
|
140
|
+
analyzer = OOPAnalyzer(config)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Configuration
|
|
144
|
+
|
|
145
|
+
Create a `oop-analyzer.json` file:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"rules": {
|
|
150
|
+
"encapsulation": {
|
|
151
|
+
"enabled": true,
|
|
152
|
+
"severity": "warning",
|
|
153
|
+
"options": {
|
|
154
|
+
"allow_self_access": true,
|
|
155
|
+
"max_chain_length": 1
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
"coupling": {
|
|
159
|
+
"enabled": true,
|
|
160
|
+
"options": {
|
|
161
|
+
"max_imports_warning": 10
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"null_object": true,
|
|
165
|
+
"polymorphism": {
|
|
166
|
+
"enabled": true,
|
|
167
|
+
"options": {
|
|
168
|
+
"min_branches": 3
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
"functions_to_objects": {
|
|
172
|
+
"enabled": true,
|
|
173
|
+
"options": {
|
|
174
|
+
"max_params": 4
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
"output_format": "json",
|
|
179
|
+
"exclude_patterns": ["**/test_*.py", "**/*_test.py", "**/tests/**"]
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Rules
|
|
184
|
+
|
|
185
|
+
### Encapsulation (Tell Don't Ask)
|
|
186
|
+
|
|
187
|
+
Detects direct property access on objects. In OOP, we should "tell" objects what to do, not "ask" them for data.
|
|
188
|
+
|
|
189
|
+
**Bad:**
|
|
190
|
+
```python
|
|
191
|
+
if user.age > 18:
|
|
192
|
+
print(user.name)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Good:**
|
|
196
|
+
```python
|
|
197
|
+
if user.is_adult():
|
|
198
|
+
user.greet()
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Better:**
|
|
202
|
+
```python
|
|
203
|
+
adult_user.greet()
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Coupling
|
|
207
|
+
|
|
208
|
+
Measures module coupling through import analysis. Shows dependency graphs and identifies highly-coupled modules where abstractions might be missing.
|
|
209
|
+
|
|
210
|
+
### Null Object
|
|
211
|
+
|
|
212
|
+
Detects None usage patterns that could be replaced by the Null Object pattern:
|
|
213
|
+
- `if x is None` checks
|
|
214
|
+
- `return None` statements
|
|
215
|
+
- Parameters with `None` defaults
|
|
216
|
+
|
|
217
|
+
### Polymorphism
|
|
218
|
+
|
|
219
|
+
Finds if/elif chains and type checks that could be replaced by polymorphism:
|
|
220
|
+
- Long if/elif chains checking the same variable
|
|
221
|
+
- `isinstance()` checks
|
|
222
|
+
- Type/kind attribute comparisons
|
|
223
|
+
|
|
224
|
+
### Functions to Objects
|
|
225
|
+
|
|
226
|
+
Identifies functions that might be better as objects:
|
|
227
|
+
- Functions with many parameters
|
|
228
|
+
- Functions returning dictionaries
|
|
229
|
+
- Groups of related functions with common prefixes
|
|
230
|
+
|
|
231
|
+
### Type Code (NEW)
|
|
232
|
+
|
|
233
|
+
Detects type code conditionals that should be replaced with polymorphism:
|
|
234
|
+
|
|
235
|
+
**Bad:**
|
|
236
|
+
```python
|
|
237
|
+
class Bird:
|
|
238
|
+
def getSpeed(self):
|
|
239
|
+
if self.type == EUROPEAN:
|
|
240
|
+
return self.getBaseSpeed()
|
|
241
|
+
elif self.type == AFRICAN:
|
|
242
|
+
return self.getBaseSpeed() - self.getLoadFactor()
|
|
243
|
+
elif self.type == NORWEGIAN_BLUE:
|
|
244
|
+
return 0 if self.isNailed else self.getBaseSpeed(self.voltage)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Good:** Use State/Strategy pattern or subclasses:
|
|
248
|
+
```python
|
|
249
|
+
class Bird(ABC):
|
|
250
|
+
@abstractmethod
|
|
251
|
+
def getSpeed(self) -> float: pass
|
|
252
|
+
|
|
253
|
+
class EuropeanBird(Bird):
|
|
254
|
+
def getSpeed(self) -> float:
|
|
255
|
+
return self.getBaseSpeed()
|
|
256
|
+
|
|
257
|
+
class AfricanBird(Bird):
|
|
258
|
+
def getSpeed(self) -> float:
|
|
259
|
+
return self.getBaseSpeed() - self.getLoadFactor()
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
References:
|
|
263
|
+
- https://refactoring.guru/replace-type-code-with-state-strategy
|
|
264
|
+
- https://refactoring.guru/replace-type-code-with-subclasses
|
|
265
|
+
|
|
266
|
+
### Reference Exposure (NEW)
|
|
267
|
+
|
|
268
|
+
Detects methods that return references to internal mutable state, breaking encapsulation:
|
|
269
|
+
|
|
270
|
+
**Bad:**
|
|
271
|
+
```python
|
|
272
|
+
class Container:
|
|
273
|
+
def __init__(self):
|
|
274
|
+
self._items = []
|
|
275
|
+
|
|
276
|
+
def get_items(self):
|
|
277
|
+
return self._items # External code can modify internal state!
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Good:** Return a copy or immutable view:
|
|
281
|
+
```python
|
|
282
|
+
class Container:
|
|
283
|
+
def __init__(self):
|
|
284
|
+
self._items = []
|
|
285
|
+
|
|
286
|
+
def get_items(self):
|
|
287
|
+
return list(self._items) # Return a copy
|
|
288
|
+
|
|
289
|
+
# Or return a tuple for immutability
|
|
290
|
+
def get_items_readonly(self):
|
|
291
|
+
return tuple(self._items)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Dictionary Usage (NEW)
|
|
295
|
+
|
|
296
|
+
Detects dictionary usage that should be replaced by proper objects (dataclasses, Pydantic models, etc.). Dictionaries are acceptable at API boundaries (parsing REST responses), but abstraction layers should use typed objects.
|
|
297
|
+
|
|
298
|
+
**Bad:**
|
|
299
|
+
```python
|
|
300
|
+
def get_user():
|
|
301
|
+
return {"name": "John", "age": 30, "email": "john@example.com"}
|
|
302
|
+
|
|
303
|
+
def process(user: dict):
|
|
304
|
+
print(user["name"]) # No type safety, easy to typo keys
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**Good:** Use dataclasses or Pydantic models:
|
|
308
|
+
```python
|
|
309
|
+
from dataclasses import dataclass
|
|
310
|
+
|
|
311
|
+
@dataclass
|
|
312
|
+
class User:
|
|
313
|
+
name: str
|
|
314
|
+
age: int
|
|
315
|
+
email: str
|
|
316
|
+
|
|
317
|
+
def get_user() -> User:
|
|
318
|
+
return User(name="John", age=30, email="john@example.com")
|
|
319
|
+
|
|
320
|
+
def process(user: User):
|
|
321
|
+
print(user.name) # Type-safe, IDE autocomplete
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Acceptable** (API boundary):
|
|
325
|
+
```python
|
|
326
|
+
def parse_api_response(response: dict) -> User:
|
|
327
|
+
# Converting from dict at the boundary is fine
|
|
328
|
+
return User(**response)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Extending with New Rules
|
|
332
|
+
|
|
333
|
+
Create a new rule by inheriting from `BaseRule`:
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
from oop_analyzer.rules.base import BaseRule, RuleResult, RuleViolation
|
|
337
|
+
|
|
338
|
+
class MyCustomRule(BaseRule):
|
|
339
|
+
name = "my_rule"
|
|
340
|
+
description = "My custom OOP rule"
|
|
341
|
+
|
|
342
|
+
def analyze(self, tree, source, file_path):
|
|
343
|
+
violations = []
|
|
344
|
+
# Analyze the AST tree
|
|
345
|
+
# Add violations as needed
|
|
346
|
+
return RuleResult(
|
|
347
|
+
rule_name=self.name,
|
|
348
|
+
violations=violations,
|
|
349
|
+
)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Register in `oop_analyzer/rules/__init__.py`:
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
RULE_REGISTRY["my_rule"] = MyCustomRule
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Safety
|
|
359
|
+
|
|
360
|
+
The analyzer is designed to be safe:
|
|
361
|
+
- **No code execution**: Only AST parsing, never `exec()` or `eval()`
|
|
362
|
+
- **File validation**: Checks file existence, type, and size limits
|
|
363
|
+
- **Syntax validation**: Gracefully handles malformed code
|
|
364
|
+
|
|
365
|
+
## Running Tests
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
# Install dev dependencies
|
|
369
|
+
pip install -e ".[dev]"
|
|
370
|
+
|
|
371
|
+
# Run all tests
|
|
372
|
+
pytest
|
|
373
|
+
|
|
374
|
+
# Run with coverage
|
|
375
|
+
pytest --cov=oop_analyzer
|
|
376
|
+
|
|
377
|
+
# Run specific test file
|
|
378
|
+
pytest tests/test_rules/test_encapsulation.py
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## License
|
|
382
|
+
|
|
383
|
+
MIT
|