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.
Files changed (58) hide show
  1. python_oop_analyzer-0.1.0/.github/workflows/ci.yml +62 -0
  2. python_oop_analyzer-0.1.0/.github/workflows/publish.yml +65 -0
  3. python_oop_analyzer-0.1.0/.gitignore +86 -0
  4. python_oop_analyzer-0.1.0/.pre-commit-config.yaml +24 -0
  5. python_oop_analyzer-0.1.0/CHANGELOG.md +34 -0
  6. python_oop_analyzer-0.1.0/LICENSE +21 -0
  7. python_oop_analyzer-0.1.0/PKG-INFO +383 -0
  8. python_oop_analyzer-0.1.0/README.md +348 -0
  9. python_oop_analyzer-0.1.0/examples/README.md +127 -0
  10. python_oop_analyzer-0.1.0/examples/boolean_flag_example.py +253 -0
  11. python_oop_analyzer-0.1.0/examples/coupling_example.py +174 -0
  12. python_oop_analyzer-0.1.0/examples/dictionary_usage_example.py +258 -0
  13. python_oop_analyzer-0.1.0/examples/encapsulation_example.py +136 -0
  14. python_oop_analyzer-0.1.0/examples/functions_to_objects_example.py +226 -0
  15. python_oop_analyzer-0.1.0/examples/null_object_example.py +182 -0
  16. python_oop_analyzer-0.1.0/examples/polymorphism_example.py +215 -0
  17. python_oop_analyzer-0.1.0/examples/reference_exposure_example.py +242 -0
  18. python_oop_analyzer-0.1.0/examples/type_code_example.py +258 -0
  19. python_oop_analyzer-0.1.0/oop_analyzer/__init__.py +12 -0
  20. python_oop_analyzer-0.1.0/oop_analyzer/analyzer.py +373 -0
  21. python_oop_analyzer-0.1.0/oop_analyzer/cli.py +160 -0
  22. python_oop_analyzer-0.1.0/oop_analyzer/config.py +155 -0
  23. python_oop_analyzer-0.1.0/oop_analyzer/formatters/__init__.py +31 -0
  24. python_oop_analyzer-0.1.0/oop_analyzer/formatters/base.py +101 -0
  25. python_oop_analyzer-0.1.0/oop_analyzer/formatters/html_formatter.py +222 -0
  26. python_oop_analyzer-0.1.0/oop_analyzer/formatters/json_formatter.py +37 -0
  27. python_oop_analyzer-0.1.0/oop_analyzer/formatters/xml_formatter.py +113 -0
  28. python_oop_analyzer-0.1.0/oop_analyzer/py.typed +0 -0
  29. python_oop_analyzer-0.1.0/oop_analyzer/rules/__init__.py +56 -0
  30. python_oop_analyzer-0.1.0/oop_analyzer/rules/base.py +186 -0
  31. python_oop_analyzer-0.1.0/oop_analyzer/rules/boolean_flag.py +391 -0
  32. python_oop_analyzer-0.1.0/oop_analyzer/rules/coupling.py +616 -0
  33. python_oop_analyzer-0.1.0/oop_analyzer/rules/dictionary_usage.py +526 -0
  34. python_oop_analyzer-0.1.0/oop_analyzer/rules/encapsulation.py +291 -0
  35. python_oop_analyzer-0.1.0/oop_analyzer/rules/functions_to_objects.py +331 -0
  36. python_oop_analyzer-0.1.0/oop_analyzer/rules/null_object.py +472 -0
  37. python_oop_analyzer-0.1.0/oop_analyzer/rules/polymorphism.py +428 -0
  38. python_oop_analyzer-0.1.0/oop_analyzer/rules/reference_exposure.py +348 -0
  39. python_oop_analyzer-0.1.0/oop_analyzer/rules/type_code.py +450 -0
  40. python_oop_analyzer-0.1.0/oop_analyzer/safety.py +163 -0
  41. python_oop_analyzer-0.1.0/pyproject.toml +130 -0
  42. python_oop_analyzer-0.1.0/tests/__init__.py +1 -0
  43. python_oop_analyzer-0.1.0/tests/conftest.py +275 -0
  44. python_oop_analyzer-0.1.0/tests/test_analyzer.py +336 -0
  45. python_oop_analyzer-0.1.0/tests/test_config.py +176 -0
  46. python_oop_analyzer-0.1.0/tests/test_formatters.py +193 -0
  47. python_oop_analyzer-0.1.0/tests/test_rules/__init__.py +1 -0
  48. python_oop_analyzer-0.1.0/tests/test_rules/test_boolean_flag.py +219 -0
  49. python_oop_analyzer-0.1.0/tests/test_rules/test_coupling.py +208 -0
  50. python_oop_analyzer-0.1.0/tests/test_rules/test_dictionary_usage.py +283 -0
  51. python_oop_analyzer-0.1.0/tests/test_rules/test_encapsulation.py +167 -0
  52. python_oop_analyzer-0.1.0/tests/test_rules/test_functions_to_objects.py +240 -0
  53. python_oop_analyzer-0.1.0/tests/test_rules/test_null_object.py +205 -0
  54. python_oop_analyzer-0.1.0/tests/test_rules/test_polymorphism.py +244 -0
  55. python_oop_analyzer-0.1.0/tests/test_rules/test_reference_exposure.py +231 -0
  56. python_oop_analyzer-0.1.0/tests/test_rules/test_type_code.py +216 -0
  57. python_oop_analyzer-0.1.0/tests/test_safety.py +166 -0
  58. 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