modwire 2.1.0__tar.gz → 2.3.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.
- modwire-2.3.0/.github/workflows/large-repo-fixtures.yml +88 -0
- {modwire-2.1.0 → modwire-2.3.0}/PKG-INFO +37 -1
- {modwire-2.1.0 → modwire-2.3.0}/README.md +36 -0
- {modwire-2.1.0 → modwire-2.3.0}/pyproject.toml +3 -2
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/_version.py +3 -3
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/architecture/__init__.py +2 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/architecture/analyzers.py +20 -13
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/architecture/config.py +44 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/architecture/matching.py +44 -8
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/architecture/policy.py +8 -2
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/extraction/manifest.py +5 -4
- modwire-2.3.0/src/modwire/extractors/base.py +781 -0
- modwire-2.3.0/src/modwire/extractors/php.py +348 -0
- modwire-2.3.0/src/modwire/extractors/python.py +472 -0
- modwire-2.3.0/src/modwire/extractors/resources.py +29 -0
- modwire-2.3.0/src/modwire/extractors/scripts/__init__.py +2 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/extractors/scripts/php_extractor.php +25 -1
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/extractors/scripts/typescript_extractor.js +29 -3
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/extractors/typescript.py +27 -6
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/metadata.py +16 -20
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire.egg-info/PKG-INFO +37 -1
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire.egg-info/SOURCES.txt +9 -1
- modwire-2.3.0/src/modwire.egg-info/scm_file_list.json +85 -0
- modwire-2.3.0/src/modwire.egg-info/scm_version.json +8 -0
- modwire-2.3.0/tests/large_projects/README.md +47 -0
- modwire-2.3.0/tests/large_projects/fixtures.json +242 -0
- modwire-2.3.0/tests/large_projects/run_large_project_fixtures.py +534 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/test_api.py +204 -34
- {modwire-2.1.0 → modwire-2.3.0}/tests/test_architecture_api.py +95 -0
- modwire-2.1.0/src/modwire/extractors/base.py +0 -356
- modwire-2.1.0/src/modwire/extractors/php.py +0 -199
- modwire-2.1.0/src/modwire/extractors/python.py +0 -150
- {modwire-2.1.0 → modwire-2.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/.github/workflows/ci.yml +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/.github/workflows/release.yml +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/.gitignore +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/CONTRIBUTING.md +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/LICENSE +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/docs/wiki/Development-checks.md +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/docs/wiki/Home.md +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/docs/wiki/Reporting-bugs.md +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/docs/wiki/Requesting-features.md +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/setup.cfg +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/show_test_source_files.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/__init__.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/architecture/insights.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/architecture/render.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/architecture/violations.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/callables.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/definitions.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/exports.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/extraction/__init__.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/extraction/cache.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/extraction/models.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/extraction/roots.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/extraction/serialization.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/extraction/service.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/extractors/__init__.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/extractors/loader.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/extractors/scripts/python_extractor.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/graph.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/shape/__init__.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/shape/config.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/shape/evaluator.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/shape/rules.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/shape/violations.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/testing/__init__.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire/testing/factories.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire.egg-info/dependency_links.txt +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire.egg-info/requires.txt +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/src/modwire.egg-info/top_level.txt +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/php/ignored/generated.php +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/php/src/application/use_cases/activate.php +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/php/src/domain/model/user.php +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/php/src/domain/services/policy.php +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/php/src/interfaces/http/controller.php +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/python/ignored/generated.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/python/src/application/use_cases/activate.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/python/src/domain/model/user.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/python/src/domain/services/policy.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/python/src/interfaces/http/controller.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/typescript/ignored/generated.ts +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/typescript/src/application/use_cases/activate.ts +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/typescript/src/domain/model/profile.tsx +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/typescript/src/domain/model/user.ts +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/typescript/src/domain/services/audit.js +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/typescript/src/domain/services/policy.ts +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/typescript/src/interfaces/http/controller.ts +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/apps/typescript/src/interfaces/http/view.jsx +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/tests/test_standalone.py +0 -0
- {modwire-2.1.0 → modwire-2.3.0}/uv.lock +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
name: Large Repository Fixtures
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
fixture:
|
|
7
|
+
description: "Fixture id to run, or all."
|
|
8
|
+
required: true
|
|
9
|
+
default: "all"
|
|
10
|
+
type: choice
|
|
11
|
+
options:
|
|
12
|
+
- all
|
|
13
|
+
- django
|
|
14
|
+
- ansible
|
|
15
|
+
- salt
|
|
16
|
+
- vscode
|
|
17
|
+
- angular
|
|
18
|
+
- typescript
|
|
19
|
+
- wordpress
|
|
20
|
+
- drupal
|
|
21
|
+
- moodle
|
|
22
|
+
mode:
|
|
23
|
+
description: "Fixture execution mode."
|
|
24
|
+
required: true
|
|
25
|
+
default: "enclosure"
|
|
26
|
+
type: choice
|
|
27
|
+
options:
|
|
28
|
+
- enclosure
|
|
29
|
+
- modwire
|
|
30
|
+
- both
|
|
31
|
+
command-timeout:
|
|
32
|
+
description: "Seconds allowed for each enclosure command."
|
|
33
|
+
required: true
|
|
34
|
+
default: "60"
|
|
35
|
+
schedule:
|
|
36
|
+
- cron: "17 3 * * 0"
|
|
37
|
+
|
|
38
|
+
jobs:
|
|
39
|
+
large-fixture:
|
|
40
|
+
name: ${{ matrix.fixture }}
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
timeout-minutes: 60
|
|
43
|
+
if: github.event_name != 'workflow_dispatch' || inputs.fixture == 'all' || inputs.fixture == matrix.fixture
|
|
44
|
+
strategy:
|
|
45
|
+
fail-fast: false
|
|
46
|
+
matrix:
|
|
47
|
+
fixture:
|
|
48
|
+
- django
|
|
49
|
+
- ansible
|
|
50
|
+
- salt
|
|
51
|
+
- vscode
|
|
52
|
+
- angular
|
|
53
|
+
- typescript
|
|
54
|
+
- wordpress
|
|
55
|
+
- drupal
|
|
56
|
+
- moodle
|
|
57
|
+
|
|
58
|
+
steps:
|
|
59
|
+
- name: Check out repository
|
|
60
|
+
uses: actions/checkout@v4
|
|
61
|
+
|
|
62
|
+
- name: Set up Python
|
|
63
|
+
uses: actions/setup-python@v5
|
|
64
|
+
with:
|
|
65
|
+
python-version: "3.13"
|
|
66
|
+
|
|
67
|
+
- name: Set up Node.js
|
|
68
|
+
uses: actions/setup-node@v4
|
|
69
|
+
with:
|
|
70
|
+
node-version: "20"
|
|
71
|
+
|
|
72
|
+
- name: Set up PHP
|
|
73
|
+
uses: shivammathur/setup-php@v2
|
|
74
|
+
with:
|
|
75
|
+
php-version: "8.3"
|
|
76
|
+
|
|
77
|
+
- name: Install enclosure and local modwire
|
|
78
|
+
run: |
|
|
79
|
+
python -m pip install --upgrade pip
|
|
80
|
+
python -m pip install "enclosure @ git+https://github.com/9orky/enclosure.git"
|
|
81
|
+
python -m pip install -e .
|
|
82
|
+
|
|
83
|
+
- name: Run large fixture
|
|
84
|
+
run: >
|
|
85
|
+
python tests/large_projects/run_large_project_fixtures.py
|
|
86
|
+
--fixture "${{ matrix.fixture }}"
|
|
87
|
+
--mode "${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'enclosure' }}"
|
|
88
|
+
--command-timeout "${{ github.event_name == 'workflow_dispatch' && inputs['command-timeout'] || '60' }}"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modwire
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Extract source-code dependencies and build dependency graphs.
|
|
5
5
|
Author: Tomasz Szpak
|
|
6
6
|
License-Expression: MIT
|
|
@@ -136,6 +136,7 @@ from modwire import extract_code
|
|
|
136
136
|
from modwire.architecture import (
|
|
137
137
|
ArchitectureBoundaryRule,
|
|
138
138
|
ArchitectureConfig,
|
|
139
|
+
ArchitectureFlowRealm,
|
|
139
140
|
ArchitectureFlowRules,
|
|
140
141
|
ArchitecturePolicyEvaluator,
|
|
141
142
|
ArchitectureRules,
|
|
@@ -176,6 +177,41 @@ violations = ArchitecturePolicyEvaluator().evaluate(code_map.graph, config)
|
|
|
176
177
|
print(render_violations(tuple(violations)))
|
|
177
178
|
```
|
|
178
179
|
|
|
180
|
+
For repositories with more than one module ladder, configure flow realms and run
|
|
181
|
+
the same analyzers across each module tag:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
config = ArchitectureConfig(
|
|
185
|
+
language="python",
|
|
186
|
+
architecture_root="src",
|
|
187
|
+
rules=ArchitectureRules(
|
|
188
|
+
tags=(
|
|
189
|
+
ArchitectureTagRule(name="backend_module", match="features/*"),
|
|
190
|
+
ArchitectureTagRule(name="gui_page", match="app/pages/*"),
|
|
191
|
+
ArchitectureTagRule(name="domain", match="features/*/domain"),
|
|
192
|
+
ArchitectureTagRule(name="application", match="features/*/application"),
|
|
193
|
+
),
|
|
194
|
+
flow=ArchitectureFlowRules(
|
|
195
|
+
realms=(
|
|
196
|
+
ArchitectureFlowRealm(
|
|
197
|
+
name="backend",
|
|
198
|
+
module_tag="backend_module",
|
|
199
|
+
layers=("domain", "application"),
|
|
200
|
+
),
|
|
201
|
+
ArchitectureFlowRealm(
|
|
202
|
+
name="gui",
|
|
203
|
+
module_tag="gui_page",
|
|
204
|
+
),
|
|
205
|
+
),
|
|
206
|
+
analyzers=("backward-flow", "no-cycles"),
|
|
207
|
+
),
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`backward-flow` evaluates realms with layers and skips layerless realms; scoped
|
|
213
|
+
analyzers such as `no-cycles` evaluate every configured realm.
|
|
214
|
+
|
|
179
215
|
Architecture insight helpers summarize ownership and graph pressure:
|
|
180
216
|
|
|
181
217
|
```python
|
|
@@ -103,6 +103,7 @@ from modwire import extract_code
|
|
|
103
103
|
from modwire.architecture import (
|
|
104
104
|
ArchitectureBoundaryRule,
|
|
105
105
|
ArchitectureConfig,
|
|
106
|
+
ArchitectureFlowRealm,
|
|
106
107
|
ArchitectureFlowRules,
|
|
107
108
|
ArchitecturePolicyEvaluator,
|
|
108
109
|
ArchitectureRules,
|
|
@@ -143,6 +144,41 @@ violations = ArchitecturePolicyEvaluator().evaluate(code_map.graph, config)
|
|
|
143
144
|
print(render_violations(tuple(violations)))
|
|
144
145
|
```
|
|
145
146
|
|
|
147
|
+
For repositories with more than one module ladder, configure flow realms and run
|
|
148
|
+
the same analyzers across each module tag:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
config = ArchitectureConfig(
|
|
152
|
+
language="python",
|
|
153
|
+
architecture_root="src",
|
|
154
|
+
rules=ArchitectureRules(
|
|
155
|
+
tags=(
|
|
156
|
+
ArchitectureTagRule(name="backend_module", match="features/*"),
|
|
157
|
+
ArchitectureTagRule(name="gui_page", match="app/pages/*"),
|
|
158
|
+
ArchitectureTagRule(name="domain", match="features/*/domain"),
|
|
159
|
+
ArchitectureTagRule(name="application", match="features/*/application"),
|
|
160
|
+
),
|
|
161
|
+
flow=ArchitectureFlowRules(
|
|
162
|
+
realms=(
|
|
163
|
+
ArchitectureFlowRealm(
|
|
164
|
+
name="backend",
|
|
165
|
+
module_tag="backend_module",
|
|
166
|
+
layers=("domain", "application"),
|
|
167
|
+
),
|
|
168
|
+
ArchitectureFlowRealm(
|
|
169
|
+
name="gui",
|
|
170
|
+
module_tag="gui_page",
|
|
171
|
+
),
|
|
172
|
+
),
|
|
173
|
+
analyzers=("backward-flow", "no-cycles"),
|
|
174
|
+
),
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`backward-flow` evaluates realms with layers and skips layerless realms; scoped
|
|
180
|
+
analyzers such as `no-cycles` evaluate every configured realm.
|
|
181
|
+
|
|
146
182
|
Architecture insight helpers summarize ownership and graph pressure:
|
|
147
183
|
|
|
148
184
|
```python
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 3, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gdb0b34253'
|
|
@@ -4,6 +4,7 @@ from .config import (
|
|
|
4
4
|
ArchitectureConfig,
|
|
5
5
|
ArchitectureConfigError,
|
|
6
6
|
ArchitectureConfigIssue,
|
|
7
|
+
ArchitectureFlowRealm,
|
|
7
8
|
ArchitectureFlowRules,
|
|
8
9
|
ArchitectureRules,
|
|
9
10
|
ArchitectureTagRule,
|
|
@@ -32,6 +33,7 @@ __all__ = [
|
|
|
32
33
|
"ArchitectureConfig",
|
|
33
34
|
"ArchitectureConfigError",
|
|
34
35
|
"ArchitectureConfigIssue",
|
|
36
|
+
"ArchitectureFlowRealm",
|
|
35
37
|
"ArchitectureFlowRules",
|
|
36
38
|
"ArchitectureMap",
|
|
37
39
|
"ArchitecturePolicyEvaluator",
|
|
@@ -40,9 +40,9 @@ def analyzer_title(name: str) -> str:
|
|
|
40
40
|
return _ANALYZERS[name].title
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
def run_analyzer(name: str, graph, tags, config):
|
|
43
|
+
def run_analyzer(name: str, graph, tags, config, realm=None):
|
|
44
44
|
analyzer = _ANALYZERS[name]
|
|
45
|
-
return analyzer.run(analyzer.name, graph, tags, config)
|
|
45
|
+
return analyzer.run(analyzer.name, graph, tags, config, realm)
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def _roots(graph):
|
|
@@ -65,12 +65,18 @@ def _dedupe(violations):
|
|
|
65
65
|
return unique
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
def _flow(name, path, index, message):
|
|
69
|
-
|
|
68
|
+
def _flow(name, path, index, message, realm=None):
|
|
69
|
+
realm_name = "" if realm is None else getattr(realm, "name", "") or getattr(
|
|
70
|
+
realm,
|
|
71
|
+
"module_tag",
|
|
72
|
+
"",
|
|
73
|
+
)
|
|
74
|
+
rule_name = f"analyzer:{realm_name}:{name}" if realm_name else f"analyzer:{name}"
|
|
75
|
+
return FlowViolation(name, tuple(path), index, rule_name, message)
|
|
70
76
|
|
|
71
77
|
|
|
72
|
-
def _backward_flow(name, graph, tags, config):
|
|
73
|
-
layers = config.rules.flow.layers
|
|
78
|
+
def _backward_flow(name, graph, tags, config, realm):
|
|
79
|
+
layers = realm.layers if realm is not None else config.rules.flow.layers
|
|
74
80
|
|
|
75
81
|
def layer(node):
|
|
76
82
|
return next((i for i, layer_name in enumerate(layers) if layer_name in tags.get(node, set())), None)
|
|
@@ -85,7 +91,7 @@ def _backward_flow(name, graph, tags, config):
|
|
|
85
91
|
for edge in graph.outgoing(node):
|
|
86
92
|
next_layer = layer(edge.to_id)
|
|
87
93
|
if current_layer is not None and next_layer is not None and next_layer < current_layer:
|
|
88
|
-
violations.append(_flow(name, [*path, edge.to_id], len(path), "layer order violated"))
|
|
94
|
+
violations.append(_flow(name, [*path, edge.to_id], len(path), "layer order violated", realm))
|
|
89
95
|
continue
|
|
90
96
|
walk(edge.to_id, current_layer if next_layer is None else next_layer, [*path, edge.to_id])
|
|
91
97
|
|
|
@@ -94,8 +100,8 @@ def _backward_flow(name, graph, tags, config):
|
|
|
94
100
|
return _dedupe(violations)
|
|
95
101
|
|
|
96
102
|
|
|
97
|
-
def _no_reentry(name, graph, tags, config):
|
|
98
|
-
module_tag = config.rules.flow.module_tag
|
|
103
|
+
def _no_reentry(name, graph, tags, config, realm):
|
|
104
|
+
module_tag = realm.module_tag if realm is not None else config.rules.flow.module_tag
|
|
99
105
|
violations = []
|
|
100
106
|
seen = set()
|
|
101
107
|
|
|
@@ -107,7 +113,7 @@ def _no_reentry(name, graph, tags, config):
|
|
|
107
113
|
inside = module_tag in tags.get(edge.to_id, set())
|
|
108
114
|
next_state = 1 if state == 0 and inside else 2 if state == 1 and not inside else state
|
|
109
115
|
if state == 2 and inside:
|
|
110
|
-
violations.append(_flow(name, [*path, edge.to_id], len(path), "module layer re-entered after exit"))
|
|
116
|
+
violations.append(_flow(name, [*path, edge.to_id], len(path), "module layer re-entered after exit", realm))
|
|
111
117
|
continue
|
|
112
118
|
walk(edge.to_id, next_state, [*path, edge.to_id])
|
|
113
119
|
|
|
@@ -116,8 +122,9 @@ def _no_reentry(name, graph, tags, config):
|
|
|
116
122
|
return _dedupe(violations)
|
|
117
123
|
|
|
118
124
|
|
|
119
|
-
def _no_cycles(name, graph, tags, config):
|
|
120
|
-
|
|
125
|
+
def _no_cycles(name, graph, tags, config, realm):
|
|
126
|
+
module_tag = realm.module_tag if realm is not None else config.rules.flow.module_tag
|
|
127
|
+
scoped = {node for node, node_tags in tags.items() if module_tag in node_tags}
|
|
121
128
|
seen, stack, index, emitted, violations = set(), [], {}, set(), []
|
|
122
129
|
|
|
123
130
|
def canonical(cycle):
|
|
@@ -137,7 +144,7 @@ def _no_cycles(name, graph, tags, config):
|
|
|
137
144
|
key = canonical(cycle)
|
|
138
145
|
if key not in emitted:
|
|
139
146
|
emitted.add(key)
|
|
140
|
-
violations.append(_flow(name, cycle, len(cycle) - 1, "module cycle detected"))
|
|
147
|
+
violations.append(_flow(name, cycle, len(cycle) - 1, "module cycle detected", realm))
|
|
141
148
|
elif edge.to_id not in seen:
|
|
142
149
|
dfs(edge.to_id)
|
|
143
150
|
stack.pop()
|
|
@@ -31,11 +31,20 @@ class ArchitectureBoundaryRule(BaseModel):
|
|
|
31
31
|
allow_same_match: bool = False
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
class ArchitectureFlowRealm(BaseModel):
|
|
35
|
+
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
36
|
+
|
|
37
|
+
name: str = ""
|
|
38
|
+
module_tag: str = ""
|
|
39
|
+
layers: tuple[str, ...] = ()
|
|
40
|
+
|
|
41
|
+
|
|
34
42
|
class ArchitectureFlowRules(BaseModel):
|
|
35
43
|
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
36
44
|
|
|
37
45
|
layers: tuple[str, ...] = ()
|
|
38
46
|
module_tag: str = ""
|
|
47
|
+
realms: tuple[ArchitectureFlowRealm, ...] = ()
|
|
39
48
|
analyzers: tuple[str, ...] = ()
|
|
40
49
|
|
|
41
50
|
@field_validator("analyzers")
|
|
@@ -50,6 +59,28 @@ class ArchitectureFlowRules(BaseModel):
|
|
|
50
59
|
|
|
51
60
|
@model_validator(mode="after")
|
|
52
61
|
def analyzers_have_required_config(self) -> ArchitectureFlowRules:
|
|
62
|
+
if self.realms:
|
|
63
|
+
if "backward-flow" in self.analyzers and not any(
|
|
64
|
+
realm.layers for realm in self.realms
|
|
65
|
+
):
|
|
66
|
+
raise ValueError(
|
|
67
|
+
"backward-flow requires at least one rules.flow.realms entry "
|
|
68
|
+
"with layers"
|
|
69
|
+
)
|
|
70
|
+
scoped_analyzers = {"no-reentry", "no-cycles"}.intersection(
|
|
71
|
+
self.analyzers
|
|
72
|
+
)
|
|
73
|
+
missing_module_tag = tuple(
|
|
74
|
+
index for index, realm in enumerate(self.realms) if not realm.module_tag
|
|
75
|
+
)
|
|
76
|
+
if scoped_analyzers and missing_module_tag:
|
|
77
|
+
names = ", ".join(sorted(scoped_analyzers))
|
|
78
|
+
indexes = ", ".join(str(index) for index in missing_module_tag)
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"{names} require rules.flow.realms module_tag values "
|
|
81
|
+
f"(missing at indexes: {indexes})"
|
|
82
|
+
)
|
|
83
|
+
return self
|
|
53
84
|
if "backward-flow" in self.analyzers and not self.layers:
|
|
54
85
|
raise ValueError("backward-flow requires rules.flow.layers")
|
|
55
86
|
scoped_analyzers = {"no-reentry", "no-cycles"}.intersection(self.analyzers)
|
|
@@ -114,13 +145,26 @@ def validate_policy_config(config) -> ArchitectureConfig:
|
|
|
114
145
|
) from error
|
|
115
146
|
|
|
116
147
|
|
|
148
|
+
def flow_realms(flow: ArchitectureFlowRules) -> tuple[ArchitectureFlowRealm, ...]:
|
|
149
|
+
if flow.realms:
|
|
150
|
+
return flow.realms
|
|
151
|
+
return (
|
|
152
|
+
ArchitectureFlowRealm(
|
|
153
|
+
module_tag=flow.module_tag,
|
|
154
|
+
layers=flow.layers,
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
117
159
|
__all__ = [
|
|
118
160
|
"ArchitectureBoundaryRule",
|
|
119
161
|
"ArchitectureConfig",
|
|
120
162
|
"ArchitectureConfigError",
|
|
121
163
|
"ArchitectureConfigIssue",
|
|
164
|
+
"ArchitectureFlowRealm",
|
|
122
165
|
"ArchitectureFlowRules",
|
|
123
166
|
"ArchitectureRules",
|
|
124
167
|
"ArchitectureTagRule",
|
|
168
|
+
"flow_realms",
|
|
125
169
|
"validate_policy_config",
|
|
126
170
|
]
|
|
@@ -43,14 +43,31 @@ class TagMatcher:
|
|
|
43
43
|
self.tags = tuple(getattr(config.rules, "tags", ()))
|
|
44
44
|
|
|
45
45
|
def match(self, node_id: str, name: str, *, scope: bool = True) -> TagMatch | None:
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
matches = self.matches(node_id, name, scope=scope)
|
|
47
|
+
if matches:
|
|
48
|
+
return matches[0]
|
|
49
|
+
if self._has_tag(name):
|
|
50
|
+
return None
|
|
51
|
+
pattern = name
|
|
48
52
|
return self.match_pattern(
|
|
49
53
|
node_id,
|
|
50
54
|
pattern,
|
|
51
55
|
name=name,
|
|
52
56
|
scope=scope,
|
|
53
|
-
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def matches(
|
|
60
|
+
self,
|
|
61
|
+
node_id: str,
|
|
62
|
+
name: str,
|
|
63
|
+
*,
|
|
64
|
+
scope: bool = True,
|
|
65
|
+
) -> tuple[TagMatch, ...]:
|
|
66
|
+
return tuple(
|
|
67
|
+
match
|
|
68
|
+
for tag in self.tags
|
|
69
|
+
if tag.name == name
|
|
70
|
+
if (match := self._match_tag(node_id, tag, scope=scope)) is not None
|
|
54
71
|
)
|
|
55
72
|
|
|
56
73
|
def match_pattern(
|
|
@@ -80,9 +97,19 @@ class TagMatcher:
|
|
|
80
97
|
)
|
|
81
98
|
return None
|
|
82
99
|
|
|
83
|
-
def first_match(
|
|
100
|
+
def first_match(
|
|
101
|
+
self,
|
|
102
|
+
node_id: str,
|
|
103
|
+
names: tuple[str, ...],
|
|
104
|
+
*,
|
|
105
|
+
scope: bool = True,
|
|
106
|
+
) -> TagMatch | None:
|
|
84
107
|
return next(
|
|
85
|
-
(
|
|
108
|
+
(
|
|
109
|
+
tag_match
|
|
110
|
+
for name in names
|
|
111
|
+
if (tag_match := self.match(node_id, name, scope=scope)) is not None
|
|
112
|
+
),
|
|
86
113
|
None,
|
|
87
114
|
)
|
|
88
115
|
|
|
@@ -90,7 +117,7 @@ class TagMatcher:
|
|
|
90
117
|
return tuple(
|
|
91
118
|
match
|
|
92
119
|
for tag in self.tags
|
|
93
|
-
if (match := self.
|
|
120
|
+
if (match := self._match_tag(node_id, tag)) is not None
|
|
94
121
|
)
|
|
95
122
|
|
|
96
123
|
def map_code_map(self, code_map) -> TagMap:
|
|
@@ -111,8 +138,17 @@ class TagMatcher:
|
|
|
111
138
|
return path[len(architecture_root) + 1 :]
|
|
112
139
|
return path
|
|
113
140
|
|
|
114
|
-
def
|
|
115
|
-
return
|
|
141
|
+
def _match_tag(self, node_id: str, tag, *, scope: bool = True) -> TagMatch | None:
|
|
142
|
+
return self.match_pattern(
|
|
143
|
+
node_id,
|
|
144
|
+
tag.match,
|
|
145
|
+
name=tag.name,
|
|
146
|
+
scope=scope,
|
|
147
|
+
exclude=tag.excluded_patterns,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def _has_tag(self, name: str) -> bool:
|
|
151
|
+
return any(tag.name == name for tag in self.tags)
|
|
116
152
|
|
|
117
153
|
|
|
118
154
|
def _normalized_patterns(language, pattern, config):
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from modwire.graph import DependencyGraph
|
|
4
4
|
|
|
5
5
|
from .analyzers import run_analyzer
|
|
6
|
-
from .config import validate_policy_config
|
|
6
|
+
from .config import flow_realms, validate_policy_config
|
|
7
7
|
from .matching import TagMatcher
|
|
8
8
|
from .violations import EdgeRuleViolation, FlowViolation
|
|
9
9
|
|
|
@@ -14,7 +14,13 @@ class ArchitecturePolicyEvaluator:
|
|
|
14
14
|
tags = _tags(graph.node_ids(), config)
|
|
15
15
|
violations: list[EdgeRuleViolation | FlowViolation] = _edge_violations(graph, config)
|
|
16
16
|
for analyzer_name in config.rules.flow.analyzers:
|
|
17
|
-
|
|
17
|
+
for realm in flow_realms(config.rules.flow):
|
|
18
|
+
if analyzer_name == "backward-flow" and not realm.layers:
|
|
19
|
+
continue
|
|
20
|
+
analyzer_realm = realm if config.rules.flow.realms else None
|
|
21
|
+
violations.extend(
|
|
22
|
+
run_analyzer(analyzer_name, graph, tags, config, analyzer_realm)
|
|
23
|
+
)
|
|
18
24
|
return violations
|
|
19
25
|
|
|
20
26
|
|
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
from .._version import __version__
|
|
9
9
|
from ..extractors import load_extractor
|
|
10
10
|
from ..extractors.base import _collect_extraction_targets
|
|
11
|
+
from ..extractors.resources import extractor_script_path
|
|
11
12
|
from .models import SourceManifest, SourceManifestEntry
|
|
12
13
|
from .roots import (
|
|
13
14
|
DEFAULT_SOURCE_ROOTS,
|
|
@@ -57,14 +58,14 @@ def discover_sources(
|
|
|
57
58
|
mtime_ns=stat.st_mtime_ns,
|
|
58
59
|
)
|
|
59
60
|
)
|
|
60
|
-
extractor_path = (
|
|
61
|
-
Path(__file__).parents[1] / "extractors" / "scripts" / extractor.extractor_file
|
|
62
|
-
).resolve()
|
|
63
61
|
runtime_path = shutil.which(extractor.command) or ""
|
|
64
62
|
runtime_mtime_ns = 0
|
|
65
63
|
if runtime_path != "":
|
|
66
64
|
runtime_mtime_ns = Path(runtime_path).stat().st_mtime_ns
|
|
67
65
|
|
|
66
|
+
with extractor_script_path(extractor.extractor_file) as extractor_path:
|
|
67
|
+
extractor_mtime_ns = extractor_path.stat().st_mtime_ns
|
|
68
|
+
|
|
68
69
|
return SourceManifest(
|
|
69
70
|
language=language,
|
|
70
71
|
workspace_root=(
|
|
@@ -83,7 +84,7 @@ def discover_sources(
|
|
|
83
84
|
runtime_mtime_ns=runtime_mtime_ns,
|
|
84
85
|
extractor_file=extractor.extractor_file,
|
|
85
86
|
extractor_path=extractor_path,
|
|
86
|
-
extractor_mtime_ns=
|
|
87
|
+
extractor_mtime_ns=extractor_mtime_ns,
|
|
87
88
|
modwire_version=__version__,
|
|
88
89
|
files_found=files_found,
|
|
89
90
|
files_checked=len(entries),
|