Coverage for lintro / tools / core / version_checking.py: 88%
50 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Tool version requirements and checking utilities.
3This module centralizes version management for external lintro tools.
5## Version Sources
7External tool versions come from two sources:
81. npm tools (prettier, oxlint, etc.): Read from package.json at runtime
92. Non-npm tools (hadolint, shellcheck, etc.): Defined in _tool_versions.py
11Both are accessed via the get_tool_version() and get_all_expected_versions()
12functions in lintro/_tool_versions.py.
14Bundled Python tools (ruff, black, bandit, mypy, yamllint) are managed
15via pyproject.toml dependencies and don't need tracking in _tool_versions.py.
17## Adding a New Tool
19All tool types must be added to manifest.json with the correct install type.
20CI runs verify-manifest-sync.py which validates every manifest entry against
21its authoritative source (pyproject.toml for pip, package.json for npm,
22TOOL_VERSIONS for binary/cargo/rustup). PRs will fail if they drift.
24### For npm Tools:
251. Add to package.json devDependencies
262. Add mapping in _NPM_PACKAGE_TO_TOOL in _tool_versions.py
273. Add entry to manifest.json with install.type = "npm"
284. Renovate updates package.json automatically
30### For Non-npm External Tools (binary, cargo, rustup):
311. Add to TOOL_VERSIONS in _tool_versions.py
322. Add entry to manifest.json (version must match TOOL_VERSIONS)
333. Add Renovate regex manager in renovate.json for both files
344. If installable via Homebrew: verify the Homebrew formula provides a
35 version compatible with the entry in TOOL_VERSIONS and manifest.json
36 before adding depends_on to lintro.rb.template. Homebrew's depends_on
37 cannot pin versions, so only add it when the Homebrew package version
38 matches. If versions diverge, omit the depends_on line and document
39 alternative install instructions (or add a note in renovate.json).
41### For Bundled Python Tools:
421. Add as dependency in pyproject.toml
432. Add entry to manifest.json with install.type = "pip"
443. Renovate tracks it automatically
454. Note: Homebrew formula excludes bundled Python tools from the venv
46 (via generate_resources.py --exclude). They are installed as separate
47 Homebrew formulae and discovered via PATH, not python -m.
48"""
50import os
51import threading
53from loguru import logger
55from lintro._tool_versions import (
56 _NPM_PACKAGE_TO_TOOL,
57 get_all_expected_versions,
58)
59from lintro.enums.tool_name import ToolName
61# Module-level set to track logged warnings and prevent duplicates
62# during parallel execution
63_logged_warnings: set[str] = set()
64_logged_warnings_lock: threading.Lock = threading.Lock()
67def _get_version_timeout() -> int:
68 """Return the validated version check timeout.
70 Returns:
71 int: Timeout in seconds; falls back to default when the env var is invalid.
72 """
73 default_timeout = 30
74 env_value = os.getenv("LINTRO_VERSION_TIMEOUT")
75 if env_value is None:
76 return default_timeout
78 try:
79 timeout = int(env_value)
80 except (TypeError, ValueError):
81 logger.warning(
82 f"Invalid LINTRO_VERSION_TIMEOUT '{env_value}'; "
83 f"using default {default_timeout}",
84 )
85 return default_timeout
87 if timeout < 1:
88 logger.warning(
89 f"LINTRO_VERSION_TIMEOUT must be >= 1; using default {default_timeout}",
90 )
91 return default_timeout
93 return timeout
96VERSION_CHECK_TIMEOUT: int = _get_version_timeout()
99def get_minimum_versions() -> dict[str, str]:
100 """Get minimum version requirements for external tools.
102 Returns versions from _tool_versions module for tools that users
103 must install separately. Includes both npm-managed tools (from package.json)
104 and non-npm tools (from TOOL_VERSIONS).
106 Returns:
107 dict[str, str]: Dictionary mapping tool names (as strings) to minimum
108 version strings. Includes string equivalents of ToolName enums
109 (e.g., "pytest") and package aliases (e.g., "typescript" for TSC).
110 """
111 result: dict[str, str] = {}
113 # Get all versions (both npm and non-npm tools)
114 all_versions = get_all_expected_versions()
116 # Convert ToolName keys to their string values
117 for tool_name, version in all_versions.items():
118 if isinstance(tool_name, ToolName):
119 result[tool_name.value] = version
120 else:
121 result[tool_name] = version
123 # Add npm package aliases (e.g., "typescript" -> tsc version)
124 for npm_pkg, tool_name in _NPM_PACKAGE_TO_TOOL.items():
125 npm_version = all_versions.get(tool_name)
126 if npm_version is not None:
127 result[npm_pkg] = npm_version
129 return result
132def get_install_hints() -> dict[str, str]:
133 """Generate installation hints for external tools.
135 Returns:
136 dict[str, str]: Dictionary mapping tool names to installation hint strings.
137 """
138 # Static templates mapping tool -> install hint template with {version} placeholder
139 templates: dict[str, str] = {
140 "bandit": (
141 "Install via: pip install bandit>={version} or uv add bandit>={version}"
142 ),
143 "black": (
144 "Install via: pip install black>={version} or uv add black>={version}"
145 ),
146 "mypy": ("Install via: pip install mypy>={version} or uv add mypy>={version}"),
147 "pydoclint": (
148 "Install via: pip install pydoclint>={version} "
149 "or uv add pydoclint>={version}"
150 ),
151 "ruff": ("Install via: pip install ruff>={version} or uv add ruff>={version}"),
152 "yamllint": (
153 "Install via: pip install yamllint>={version} or uv add yamllint>={version}"
154 ),
155 "pytest": (
156 "Install via: pip install pytest>={version} or uv add pytest>={version}"
157 ),
158 "markdownlint": "Install via: bun add -d markdownlint-cli2@>={version}",
159 "markdownlint-cli2": "Install via: bun add -d markdownlint-cli2@>={version}",
160 "oxfmt": "Install via: bun add -d oxfmt@>={version}",
161 "oxlint": "Install via: bun add -d oxlint@>={version}",
162 "prettier": "Install via: bun add -d prettier@>={version}",
163 "tsc": (
164 "Install via: bun add -g typescript@{version}, "
165 "npm install -g typescript@{version}, or brew install typescript"
166 ),
167 "typescript": (
168 "Install via: bun add -g typescript@{version}, "
169 "npm install -g typescript@{version}, or brew install typescript"
170 ),
171 "hadolint": (
172 "Install via: https://github.com/hadolint/hadolint/releases (v{version}+)"
173 ),
174 "actionlint": (
175 "Install via: https://github.com/rhysd/actionlint/releases (v{version}+)"
176 ),
177 "clippy": "Install via: rustup component add clippy (requires Rust {version}+)",
178 "rustc": (
179 "Install via: rustup toolchain install {version} "
180 "&& rustup default {version}"
181 ),
182 "rustfmt": "Install via: rustup component add rustfmt (v{version}+)",
183 "cargo_audit": "Install via: cargo install cargo-audit (v{version}+)",
184 "cargo_deny": "Install via: cargo install cargo-deny (v{version}+)",
185 "biome": "Install via: bun add -d @biomejs/biome@>={version}",
186 "semgrep": (
187 "Install via: pip install semgrep>={version} or brew install semgrep"
188 ),
189 "gitleaks": (
190 "Install via: https://github.com/gitleaks/gitleaks/releases (v{version}+)"
191 ),
192 "osv_scanner": (
193 "Install via: https://github.com/google/osv-scanner/releases (v{version}+)"
194 ),
195 "shellcheck": (
196 "Install via: https://github.com/koalaman/shellcheck/releases (v{version}+)"
197 ),
198 "shfmt": "Install via: https://github.com/mvdan/sh/releases (v{version}+)",
199 "sqlfluff": (
200 "Install via: pip install sqlfluff>={version} or uv add sqlfluff>={version}"
201 ),
202 "taplo": (
203 "Install via: cargo install taplo-cli "
204 "or download from https://github.com/tamasfe/taplo/releases (v{version}+)"
205 ),
206 "astro_check": (
207 "Install via: bun add astro@>={version} or npm install astro@>={version}"
208 ),
209 "astro": (
210 "Install via: bun add astro@>={version} or npm install astro@>={version}"
211 ),
212 "svelte_check": (
213 "Install via: bun add -D svelte-check@>={version} "
214 "or npm install -D svelte-check@>={version}"
215 ),
216 "svelte-check": (
217 "Install via: bun add -D svelte-check@>={version} "
218 "or npm install -D svelte-check@>={version}"
219 ),
220 "vue_tsc": (
221 "Install via: bun add -D vue-tsc@>={version} "
222 "or npm install -D vue-tsc@>={version}"
223 ),
224 "vue-tsc": (
225 "Install via: bun add -D vue-tsc@>={version} "
226 "or npm install -D vue-tsc@>={version}"
227 ),
228 }
230 versions = get_minimum_versions()
231 hints: dict[str, str] = {}
233 # Build hints only for tools that exist in versions
234 for tool, template in templates.items():
235 version = versions.get(tool)
236 if version is not None:
237 hints[tool] = template.format(version=version)
239 # Warn about tools in versions that don't have templates (only once)
240 missing = set(versions) - set(templates)
241 if missing:
242 warning_key = f"missing_hints:{','.join(sorted(missing))}"
243 with _logged_warnings_lock:
244 if warning_key not in _logged_warnings:
245 _logged_warnings.add(warning_key)
246 logger.warning(
247 f"Missing install hints for tools: {', '.join(sorted(missing))}",
248 )
250 return hints