pyossmtool 0.0.1__py3-none-any.whl
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.
- pyossmtool/__init__.py +3 -0
- pyossmtool/bundle/catalog/checks/bandit.scan.yaml +13 -0
- pyossmtool/bundle/catalog/checks/codespell.spelling.yaml +12 -0
- pyossmtool/bundle/catalog/checks/deadcode.scan.yaml +13 -0
- pyossmtool/bundle/catalog/checks/gate.acronym-allowlist.yaml +14 -0
- pyossmtool/bundle/catalog/checks/gate.folder-breadth.yaml +14 -0
- pyossmtool/bundle/catalog/checks/gate.module-private-vars.yaml +14 -0
- pyossmtool/bundle/catalog/checks/gate.module-size.yaml +14 -0
- pyossmtool/bundle/catalog/checks/gitleaks.secrets.yaml +21 -0
- pyossmtool/bundle/catalog/checks/hadolint.dockerfile.yaml +16 -0
- pyossmtool/bundle/catalog/checks/jscpd.duplication.yaml +19 -0
- pyossmtool/bundle/catalog/checks/markdownlint.check.yaml +17 -0
- pyossmtool/bundle/catalog/checks/mdformat.apply.yaml +10 -0
- pyossmtool/bundle/catalog/checks/mdformat.format.yaml +14 -0
- pyossmtool/bundle/catalog/checks/mutmut.run.yaml +13 -0
- pyossmtool/bundle/catalog/checks/pydeps.cycles.yaml +14 -0
- pyossmtool/bundle/catalog/checks/pytest.coverage.yaml +19 -0
- pyossmtool/bundle/catalog/checks/pytest.test.yaml +14 -0
- pyossmtool/bundle/catalog/checks/radon.cc.yaml +13 -0
- pyossmtool/bundle/catalog/checks/radon.mi.yaml +13 -0
- pyossmtool/bundle/catalog/checks/ruff.format.apply.yaml +11 -0
- pyossmtool/bundle/catalog/checks/ruff.format.yaml +14 -0
- pyossmtool/bundle/catalog/checks/ruff.lint.yaml +15 -0
- pyossmtool/bundle/catalog/checks/ruff.unused.yaml +17 -0
- pyossmtool/bundle/catalog/checks/semgrep.scan.yaml +18 -0
- pyossmtool/bundle/catalog/checks/shellcheck.yaml +13 -0
- pyossmtool/bundle/catalog/checks/shfmt.apply.yaml +11 -0
- pyossmtool/bundle/catalog/checks/shfmt.format.yaml +14 -0
- pyossmtool/bundle/catalog/checks/sourcery.review.yaml +14 -0
- pyossmtool/bundle/catalog/checks/ty.check.yaml +15 -0
- pyossmtool/bundle/catalog/checks/vulture.scan.yaml +16 -0
- pyossmtool/bundle/catalog/checks/yamlfmt.apply.yaml +10 -0
- pyossmtool/bundle/catalog/checks/yamlfmt.format.yaml +14 -0
- pyossmtool/bundle/catalog/checks/yamllint.check.yaml +16 -0
- pyossmtool/bundle/catalog/tools/bandit.yaml +20 -0
- pyossmtool/bundle/catalog/tools/codespell.yaml +17 -0
- pyossmtool/bundle/catalog/tools/deadcode.yaml +13 -0
- pyossmtool/bundle/catalog/tools/gitleaks.yaml +9 -0
- pyossmtool/bundle/catalog/tools/hadolint.yaml +11 -0
- pyossmtool/bundle/catalog/tools/jscpd.yaml +15 -0
- pyossmtool/bundle/catalog/tools/markdownlint.yaml +18 -0
- pyossmtool/bundle/catalog/tools/mdformat.yaml +13 -0
- pyossmtool/bundle/catalog/tools/mutmut.yaml +10 -0
- pyossmtool/bundle/catalog/tools/pydeps.yaml +10 -0
- pyossmtool/bundle/catalog/tools/pytest.yaml +12 -0
- pyossmtool/bundle/catalog/tools/radon.yaml +10 -0
- pyossmtool/bundle/catalog/tools/ruff.yaml +26 -0
- pyossmtool/bundle/catalog/tools/script.yaml +8 -0
- pyossmtool/bundle/catalog/tools/semgrep.yaml +21 -0
- pyossmtool/bundle/catalog/tools/shellcheck.yaml +10 -0
- pyossmtool/bundle/catalog/tools/shfmt.yaml +13 -0
- pyossmtool/bundle/catalog/tools/sourcery.yaml +10 -0
- pyossmtool/bundle/catalog/tools/ty.yaml +20 -0
- pyossmtool/bundle/catalog/tools/vulture.yaml +13 -0
- pyossmtool/bundle/catalog/tools/yamlfmt.yaml +24 -0
- pyossmtool/bundle/catalog/tools/yamllint.yaml +24 -0
- pyossmtool/bundle/defaults/allowlists/acronyms.yaml +10 -0
- pyossmtool/bundle/defaults/allowlists/folder-breadth.txt +1 -0
- pyossmtool/bundle/defaults/allowlists/module-private-vars.txt +1 -0
- pyossmtool/bundle/defaults/allowlists/module-size.txt +1 -0
- pyossmtool/bundle/defaults/configs/bandit.yaml +5 -0
- pyossmtool/bundle/defaults/configs/gates/acronym-allowlist.yaml +4 -0
- pyossmtool/bundle/defaults/configs/gates/folder-breadth.yaml +12 -0
- pyossmtool/bundle/defaults/configs/gates/module-private-vars.yaml +4 -0
- pyossmtool/bundle/defaults/configs/gates/module-size.yaml +6 -0
- pyossmtool/bundle/defaults/configs/markdownlint.json +8 -0
- pyossmtool/bundle/defaults/configs/ruff.toml +8 -0
- pyossmtool/bundle/defaults/configs/ty.toml +5 -0
- pyossmtool/bundle/defaults/configs/yamlfmt.yaml +4 -0
- pyossmtool/bundle/defaults/configs/yamllint.yaml +12 -0
- pyossmtool/bundle/defaults/gates/README.md +150 -0
- pyossmtool/bundle/defaults/gates/acronym-allowlist.sh +15 -0
- pyossmtool/bundle/defaults/gates/folder-breadth.sh +22 -0
- pyossmtool/bundle/defaults/gates/lib.sh +116 -0
- pyossmtool/bundle/defaults/gates/module-private-vars.sh +77 -0
- pyossmtool/bundle/defaults/gates/module-size.sh +60 -0
- pyossmtool/bundle/defaults/semgrep/python-quality.yml +19 -0
- pyossmtool/bundle/suites/demo.yaml +11 -0
- pyossmtool/bundle/suites/extended.yaml +34 -0
- pyossmtool/bundle/suites/format.yaml +10 -0
- pyossmtool/bundle/suites/formatting.yaml +10 -0
- pyossmtool/bundle/suites/policy.yaml +11 -0
- pyossmtool/bundle/suites/python-quality.yaml +9 -0
- pyossmtool/bundle/suites/reslab-parity.yaml +41 -0
- pyossmtool/bundle/suites/standard.yaml +45 -0
- pyossmtool/cli.py +233 -0
- pyossmtool/config_resolver.py +117 -0
- pyossmtool/discovery.py +50 -0
- pyossmtool/gate_config.py +119 -0
- pyossmtool/gates.py +102 -0
- pyossmtool/ignore.py +270 -0
- pyossmtool/ignore_materialize.py +184 -0
- pyossmtool/installer.py +119 -0
- pyossmtool/models.py +279 -0
- pyossmtool/parsers/__init__.py +76 -0
- pyossmtool/parsers/analysis.py +329 -0
- pyossmtool/parsers/base.py +29 -0
- pyossmtool/parsers/common.py +57 -0
- pyossmtool/parsers/format_text.py +67 -0
- pyossmtool/parsers/gates.py +92 -0
- pyossmtool/parsers/patterns.py +78 -0
- pyossmtool/parsers/prose.py +193 -0
- pyossmtool/parsers/ruff.py +103 -0
- pyossmtool/parsers/shell.py +83 -0
- pyossmtool/policy/__init__.py +0 -0
- pyossmtool/policy/acronym_allowlist.py +322 -0
- pyossmtool/policy/folder_breadth.py +306 -0
- pyossmtool/registry.py +72 -0
- pyossmtool/reporter.py +87 -0
- pyossmtool/resolver.py +146 -0
- pyossmtool/runner.py +482 -0
- pyossmtool/runner_script.py +131 -0
- pyossmtool/target_expand.py +112 -0
- pyossmtool-0.0.1.dist-info/METADATA +94 -0
- pyossmtool-0.0.1.dist-info/RECORD +119 -0
- pyossmtool-0.0.1.dist-info/WHEEL +5 -0
- pyossmtool-0.0.1.dist-info/entry_points.txt +2 -0
- pyossmtool-0.0.1.dist-info/licenses/LICENSE +21 -0
- pyossmtool-0.0.1.dist-info/top_level.txt +1 -0
pyossmtool/discovery.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Discover check targets from tool file globs under a scan root.
|
|
2
|
+
|
|
3
|
+
Empty ``ToolDef.files`` (and no check-level ``include``) means the tool accepts a
|
|
4
|
+
directory root — pass ``target`` through unchanged. Non-empty globs expand to
|
|
5
|
+
matching files under ``project_root / target``, filtered by the effective
|
|
6
|
+
gitignore matcher. An empty match set means the check should be skipped
|
|
7
|
+
(silent pass).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from pyossmtool.ignore import EffectiveIgnores
|
|
15
|
+
from pyossmtool.models import CheckDef, ToolDef
|
|
16
|
+
from pyossmtool.target_expand import expand_include_globs
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def effective_file_globs(check: CheckDef, tool: ToolDef) -> list[str]:
|
|
20
|
+
"""Check-level include narrows; otherwise use tool files."""
|
|
21
|
+
if check.include:
|
|
22
|
+
return list(check.include)
|
|
23
|
+
return list(tool.files)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def argv_targets_for_check(
|
|
27
|
+
*,
|
|
28
|
+
project_root: Path,
|
|
29
|
+
check: CheckDef,
|
|
30
|
+
tool: ToolDef,
|
|
31
|
+
target: str,
|
|
32
|
+
ignores: EffectiveIgnores | None = None,
|
|
33
|
+
) -> list[str] | None:
|
|
34
|
+
globs = effective_file_globs(check, tool)
|
|
35
|
+
if not globs:
|
|
36
|
+
return [target]
|
|
37
|
+
root = (project_root / target).resolve()
|
|
38
|
+
matches = expand_include_globs(root, globs)
|
|
39
|
+
if ignores is not None:
|
|
40
|
+
matches = [rel for rel in matches if not _is_ignored(target, rel, ignores)]
|
|
41
|
+
if not matches:
|
|
42
|
+
return None
|
|
43
|
+
return [str(Path(target) / rel) for rel in matches]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _is_ignored(target: str, rel: str, ignores: EffectiveIgnores) -> bool:
|
|
47
|
+
repo_relative = str(Path(target) / rel).replace("\\", "/")
|
|
48
|
+
if repo_relative.startswith("./"):
|
|
49
|
+
repo_relative = repo_relative[2:]
|
|
50
|
+
return ignores.is_ignored(repo_relative)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Load gate policy configuration (paths mode > project file > bundled)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from pyossmtool.models import CheckDef, ConfigMode, GateConfigSpec, ProjectConfig
|
|
11
|
+
from pyossmtool.registry import BUNDLE_ROOT
|
|
12
|
+
|
|
13
|
+
BUNDLED_CONFIGS = BUNDLE_ROOT / "defaults" / "configs"
|
|
14
|
+
BUNDLED_ALLOWLISTS = BUNDLE_ROOT / "defaults" / "allowlists"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def resolve_gate_config_path(
|
|
18
|
+
check: CheckDef,
|
|
19
|
+
project_root: Path,
|
|
20
|
+
project_config: ProjectConfig | None,
|
|
21
|
+
) -> Path | None:
|
|
22
|
+
override = _paths_mode_override(check.id, project_root, project_config)
|
|
23
|
+
if override is not None:
|
|
24
|
+
return override
|
|
25
|
+
|
|
26
|
+
spec = check.config
|
|
27
|
+
if spec is None:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
if spec.project_file:
|
|
31
|
+
project_path = project_root / spec.project_file
|
|
32
|
+
if project_path.exists():
|
|
33
|
+
return project_path.resolve()
|
|
34
|
+
|
|
35
|
+
return _bundled_config_path(spec)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_gate_config(
|
|
39
|
+
check: CheckDef,
|
|
40
|
+
project_root: Path,
|
|
41
|
+
project_config: ProjectConfig | None,
|
|
42
|
+
) -> tuple[Path | None, dict[str, Any]]:
|
|
43
|
+
path = resolve_gate_config_path(check, project_root, project_config)
|
|
44
|
+
if path is None or not path.exists():
|
|
45
|
+
return path, {}
|
|
46
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
47
|
+
if not isinstance(data, dict):
|
|
48
|
+
raise ValueError(f"Gate config must be a mapping: {path}")
|
|
49
|
+
config = dict(data)
|
|
50
|
+
_resolve_allowlist_path(check, project_root, config)
|
|
51
|
+
return path, config
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _paths_mode_override(check_id: str, project_root: Path, project_config: ProjectConfig | None) -> Path | None:
|
|
55
|
+
spec = project_config.configs if project_config else None
|
|
56
|
+
if not spec or spec.mode != ConfigMode.PATHS:
|
|
57
|
+
return None
|
|
58
|
+
override = spec.paths.get(check_id)
|
|
59
|
+
return (project_root / override).resolve() if override else None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _bundled_config_path(spec: GateConfigSpec) -> Path | None:
|
|
63
|
+
if not spec.bundled:
|
|
64
|
+
return None
|
|
65
|
+
path = BUNDLED_CONFIGS / spec.bundled
|
|
66
|
+
return path.resolve() if path.exists() else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _resolve_allowlist_path(check: CheckDef, project_root: Path, config: dict[str, Any]) -> None:
|
|
70
|
+
raw = config.get("allowlist_file")
|
|
71
|
+
if not raw:
|
|
72
|
+
return
|
|
73
|
+
path = _allowlist_path(project_root, raw)
|
|
74
|
+
if path.exists():
|
|
75
|
+
config["allowlist_file"] = str(path.resolve())
|
|
76
|
+
return
|
|
77
|
+
bundled = _bundled_allowlist(check)
|
|
78
|
+
config["allowlist_file"] = str(bundled.resolve() if bundled else path.resolve())
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _allowlist_path(project_root: Path, raw: Any) -> Path:
|
|
82
|
+
path = Path(str(raw))
|
|
83
|
+
if path.is_absolute():
|
|
84
|
+
return path
|
|
85
|
+
return project_root / path
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _bundled_allowlist(check: CheckDef) -> Path | None:
|
|
89
|
+
if check.config is None or not check.config.allowlist_bundled:
|
|
90
|
+
return None
|
|
91
|
+
bundled = BUNDLED_ALLOWLISTS / check.config.allowlist_bundled
|
|
92
|
+
return bundled if bundled.exists() else None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def gate_env_from_config(config: dict[str, Any], project_root: Path) -> dict[str, str]:
|
|
96
|
+
env: dict[str, str] = {}
|
|
97
|
+
for key, value in config.items():
|
|
98
|
+
if value is None:
|
|
99
|
+
continue
|
|
100
|
+
env[f"GATE_{key.upper()}"] = _gate_env_value(value)
|
|
101
|
+
allowlist = config.get("allowlist_file")
|
|
102
|
+
if allowlist:
|
|
103
|
+
env["GATE_ALLOWLIST_FILE"] = str(_allowlist_env_path(allowlist, project_root))
|
|
104
|
+
return env
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _allowlist_env_path(allowlist: Any, project_root: Path) -> Path:
|
|
108
|
+
path = Path(str(allowlist))
|
|
109
|
+
if not path.is_absolute():
|
|
110
|
+
path = project_root / path
|
|
111
|
+
return path.resolve()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _gate_env_value(value: Any) -> str:
|
|
115
|
+
if isinstance(value, list):
|
|
116
|
+
return " ".join(str(item) for item in value)
|
|
117
|
+
if isinstance(value, bool):
|
|
118
|
+
return "1" if value else "0"
|
|
119
|
+
return str(value)
|
pyossmtool/gates.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Script gate scaffolding and library path resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import stat
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from pyossmtool.registry import BUNDLE_ROOT
|
|
10
|
+
|
|
11
|
+
GATES_LIB = BUNDLE_ROOT / "defaults" / "gates" / "lib.sh"
|
|
12
|
+
PROJECT_GATES_DIR = ".pyossmtool/gates"
|
|
13
|
+
PROJECT_CHECKS_DIR = ".pyossmtool/catalog/checks"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def lib_path() -> Path:
|
|
17
|
+
return GATES_LIB
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def default_report_path(check_id: str) -> str:
|
|
21
|
+
return f".pyossmtool/reports/{check_id}.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def gate_check_id(name: str) -> str:
|
|
25
|
+
slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
|
|
26
|
+
return f"gate.{slug}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def gate_script_path(name: str) -> str:
|
|
30
|
+
slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
|
|
31
|
+
return f"{PROJECT_GATES_DIR}/{slug}.sh"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def gate_catalog_path(name: str) -> Path:
|
|
35
|
+
return Path(PROJECT_CHECKS_DIR) / f"{gate_check_id(name)}.yaml"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def render_gate_script(name: str, description: str) -> str:
|
|
39
|
+
slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
|
|
40
|
+
return f"""#!/usr/bin/env bash
|
|
41
|
+
# {description}
|
|
42
|
+
set -euo pipefail
|
|
43
|
+
|
|
44
|
+
# shellcheck source=/dev/null
|
|
45
|
+
source "$(pyossmtool gates lib-path)"
|
|
46
|
+
|
|
47
|
+
gate_init "{slug}"
|
|
48
|
+
|
|
49
|
+
# Example: fail when a scan root is missing
|
|
50
|
+
if [[ ! -d "${{PYOSSMTOOL_TARGET:-.}}" ]]; then
|
|
51
|
+
\tgate_fail "missing-target" "Scan target not found: ${{PYOSSMTOOL_TARGET:-.}}"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Add your policy checks here, e.g.:
|
|
55
|
+
# gate_fail "module-size" "src/foo.py exceeds line limit" "src/foo.py" "1"
|
|
56
|
+
|
|
57
|
+
gate_finish
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def render_gate_catalog(name: str, description: str) -> str:
|
|
62
|
+
check_id = gate_check_id(name)
|
|
63
|
+
script = gate_script_path(name)
|
|
64
|
+
report = default_report_path(check_id)
|
|
65
|
+
title = name.replace("-", " ").title()
|
|
66
|
+
slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
|
|
67
|
+
return f"""id: {check_id}
|
|
68
|
+
tool: script
|
|
69
|
+
name: {title}
|
|
70
|
+
description: {description}
|
|
71
|
+
script: {script}
|
|
72
|
+
parser: gate_json
|
|
73
|
+
output_file: {report}
|
|
74
|
+
argv: []
|
|
75
|
+
config:
|
|
76
|
+
bundled: gates/{slug}.yaml
|
|
77
|
+
project_file: .pyossmtool/configs/gates/{check_id}.yaml
|
|
78
|
+
success:
|
|
79
|
+
exit_codes: [0]
|
|
80
|
+
remediation:
|
|
81
|
+
suggested_commands:
|
|
82
|
+
- "./{script}"
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def scaffold_gate(project_root: Path, name: str, description: str) -> tuple[Path, Path]:
|
|
87
|
+
script_rel = gate_script_path(name)
|
|
88
|
+
script_path = project_root / script_rel
|
|
89
|
+
catalog_path = project_root / gate_catalog_path(name)
|
|
90
|
+
|
|
91
|
+
script_path.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
catalog_path.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
|
|
94
|
+
if script_path.exists():
|
|
95
|
+
raise FileExistsError(f"Gate script already exists: {script_path}")
|
|
96
|
+
if catalog_path.exists():
|
|
97
|
+
raise FileExistsError(f"Gate catalog entry already exists: {catalog_path}")
|
|
98
|
+
|
|
99
|
+
script_path.write_text(render_gate_script(name, description), encoding="utf-8")
|
|
100
|
+
script_path.chmod(script_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
101
|
+
catalog_path.write_text(render_gate_catalog(name, description), encoding="utf-8")
|
|
102
|
+
return script_path, catalog_path
|
pyossmtool/ignore.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Resolve and apply unified ignore profiles and paths for quality checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pathspec
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from pyossmtool.models import CheckDef, IgnoreSpec, ProjectConfig, SuiteCheckRef, SuiteDef, ToolDef
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ToolIgnoreMaterial:
|
|
16
|
+
"""Tool-specific ignore materialization output."""
|
|
17
|
+
|
|
18
|
+
argv: list[str] = field(default_factory=list)
|
|
19
|
+
config_path: Path | None = None
|
|
20
|
+
skip_file: Path | None = None
|
|
21
|
+
post_subcommand: bool = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class EffectiveIgnores:
|
|
26
|
+
"""Merged ignore patterns for a single check run."""
|
|
27
|
+
|
|
28
|
+
profile_files: list[str] = field(default_factory=list)
|
|
29
|
+
path_patterns: list[str] = field(default_factory=list)
|
|
30
|
+
_matcher: pathspec.PathSpec | None = field(default=None, repr=False)
|
|
31
|
+
|
|
32
|
+
def matcher(self) -> pathspec.PathSpec:
|
|
33
|
+
if self._matcher is None:
|
|
34
|
+
patterns = list(self.path_patterns)
|
|
35
|
+
for profile in self.profile_files:
|
|
36
|
+
patterns.extend(_load_profile_patterns(Path(profile)))
|
|
37
|
+
self._matcher = pathspec.PathSpec.from_lines("gitignore", patterns)
|
|
38
|
+
return self._matcher
|
|
39
|
+
|
|
40
|
+
def is_ignored(self, repo_relative_path: str) -> bool:
|
|
41
|
+
normalized = _normalize_repo_path(repo_relative_path)
|
|
42
|
+
if not normalized:
|
|
43
|
+
return False
|
|
44
|
+
return self.matcher().match_file(normalized)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def merge_ignore_specs(*specs: IgnoreSpec | None) -> IgnoreSpec:
|
|
48
|
+
profiles: list[str] = []
|
|
49
|
+
paths: list[str] = []
|
|
50
|
+
seen_profiles: set[str] = set()
|
|
51
|
+
seen_paths: set[str] = set()
|
|
52
|
+
for spec in specs:
|
|
53
|
+
if spec is None:
|
|
54
|
+
continue
|
|
55
|
+
_extend_unique(profiles, seen_profiles, spec.ignore_profile)
|
|
56
|
+
_extend_unique(paths, seen_paths, spec.ignore_paths)
|
|
57
|
+
return IgnoreSpec(ignore_profile=profiles, ignore_paths=paths)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _extend_unique(target: list[str], seen: set[str], values: list[str]) -> None:
|
|
61
|
+
for item in values:
|
|
62
|
+
if item in seen:
|
|
63
|
+
continue
|
|
64
|
+
seen.add(item)
|
|
65
|
+
target.append(item)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def resolve_effective_ignores(
|
|
69
|
+
project_root: Path,
|
|
70
|
+
*,
|
|
71
|
+
suite: SuiteDef | None = None,
|
|
72
|
+
project_config: ProjectConfig | None = None,
|
|
73
|
+
check_ref: SuiteCheckRef | None = None,
|
|
74
|
+
check: CheckDef | None = None,
|
|
75
|
+
bundled_patterns: list[str] | None = None,
|
|
76
|
+
) -> EffectiveIgnores:
|
|
77
|
+
merged = merge_ignore_specs(
|
|
78
|
+
IgnoreSpec(ignore_profile=[".gitignore"], ignore_paths=bundled_patterns or []),
|
|
79
|
+
_ignore_spec_from(suite),
|
|
80
|
+
_ignore_spec_from(project_config),
|
|
81
|
+
_ignore_spec_from(check),
|
|
82
|
+
_ignore_spec_from(check_ref),
|
|
83
|
+
)
|
|
84
|
+
return _effective_from_merged(merged, project_root)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _ignore_spec_from(source) -> IgnoreSpec:
|
|
88
|
+
if source is None:
|
|
89
|
+
return IgnoreSpec()
|
|
90
|
+
return IgnoreSpec(ignore_profile=source.ignore_profile, ignore_paths=source.ignore_paths)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _effective_from_merged(merged: IgnoreSpec, project_root: Path) -> EffectiveIgnores:
|
|
94
|
+
profile_files = [
|
|
95
|
+
str((project_root / rel).resolve()) for rel in merged.ignore_profile if (project_root / rel).resolve().is_file()
|
|
96
|
+
]
|
|
97
|
+
path_patterns = [_normalize_pattern(pattern) for pattern in merged.ignore_paths]
|
|
98
|
+
return EffectiveIgnores(profile_files=profile_files, path_patterns=path_patterns)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def filter_findings(findings: list, ignores: EffectiveIgnores) -> list:
|
|
102
|
+
|
|
103
|
+
return [finding for finding in findings if _keep_finding(finding, ignores)]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _keep_finding(finding, ignores: EffectiveIgnores) -> bool:
|
|
107
|
+
from pyossmtool.models import Finding
|
|
108
|
+
|
|
109
|
+
if not isinstance(finding, Finding):
|
|
110
|
+
return True
|
|
111
|
+
if not finding.location or not finding.location.file:
|
|
112
|
+
return True
|
|
113
|
+
return not ignores.is_ignored(finding.location.file)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def materialize_for_tool(
|
|
117
|
+
tool: ToolDef,
|
|
118
|
+
ignores: EffectiveIgnores,
|
|
119
|
+
*,
|
|
120
|
+
project_root: Path,
|
|
121
|
+
check_id: str,
|
|
122
|
+
base_config_path: Path | None = None,
|
|
123
|
+
) -> ToolIgnoreMaterial:
|
|
124
|
+
from pyossmtool.ignore_materialize import materialize_for_tool as _materialize
|
|
125
|
+
|
|
126
|
+
return _materialize(
|
|
127
|
+
tool,
|
|
128
|
+
ignores,
|
|
129
|
+
project_root=project_root,
|
|
130
|
+
check_id=check_id,
|
|
131
|
+
base_config_path=base_config_path,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def ignore_env(ignores: EffectiveIgnores, project_root: Path) -> dict[str, str]:
|
|
136
|
+
profiles = [str((project_root / rel).resolve()) for rel in _profile_rel_paths(ignores, project_root)]
|
|
137
|
+
env: dict[str, str] = {}
|
|
138
|
+
if profiles:
|
|
139
|
+
env["PYOSSMTOOL_IGNORE_PROFILES"] = "\n".join(profiles)
|
|
140
|
+
if ignores.path_patterns:
|
|
141
|
+
env["PYOSSMTOOL_IGNORE_PATHS"] = "\n".join(ignores.path_patterns)
|
|
142
|
+
return env
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _profile_rel_paths(ignores: EffectiveIgnores, project_root: Path) -> list[str]:
|
|
146
|
+
rels: list[str] = []
|
|
147
|
+
for path in ignores.profile_files:
|
|
148
|
+
try:
|
|
149
|
+
rels.append(str(Path(path).resolve().relative_to(project_root.resolve())))
|
|
150
|
+
except ValueError:
|
|
151
|
+
rels.append(path)
|
|
152
|
+
return rels
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _all_patterns(ignores: EffectiveIgnores) -> list[str]:
|
|
156
|
+
patterns = list(ignores.path_patterns)
|
|
157
|
+
for profile in ignores.profile_files:
|
|
158
|
+
patterns.extend(_load_profile_patterns(Path(profile)))
|
|
159
|
+
deduped: list[str] = []
|
|
160
|
+
seen: set[str] = set()
|
|
161
|
+
for pattern in patterns:
|
|
162
|
+
normalized = _normalize_pattern(pattern)
|
|
163
|
+
if normalized and normalized not in seen:
|
|
164
|
+
seen.add(normalized)
|
|
165
|
+
deduped.append(normalized)
|
|
166
|
+
return deduped
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _load_profile_patterns(path: Path) -> list[str]:
|
|
170
|
+
if not path.is_file():
|
|
171
|
+
return []
|
|
172
|
+
return [
|
|
173
|
+
pattern
|
|
174
|
+
for line in path.read_text(encoding="utf-8").splitlines()
|
|
175
|
+
if (pattern := _profile_line_pattern(line)) is not None
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _profile_line_pattern(line: str) -> str | None:
|
|
180
|
+
stripped = line.strip()
|
|
181
|
+
if not stripped or stripped.startswith("#") or stripped.startswith("!"):
|
|
182
|
+
return None
|
|
183
|
+
return stripped
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _normalize_pattern(pattern: str) -> str:
|
|
187
|
+
pattern = pattern.strip().replace("\\", "/")
|
|
188
|
+
if pattern.startswith("./"):
|
|
189
|
+
pattern = pattern[2:]
|
|
190
|
+
return pattern.rstrip("/") if not pattern.endswith("/") else pattern
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _normalize_repo_path(path: str) -> str:
|
|
194
|
+
normalized = path.strip().replace("\\", "/")
|
|
195
|
+
if normalized.startswith("./"):
|
|
196
|
+
normalized = normalized[2:]
|
|
197
|
+
return normalized
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _patterns_as_yaml_block(patterns: list[str]) -> str:
|
|
201
|
+
return "\n".join(patterns) + ("\n" if patterns else "")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _merge_yaml_config(base_path: Path | None, overlay: dict) -> dict:
|
|
205
|
+
merged = _load_yaml_dict(base_path)
|
|
206
|
+
for key, value in overlay.items():
|
|
207
|
+
merged[key] = _merge_yaml_value(merged, key, value)
|
|
208
|
+
return merged
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _load_yaml_dict(base_path: Path | None) -> dict:
|
|
212
|
+
if not base_path or not base_path.exists():
|
|
213
|
+
return {}
|
|
214
|
+
loaded = yaml.safe_load(base_path.read_text(encoding="utf-8")) or {}
|
|
215
|
+
return loaded if isinstance(loaded, dict) else {}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _merge_yaml_value(merged: dict, key: str, value) -> object:
|
|
219
|
+
if key == "ignore" and "ignore" in merged:
|
|
220
|
+
return _merge_ignore_block(merged["ignore"], value)
|
|
221
|
+
if key == "exclude" and "exclude" in merged:
|
|
222
|
+
return _merge_exclude_list(merged["exclude"], value)
|
|
223
|
+
return value
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _merge_ignore_block(existing_value, value) -> str:
|
|
227
|
+
existing = str(existing_value).strip().splitlines()
|
|
228
|
+
new_lines = str(value).strip().splitlines()
|
|
229
|
+
return _patterns_as_yaml_block(list(dict.fromkeys([*existing, *new_lines])))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _merge_exclude_list(existing_value, value) -> list:
|
|
233
|
+
existing = list(existing_value) if isinstance(existing_value, list) else []
|
|
234
|
+
new_items = value if isinstance(value, list) else [value]
|
|
235
|
+
return list(dict.fromkeys([*existing, *new_items]))
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def bundled_tool_ignore_patterns(tool_id: str) -> list[str]:
|
|
239
|
+
"""Static bundled defaults merged before suite/project ignores."""
|
|
240
|
+
if tool_id == "deadcode":
|
|
241
|
+
return ["**/models.py"]
|
|
242
|
+
return []
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def ignores_from_env(project_root: Path | None = None) -> EffectiveIgnores:
|
|
246
|
+
"""Build ignores from PYOSSMTOOL_IGNORE_* environment variables (script gates)."""
|
|
247
|
+
import os
|
|
248
|
+
|
|
249
|
+
root = (project_root or Path.cwd()).resolve()
|
|
250
|
+
profile_files = _resolve_env_profiles(root, os.environ.get("PYOSSMTOOL_IGNORE_PROFILES", ""))
|
|
251
|
+
path_patterns = _env_path_patterns(os.environ.get("PYOSSMTOOL_IGNORE_PATHS", ""))
|
|
252
|
+
return EffectiveIgnores(profile_files=profile_files, path_patterns=path_patterns)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _env_path_patterns(text: str) -> list[str]:
|
|
256
|
+
return [_normalize_pattern(item) for item in text.splitlines() if item.strip()]
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _resolve_env_profiles(root: Path, text: str) -> list[str]:
|
|
260
|
+
resolved: list[str] = []
|
|
261
|
+
for item in text.splitlines():
|
|
262
|
+
profile = item.strip()
|
|
263
|
+
if not profile:
|
|
264
|
+
continue
|
|
265
|
+
path = Path(profile)
|
|
266
|
+
if not path.is_absolute():
|
|
267
|
+
path = root / path
|
|
268
|
+
if path.is_file():
|
|
269
|
+
resolved.append(str(path.resolve()))
|
|
270
|
+
return resolved
|