diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 5901b6b..e6c1bf6 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -5,6 +5,13 @@ early_access: false reviews: profile: chill request_changes_workflow: true + # Repository-owned checks validate deliberately invalid static-analysis fixtures. + path_filters: + - "!tests/semgrep/**" + pre_merge_checks: + # Ruff owns docstring policy without treating fixtures as public APIs. + docstrings: + mode: off # Publish the legacy commit status consumed by the main-branch ruleset. # Keep review_progress off until CodeRabbit can publish the required check run. review_progress: false diff --git a/justfile b/justfile index d716d1a..6cbb15b 100644 --- a/justfile +++ b/justfile @@ -24,13 +24,13 @@ cargo_nextest_version := "0.9.143" cargo_update_version := "22.1.1" clippy_sarif_version := "0.8.0" dprint_version := "0.57.0" -git_cliff_version := "2.13.1" +git_cliff_version := "2.14.1" just_version := "1.58.0" -rumdl_version := "0.2.62" +rumdl_version := "0.2.63" sarif_fmt_version := "0.8.0" taplo_version := "0.10.0" -typos_version := "1.50.0" -uv_version := "0.12.7" +typos_version := "1.50.1" +uv_version := "0.12.9" zizmor_version := "1.30.0" # Internal helpers: ensure external tooling is installed @@ -209,11 +209,9 @@ _ensure-uv-available: _ensure-stable-uv-version: _ensure-uv-available #!/usr/bin/env bash set -euo pipefail - version_output="$(uv --version 2>&1)" - if [[ ! "$version_output" =~ ^uv[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+)([[:space:]]|$) ]]; then - echo "❌ 'uv --version' must report a stable X.Y.Z version; got: $version_output" >&2 - exit 1 - fi + uv_executable="$(command -v uv)" + version_output="$("$uv_executable" --version)" + "$uv_executable" run --locked update-cargo-tool-pins "--check-uv-version=$version_output" _ensure-yamllint: _ensure-uv #!/usr/bin/env bash @@ -396,7 +394,7 @@ check-fast: # CI simulation: flat GitHub-equivalent union of leaf validators. # Keep this dependency list explicit so each validation surface runs once without # re-entering broad check/test bundles. All Cargo targets match the SARIF lint scope. -ci: action-lint zizmor markdown-check spell-check docs-version-check toml-parse-check toml-fmt-check toml-lint yaml-fmt-check yaml-lint citation-check validate-json justfile-fmt-check python-format-check python-lint python-typecheck test-python cargo-lock-check fmt-check clippy-all-targets doc-check semgrep semgrep-test unused-deps shell-check test-rust-ci test-doc test-doc-exact bench-compile examples +ci: action-lint zizmor markdown-check spell-check docs-version-check toml-parse-check toml-fmt-check toml-lint yaml-fmt-check yaml-lint citation-check validate-json justfile-fmt-check python-format-check python-lint python-fixture-lint python-typecheck test-python cargo-lock-check fmt-check clippy-all-targets doc-check semgrep semgrep-test unused-deps shell-check test-rust-ci test-doc test-doc-exact bench-compile examples @echo "🎯 CI checks complete!" # Validate CITATION.cff against the Citation File Format schema. @@ -702,26 +700,29 @@ plot-vs-linalg metric="lu_solve" stat="median" sample="new" log_y="false" allow_ uv run --locked criterion-dim-plot "${args[@]}" # Python tooling (uv) -python-check: python-format-check python-lint python-typecheck +python-check: python-format-check python-lint python-fixture-lint python-typecheck -python-ci: python-format-check python-lint python-typecheck test-python +python-ci: python-format-check python-lint python-fixture-lint python-typecheck test-python @echo "✅ Python checks complete!" python-fix: python-sync uv run --locked ruff check scripts/ --fix - uv run --locked ruff format scripts/ + uv run --locked ruff format scripts/ tests/semgrep/scripts/ python-format-check: python-sync - uv run --locked ruff format --check scripts/ + uv run --locked ruff format --check scripts/ tests/semgrep/scripts/ python-lint: python-sync uv run --locked ruff check scripts/ +python-fixture-lint: python-sync + uv run --locked ruff check tests/semgrep/scripts/ + python-sync: _ensure-uv uv sync --locked --group dev python-typecheck: python-sync - uv run --locked ty check scripts/ --error all + uv run --locked ty check scripts/ tests/semgrep/scripts/ --error all rust-core-check: cargo-lock-check fmt-check clippy-core doc-check semgrep semgrep-test unused-deps @echo "✅ Rust core checks complete!" @@ -1115,7 +1116,7 @@ update-cargo-tools: _ensure-stable-uv-version _ensure-cargo-install-update # Advance Cargo and exact Python development requirements plus their lockfiles. [doc('Update Cargo and Python development requirements plus all Cargo/uv locked dependencies.')] -update-dependencies: _ensure-cargo-edit _ensure-uv-available update-cargo-dependencies update-python-dependencies +update-dependencies: _ensure-cargo-edit _ensure-stable-uv-version update-cargo-dependencies update-python-dependencies # Advance Cargo dependency declarations and lockfile entries. [doc('Update Cargo.toml dependency requirements and Cargo.lock.')] diff --git a/pyproject.toml b/pyproject.toml index 1d91dfd..7abef77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ line-length = 160 src = [ "scripts" ] [tool.ruff.lint] -select = [ "E", "F", "W", "ANN201", "ANN202", "ANN204", "C90", "I", "N", "UP", "YTT", "ASYNC", "S", "BLE", "FBT", "B", "A", "COM", "C4", "DTZ", "FA", "T10", "EM", "EXE", "FIX", "FLY", "FURB", "ISC", "ICN", "LOG", "G", "INP", "PERF", "PIE", "T20", "PYI", "PT", "Q", "RSE", "RET", "SLF", "SIM", "SLOT", "TC", "TID", "TD", "ARG", "PTH", "ERA", "PD", "PGH", "PL", "TRY", "NPY", "RUF", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107" ] +select = [ "E", "F", "W", "ANN001", "ANN002", "ANN003", "ANN201", "ANN202", "ANN204", "ANN205", "ANN206", "C90", "I", "N", "UP", "YTT", "ASYNC", "S", "BLE", "FBT", "B", "A", "COM", "C4", "DTZ", "FA", "T10", "EM", "EXE", "FIX", "FLY", "FURB", "ISC", "ICN", "LOG", "G", "INP", "PERF", "PIE", "T20", "PYI", "PT", "Q", "RSE", "RET", "SLF", "SIM", "SLOT", "TC", "TID", "TD", "ARG", "PTH", "ERA", "PD", "PGH", "PL", "TRY", "NPY", "RUF", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107" ] fixable = [ "ALL" ] unfixable = [ ] ignore = [ @@ -83,6 +83,9 @@ ignore = [ [tool.ruff.lint.per-file-ignores] "**/tests/test_*.py" = [ "S101", "SLF001", "D101", "D102", "D103" ] +# Static-analysis fixtures intentionally contain the patterns these rules reject. +"tests/semgrep/scripts/python_portability.py" = [ "D103", "INP001", "N813", "PLW1510", "S603", "S607", "SIM115", "UP020", "UP021" ] +"tests/semgrep/scripts/tests/python_exceptions.py" = [ "BLE001", "D100", "D103", "EM101", "INP001", "S110", "S607", "SIM105", "TRY002" ] [tool.ruff.lint.mccabe] max-complexity = 10 @@ -152,7 +155,7 @@ dev = [ "actionlint-py==1.7.12.24", "pytest==9.1.1", "ruff==0.16.5", - "semgrep==1.175.0", + "semgrep==1.176.0", "shellcheck-py==0.11.0.1", "shfmt-py==4.1.0", "ty==0.0.77", diff --git a/scripts/archive_changelog.py b/scripts/archive_changelog.py index 77479f9..1da184b 100755 --- a/scripts/archive_changelog.py +++ b/scripts/archive_changelog.py @@ -303,6 +303,7 @@ def _stage_text(path: Path, text: str) -> Path: with tempfile.NamedTemporaryFile( "w", encoding="utf-8", + newline="", dir=path.parent, prefix=f".{path.name}.", suffix=".tmp", diff --git a/scripts/archive_performance.py b/scripts/archive_performance.py index ef8f0fb..4620ed3 100644 --- a/scripts/archive_performance.py +++ b/scripts/archive_performance.py @@ -435,6 +435,7 @@ def _write_text(path: Path, text: str) -> None: with tempfile.NamedTemporaryFile( "w", encoding="utf-8", + newline="", dir=path.parent, prefix=f".{path.name}.", suffix=".tmp", diff --git a/scripts/bench_compare.py b/scripts/bench_compare.py index 2bd4e21..8b665e2 100644 --- a/scripts/bench_compare.py +++ b/scripts/bench_compare.py @@ -2129,6 +2129,7 @@ def _write_text_atomic(path: Path, text: str) -> None: with tempfile.NamedTemporaryFile( "w", encoding="utf-8", + newline="", dir=path.parent, prefix=f".{path.name}.", suffix=".tmp", diff --git a/scripts/criterion_dim_plot.py b/scripts/criterion_dim_plot.py index 9b9ebdf..0968e42 100644 --- a/scripts/criterion_dim_plot.py +++ b/scripts/criterion_dim_plot.py @@ -402,7 +402,7 @@ def _require_confidence_interval(lo: float, hi: float, context: str) -> None: def _write_csv(out_csv: Path, rows: list[Row]) -> None: out_csv.parent.mkdir(parents=True, exist_ok=True) - with out_csv.open("w", encoding="utf-8") as f: + with out_csv.open("w", encoding="utf-8", newline="\n") as f: f.write("D,la_stack,la_lo,la_hi,nalgebra,na_lo,na_hi,faer,fa_lo,fa_hi\n") for row in rows: f.write(f"{row.dim},{row.la_time},{row.la_lo},{row.la_hi},{row.na_time},{row.na_lo},{row.na_hi},{row.fa_time},{row.fa_lo},{row.fa_hi}\n") @@ -461,7 +461,7 @@ def _update_readme_table(readme_path: Path, marker_begin: str, marker_end: str, if new_lines == lines: return False - readme_path.write_text("".join(new_lines), encoding="utf-8") + readme_path.write_text("".join(new_lines), encoding="utf-8", newline="\n") return True @@ -1095,7 +1095,7 @@ def _capture_performance_provenance( def _write_provenance(path: Path, provenance: dict[str, object]) -> None: """Write stable, sorted JSON provenance beside generated benchmark assets.""" path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(provenance, indent=2, sort_keys=True) + "\n", encoding="utf-8") + path.write_text(json.dumps(provenance, indent=2, sort_keys=True) + "\n", encoding="utf-8", newline="\n") def _validate_readme_target(root: Path, args: PlotCliArgs) -> int: # noqa: C901, PLR0911, PLR0912 @@ -1283,7 +1283,7 @@ def _update_staged_readme_publication( stat=args.stat, version=package_version, ) - staged_readme.write_text(updated_readme, encoding="utf-8") + staged_readme.write_text(updated_readme, encoding="utf-8", newline="\n") def _stage_and_publish_outputs( # noqa: PLR0913 diff --git a/scripts/postprocess_changelog.py b/scripts/postprocess_changelog.py index 119a18d..7d00cdb 100644 --- a/scripts/postprocess_changelog.py +++ b/scripts/postprocess_changelog.py @@ -904,6 +904,7 @@ def _write_text_atomic(path: Path, text: str) -> None: with tempfile.NamedTemporaryFile( "w", encoding="utf-8", + newline="", dir=path.parent, prefix=f".{path.name}.", suffix=".tmp", diff --git a/scripts/subprocess_utils.py b/scripts/subprocess_utils.py index e816a76..cf350a9 100644 --- a/scripts/subprocess_utils.py +++ b/scripts/subprocess_utils.py @@ -233,7 +233,12 @@ def run_safe_command( def _darwin_cpu_model() -> str: try: return run_safe_command("sysctl", ["-n", "machdep.cpu.brand_string"]).stdout.strip() - except ExecutableNotFoundError, OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired: + except ( + ExecutableNotFoundError, + OSError, + subprocess.CalledProcessError, + subprocess.TimeoutExpired, + ): return "" diff --git a/scripts/tests/test_justfile_discoverability.py b/scripts/tests/test_justfile_discoverability.py index 60338be..319e150 100644 --- a/scripts/tests/test_justfile_discoverability.py +++ b/scripts/tests/test_justfile_discoverability.py @@ -3,12 +3,15 @@ import json import os import re +import shlex import shutil import stat import subprocess from pathlib import Path from typing import Any +import pytest + import update_cargo_tool_pins REPO_ROOT = Path(__file__).resolve().parents[2] @@ -40,6 +43,49 @@ def just_recipes() -> dict[str, dict[str, Any]]: return recipes +def write_fake_uv(directory: Path, version_output: str, *, windows_lookup_output: str | None = None) -> None: + """Write a Bash uv shim and an optional conflicting native Windows shim.""" + real_uv = shutil.which("uv") + assert real_uv is not None + posix_shim = directory / "uv" + posix_shim.write_text( + f"""#!/bin/sh +if [ "$1" = "--version" ]; then + printf '%s\\n' {shlex.quote(version_output)} +else + exec {shlex.quote(real_uv)} "$@" +fi +""", + encoding="utf-8", + newline="\n", + ) + posix_shim.chmod(posix_shim.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + + if os.name == "nt": + native_output = windows_lookup_output or version_output + if re.fullmatch(r"[A-Za-z0-9 ._-]+", native_output) is None: + msg = f"unsupported native uv version fixture output: {native_output!r}" + raise ValueError(msg) + windows_shim = directory / "uv.cmd" + real_uv_command = subprocess.list2cmdline([real_uv]) + script = "\r\n".join( + ( + "@echo off", + 'if "%~1"=="--version" (', + f" echo({native_output}", + " exit /b 0", + ")", + f"{real_uv_command} %*", + "", + ) + ) + windows_shim.write_text( + script, + encoding="utf-8", + newline="", + ) + + def test_uv_backed_helpers_reuse_pinned_guard() -> None: """Local uv consumers should share one exact-version implementation.""" recipes = just_recipes() @@ -59,9 +105,7 @@ def test_uv_guard_reports_expected_and_actual_versions(tmp_path: Path) -> None: """A mismatched uv executable should fail with actionable version details.""" fake_bin = tmp_path / "bin" fake_bin.mkdir() - fake_uv = fake_bin / "uv" - fake_uv.write_text("#!/bin/sh\nprintf '%s\\n' 'uv 9.9.9'\n", encoding="utf-8") - fake_uv.chmod(fake_uv.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + write_fake_uv(fake_bin, "uv 9.9.9") environment = os.environ.copy() environment["CARGO_HOME"] = str(tmp_path / "cargo") @@ -74,21 +118,35 @@ def test_uv_guard_reports_expected_and_actual_versions(tmp_path: Path) -> None: assert f"version '9.9.9', expected '{expected}'" in result.stderr -def test_stable_uv_preflight_rejects_nonstable_or_embedded_versions(tmp_path: Path) -> None: +@pytest.mark.parametrize( + "output", + ["uv 9.9.9 using runtime 3.14.0", "uv version unknown", "uv 9.9.9-beta.1", "uv 9.9.9.1", "uv release-9.9.9"], +) +def test_stable_uv_preflight_rejects_unstorable_versions(tmp_path: Path, output: str) -> None: """Update preflights should reject uv versions the pin reconciler cannot store.""" fake_bin = tmp_path / "bin" fake_bin.mkdir() - fake_uv = fake_bin / "uv" + write_fake_uv(fake_bin, output, windows_lookup_output="uv 7.7.7") + environment = os.environ.copy() + environment["PATH"] = f"{fake_bin}{os.pathsep}{environment['PATH']}" + + result = run_just("_ensure-stable-uv-version", check=False, env=environment) + + assert result.returncode != 0 + assert "must report exactly one stable X.Y.Z version" in result.stderr + + +def test_stable_uv_preflight_accepts_newer_stable_version(tmp_path: Path) -> None: + """Update reconciliation may advance the repository's active uv pin.""" + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + write_fake_uv(fake_bin, "uv 9.9.9") environment = os.environ.copy() environment["PATH"] = f"{fake_bin}{os.pathsep}{environment['PATH']}" - for output in ("uv 9.9.9-beta.1", "uv 9.9.9.1", "uv release-9.9.9"): - fake_uv.write_text(f"#!/bin/sh\nprintf '%s\\n' '{output}'\n", encoding="utf-8") - fake_uv.chmod(fake_uv.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) - result = run_just("_ensure-stable-uv-version", check=False, env=environment) + result = run_just("_ensure-stable-uv-version", check=False, env=environment) - assert result.returncode != 0 - assert "must report a stable X.Y.Z version" in result.stderr + assert result.returncode == 0 def test_update_workflow_composes_scoped_dependency_and_tool_updates() -> None: @@ -107,14 +165,14 @@ def test_update_workflow_composes_scoped_dependency_and_tool_updates() -> None: aggregate_update = aggregate_result.stdout + aggregate_result.stderr cargo_upgrade = "cargo upgrade --incompatible allow --exclude num-bigint --exclude num-rational" assert aggregate_update.index("command -v cargo-install-update") < aggregate_update.index(cargo_upgrade) - assert aggregate_update.index("must report a stable X.Y.Z version") < aggregate_update.index(cargo_upgrade) + assert aggregate_update.index("--check-uv-version") < aggregate_update.index(cargo_upgrade) dependency_result = run_just("--dry-run", "update-dependencies") dependency_update = dependency_result.stdout + dependency_result.stderr dependency_preflights = [dependency["recipe"] for dependency in recipes["update-dependencies"]["dependencies"]] - assert dependency_preflights[:2] == ["_ensure-cargo-edit", "_ensure-uv-available"] + assert dependency_preflights[:2] == ["_ensure-cargo-edit", "_ensure-stable-uv-version"] assert dependency_update.index("cargo upgrade --version") < dependency_update.index(cargo_upgrade) - assert dependency_update.index("uv --version") < dependency_update.index(cargo_upgrade) + assert dependency_update.index("--check-uv-version") < dependency_update.index(cargo_upgrade) assert cargo_upgrade in dependency_update assert "cargo update" in dependency_update assert "uv run --locked update-python-dev-pins" in dependency_update @@ -128,7 +186,7 @@ def test_update_workflow_composes_scoped_dependency_and_tool_updates() -> None: tool_update = tool_result.stdout + tool_result.stderr assert "command -v cargo-install-update" in tool_update assert "cargo install-update --locked" in tool_update - assert tool_update.index("must report a stable X.Y.Z version") < tool_update.index("cargo install-update --locked") + assert tool_update.index("--check-uv-version") < tool_update.index("cargo install-update --locked") assert "update-cargo-tool-pins" in tool_update assert "cargo install-update --all" not in tool_update assert "uv tool upgrade" not in tool_update @@ -152,3 +210,14 @@ def test_managed_tool_pins_exist_once_in_root_justfile() -> None: for pin in update_cargo_tool_pins.PIN_TO_TOOL: assert len(re.findall(rf'(?m)^{re.escape(pin)}\s*:=\s*"[^"]+"\s*$', justfile_text)) == 1 + + +def test_ci_enforces_full_python_fixture_lint_policy() -> None: + """Canonical CI should lint fixtures without narrowing the Ruff configuration.""" + recipes = just_recipes() + ci_dependencies = {dependency["recipe"] for dependency in recipes["ci"]["dependencies"]} + fixture_lint_body = json.dumps(recipes["python-fixture-lint"]["body"]) + + assert "python-fixture-lint" in ci_dependencies + assert "ruff check tests/semgrep/scripts/" in fixture_lint_body + assert "--select" not in fixture_lint_body diff --git a/scripts/tests/test_update_cargo_tool_pins.py b/scripts/tests/test_update_cargo_tool_pins.py index c5caeab..df30f1e 100644 --- a/scripts/tests/test_update_cargo_tool_pins.py +++ b/scripts/tests/test_update_cargo_tool_pins.py @@ -104,12 +104,30 @@ def test_reconcile_pins_preserves_prerelease_with_build_metadata(tmp_path: Path) assert f'rumdl_version := "{version}"' in justfile.read_text(encoding="utf-8").splitlines() -@pytest.mark.parametrize("output", ["uv 1.2.3-rc.1", "uv 1.2.3.4", "uv release-1.2.3"]) -def test_parse_tool_version_rejects_nonstable_or_embedded_versions(output: str) -> None: +@pytest.mark.parametrize( + "output", + ["uv 1.2.3 using runtime 3.14.0", "uv version unknown", "uv 1.2.3-rc.1", "uv 1.2.3.4", "uv release-1.2.3"], +) +def test_parse_tool_version_rejects_ambiguous_missing_prerelease_or_embedded_versions(output: str) -> None: with pytest.raises(ValueError, match="expected exactly one uv version"): update_cargo_tool_pins.parse_tool_version(output, "uv") +def test_check_uv_version_reuses_pin_reconciler_parser() -> None: + with pytest.raises(ValueError, match=r"must report exactly one stable X\.Y\.Z version"): + update_cargo_tool_pins.check_uv_version("uv 9.9.9 using runtime 3.14.0") + + +def test_main_checks_captured_uv_output_without_resolving_another_executable(monkeypatch: pytest.MonkeyPatch) -> None: + def unexpected_uv_lookup(*_args: object, **_kwargs: object) -> Never: + msg = "captured uv output must not trigger another executable lookup" + raise AssertionError(msg) + + monkeypatch.setattr(update_cargo_tool_pins, "run_safe_command", unexpected_uv_lookup) + + assert update_cargo_tool_pins.main(["--check-uv-version=uv 9.9.9"]) == 0 + + def test_main_reports_missing_cargo_without_traceback( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], diff --git a/scripts/update_cargo_tool_pins.py b/scripts/update_cargo_tool_pins.py index b69e73c..81f4fb1 100644 --- a/scripts/update_cargo_tool_pins.py +++ b/scripts/update_cargo_tool_pins.py @@ -112,19 +112,38 @@ def reconcile_pins(justfile: Path, installed_output: str, uv_output: str) -> dic def parse_args(argv: list[str] | None = None) -> argparse.Namespace: """Parse command-line arguments.""" parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--check-uv-version", + metavar="OUTPUT", + help="validate captured uv output as exactly one stable X.Y.Z version without reconciling pins", + ) parser.add_argument("--justfile", type=Path, default=Path("justfile"), help="Just source containing repository tool pins") return parser.parse_args(argv) +def check_uv_version(output: str) -> str: + """Parse captured uv output or explain why the reconciler cannot store it.""" + try: + return parse_tool_version(output, "uv") + except ValueError as error: + observed = output.strip() or "" + msg = f"'uv --version' must report exactly one stable X.Y.Z version; got: {observed} ({error})" + raise ValueError(msg) from error + + def main(argv: list[str] | None = None) -> int: - """Reconcile managed pins from the active Cargo and uv installations.""" + """Validate uv output or reconcile pins from active tool installations.""" args = parse_args(argv) + operation = "validate uv version" if args.check_uv_version is not None else "update tool pins" try: + if args.check_uv_version is not None: + check_uv_version(args.check_uv_version) + return 0 cargo = run_cargo_command(["install", "--list"], timeout=30) uv = run_safe_command("uv", ["--version"], timeout=30) changes = reconcile_pins(args.justfile, cargo.stdout, uv.stdout) except (ExecutableNotFoundError, OSError, subprocess.SubprocessError, ValueError) as error: - print(f"failed to update tool pins: {error}", file=sys.stderr) + print(f"failed to {operation}: {error}", file=sys.stderr) return 1 if not changes: diff --git a/semgrep.yaml b/semgrep.yaml index ec654ab..e0fd19e 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -905,6 +905,178 @@ rules: - pattern-regex: '^\s*#\s*\[\s*expect\s*\([^\]\n]*\)\s*\]' - pattern-not-regex: '\breason\s*=' + - id: la-stack.python.git-stdin-binary-transport + languages: + - python + severity: WARNING + message: "Keep Git stdin byte-exact: use text=False and do not enable encoding, errors, or universal_newlines on the subprocess." + metadata: + category: correctness + rationale: "Windows text pipes rewrite LF and CRLF, corrupting hashes, patches, and verbatim tag messages before Git receives them." + paths: + include: + - "/scripts/**/*.py" + - "/tests/semgrep/scripts/**/*.py" + exclude: + - "/scripts/tests/**" + - "/tests/semgrep/scripts/tests/**" + patterns: + - pattern-inside: | + def run_git_command_with_input(...): + ... + - pattern-either: + - pattern: subprocess.run(..., input=$INPUT, ...) + - pattern: subprocess.Popen(...) + - pattern-either: + - patterns: + - pattern-either: + - pattern: subprocess.run(..., input=$INPUT, ...) + - pattern: subprocess.Popen(...) + - pattern-not: $CALL(..., text=False, ...) + - patterns: + - pattern-either: + - pattern: subprocess.run(..., encoding=$ENCODING, ...) + - pattern: subprocess.Popen(..., encoding=$ENCODING, ...) + - pattern-not: $CALL(..., encoding=None, ...) + - patterns: + - pattern-either: + - pattern: subprocess.run(..., errors=$ERRORS, ...) + - pattern: subprocess.Popen(..., errors=$ERRORS, ...) + - pattern-not: $CALL(..., errors=None, ...) + - patterns: + - pattern-either: + - pattern: subprocess.run(..., universal_newlines=$NEWLINES, ...) + - pattern: subprocess.Popen(..., universal_newlines=$NEWLINES, ...) + - pattern-not: $CALL(..., universal_newlines=False, ...) + - pattern-not: $CALL(..., universal_newlines=None, ...) + + - id: la-stack.python.git-stdin-use-shared-helper + languages: + - python + severity: WARNING + message: "Send Git stdin through subprocess_utils.run_git_command_with_input so Windows cannot translate the input bytes." + metadata: + category: correctness + rationale: "The shared Git-input helper preserves exact encoded input; the generic text wrappers do not make that guarantee." + paths: + include: + - "/scripts/**/*.py" + - "/tests/semgrep/scripts/**/*.py" + exclude: + - "/scripts/tests/**" + - "/tests/semgrep/scripts/tests/**" + patterns: + - pattern-either: + - pattern: run_git_command(..., input=$INPUT, ...) + - pattern: subprocess_utils.run_git_command(..., input=$INPUT, ...) + - pattern: run_safe_command("git", ..., input=$INPUT, ...) + - pattern: subprocess_utils.run_safe_command("git", ..., input=$INPUT, ...) + - pattern: subprocess.run(["git", ...], ..., input=$INPUT, ...) + - pattern: subprocess.Popen(["git", ...], ..., stdin=subprocess.PIPE, ...) + - pattern-not: $CALL(..., input=None, ...) + + - id: la-stack.python.text-writes-explicit-policy + languages: + - python + severity: WARNING + message: 'Pass explicit non-None encoding and newline keywords: UTF-8 with newline="\n" for generated LF text, or newline="" to preserve supplied endings.' + metadata: + category: correctness + rationale: "Platform-default text writes make artifact bytes depend on the host OS, even when Git checkout conversion is disabled." + paths: + include: + - "/scripts/**/*.py" + - "/tests/semgrep/scripts/**/*.py" + exclude: + - "/scripts/tests/**" + - "/tests/semgrep/scripts/tests/**" + pattern-either: + - patterns: + - pattern-either: + - pattern: $PATH.write_text(...) + - patterns: + - pattern-either: + - pattern: open($PATH, $MODE, ...) + - pattern: open(..., mode=$MODE, ...) + - pattern: io.open($PATH, $MODE, ...) + - pattern: io.open(..., mode=$MODE, ...) + - pattern: $PATH.open($MODE, ...) + - pattern: $PATH.open(..., mode=$MODE, ...) + - pattern: os.fdopen($FD, $MODE, ...) + - pattern: os.fdopen(..., mode=$MODE, ...) + - pattern: tempfile.NamedTemporaryFile($MODE, ...) + - pattern: tempfile.NamedTemporaryFile(..., mode=$MODE, ...) + - pattern: tempfile.TemporaryFile($MODE, ...) + - pattern: tempfile.TemporaryFile(..., mode=$MODE, ...) + - pattern: tempfile.SpooledTemporaryFile($MAX_SIZE, $MODE, ...) + - pattern: tempfile.SpooledTemporaryFile(..., mode=$MODE, ...) + - metavariable-regex: + metavariable: $MODE + regex: ^['"](?=[^'"]*[wax+])(?![^'"]*b)[rawxt+]+['"]$ + - pattern-not: $CALL(..., encoding=$ENCODING, ..., newline=$NEWLINE, ...) + - pattern: $PATH.write_text(..., encoding=None, ...) + - pattern: $PATH.write_text(..., newline=None, ...) + - patterns: + - pattern-either: + - pattern: open($PATH, $MODE, ..., encoding=None, ...) + - pattern: open(..., mode=$MODE, ..., encoding=None, ...) + - pattern: io.open($PATH, $MODE, ..., encoding=None, ...) + - pattern: io.open(..., mode=$MODE, ..., encoding=None, ...) + - pattern: $PATH.open($MODE, ..., encoding=None, ...) + - pattern: $PATH.open(..., mode=$MODE, ..., encoding=None, ...) + - pattern: os.fdopen($FD, $MODE, ..., encoding=None, ...) + - pattern: os.fdopen(..., mode=$MODE, ..., encoding=None, ...) + - pattern: tempfile.NamedTemporaryFile($MODE, ..., encoding=None, ...) + - pattern: tempfile.NamedTemporaryFile(..., mode=$MODE, ..., encoding=None, ...) + - pattern: tempfile.TemporaryFile($MODE, ..., encoding=None, ...) + - pattern: tempfile.TemporaryFile(..., mode=$MODE, ..., encoding=None, ...) + - pattern: tempfile.SpooledTemporaryFile($MAX_SIZE, $MODE, ..., encoding=None, ...) + - pattern: tempfile.SpooledTemporaryFile(..., mode=$MODE, ..., encoding=None, ...) + - pattern: open($PATH, $MODE, ..., newline=None, ...) + - pattern: open(..., mode=$MODE, ..., newline=None, ...) + - pattern: io.open($PATH, $MODE, ..., newline=None, ...) + - pattern: io.open(..., mode=$MODE, ..., newline=None, ...) + - pattern: $PATH.open($MODE, ..., newline=None, ...) + - pattern: $PATH.open(..., mode=$MODE, ..., newline=None, ...) + - pattern: os.fdopen($FD, $MODE, ..., newline=None, ...) + - pattern: os.fdopen(..., mode=$MODE, ..., newline=None, ...) + - pattern: tempfile.NamedTemporaryFile($MODE, ..., newline=None, ...) + - pattern: tempfile.NamedTemporaryFile(..., mode=$MODE, ..., newline=None, ...) + - pattern: tempfile.TemporaryFile($MODE, ..., newline=None, ...) + - pattern: tempfile.TemporaryFile(..., mode=$MODE, ..., newline=None, ...) + - pattern: tempfile.SpooledTemporaryFile($MAX_SIZE, $MODE, ..., newline=None, ...) + - pattern: tempfile.SpooledTemporaryFile(..., mode=$MODE, ..., newline=None, ...) + - metavariable-regex: + metavariable: $MODE + regex: ^['"](?=[^'"]*[wax+])(?![^'"]*b)[rawxt+]+['"]$ + + - id: la-stack.python.explicit-path-text-encoding-in-tests + languages: + - python + severity: WARNING + message: 'Pass encoding="utf-8" to Path.read_text/write_text in Python tests.' + metadata: + category: portability + rationale: >- + Test fixtures that omit an explicit encoding can pass on UTF-8 platforms + while failing on Windows when output contains Unicode symbols. + paths: + include: + - "/scripts/tests/**/*.py" + - "/tests/semgrep/scripts/tests/**/*.py" + patterns: + - pattern-either: + - patterns: + - pattern: $PATH.read_text(...) + - pattern-not: $PATH.read_text(..., encoding="utf-8", ...) + - pattern-not: $PATH.read_text(..., encoding='utf-8', ...) + - pattern-not: $PATH.read_text(..., encoding=UTF8, ...) + - patterns: + - pattern: $PATH.write_text(...) + - pattern-not: $PATH.write_text(..., encoding="utf-8", ...) + - pattern-not: $PATH.write_text(..., encoding='utf-8', ...) + - pattern-not: $PATH.write_text(..., encoding=UTF8, ...) + - id: la-stack.python.no-broad-exception languages: - python diff --git a/tests/semgrep/scripts/python_portability.py b/tests/semgrep/scripts/python_portability.py new file mode 100644 index 0000000..1fe5fec --- /dev/null +++ b/tests/semgrep/scripts/python_portability.py @@ -0,0 +1,244 @@ +"""Static-analysis fixtures only; never execute these example calls.""" + +import io +import os +import subprocess +import tempfile +from subprocess import run as run_process +from tempfile import NamedTemporaryFile as named_file +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + from pathlib import Path + +import subprocess_utils as utils +from subprocess_utils import run_git_command, run_git_command_with_input as git_input, run_safe_command + + +def run_git_command_with_input(payload: str, argv: Sequence[str], options: Mapping[str, Any]) -> None: + # Original failure: the text defaults were hidden in shared kwargs. + kwargs: dict[str, Any] = {"text": True, "encoding": "utf-8"} + # ruleid: la-stack.python.git-stdin-binary-transport + subprocess.run(argv, input=payload, **kwargs) + # ruleid: la-stack.python.git-stdin-binary-transport + subprocess.run(argv, input=payload, text=True) + # ruleid: la-stack.python.git-stdin-binary-transport + subprocess.run(argv, input=payload.encode(), text=False, encoding="utf-8") + # ruleid: la-stack.python.git-stdin-binary-transport + subprocess.run(argv, input=payload.encode(), text=False, errors="strict") + # ruleid: la-stack.python.git-stdin-binary-transport + subprocess.run(argv, input=payload.encode(), text=False, universal_newlines=True) + # ruleid: la-stack.python.git-stdin-binary-transport + run_process(argv, input=payload, text=True) + # ruleid: la-stack.python.git-stdin-binary-transport + subprocess.Popen(argv, stdin=subprocess.PIPE) + # ruleid: la-stack.python.git-stdin-binary-transport + subprocess.Popen(argv, stdin=subprocess.PIPE, text=False, encoding="utf-8") + + # ok: la-stack.python.git-stdin-binary-transport + subprocess.run(argv, input=payload.encode("utf-8"), text=False, **options) + # ok: la-stack.python.git-stdin-binary-transport + run_process(argv, input=payload.encode(), text=False) + # ok: la-stack.python.git-stdin-binary-transport + subprocess.run(argv, input=payload.encode(), text=False, encoding=None, errors=None, universal_newlines=False) + # ok: la-stack.python.git-stdin-binary-transport + subprocess.Popen(argv, stdin=subprocess.PIPE, text=False) + + +def git_input_routing(payload: str, argv: Sequence[str]) -> None: + # ruleid: la-stack.python.git-stdin-use-shared-helper + run_git_command(argv, input=payload) + # ruleid: la-stack.python.git-stdin-use-shared-helper + utils.run_git_command(argv, input=payload) + # ruleid: la-stack.python.git-stdin-use-shared-helper + run_safe_command("git", argv, input=payload) + # ruleid: la-stack.python.git-stdin-use-shared-helper + utils.run_safe_command("git", argv, input=payload) + # ruleid: la-stack.python.git-stdin-use-shared-helper + subprocess.run(["git", "hash-object", "--stdin"], input=payload, text=True) + # ruleid: la-stack.python.git-stdin-use-shared-helper + run_process(["git", "hash-object", "--stdin"], input=payload, text=True) + # ruleid: la-stack.python.git-stdin-use-shared-helper + process = subprocess.Popen(["git", "hash-object", "--stdin"], stdin=subprocess.PIPE, text=True) + process.communicate(payload) + # Even binary Git input must go through the shared helper. + # ruleid: la-stack.python.git-stdin-use-shared-helper + process = subprocess.Popen(["git", "hash-object", "--stdin"], stdin=subprocess.PIPE, text=False) + process.communicate(payload.encode("utf-8")) + + # ok: la-stack.python.git-stdin-use-shared-helper + git_input(argv, payload) + # ok: la-stack.python.git-stdin-use-shared-helper + utils.run_git_command_with_input(argv, input_data=payload) + # ok: la-stack.python.git-stdin-use-shared-helper + run_git_command(argv) + # ok: la-stack.python.git-stdin-use-shared-helper + run_git_command(argv, input=None) + # Ordinary text-mode subprocesses outside the Git-input helper are allowed. + # ok: la-stack.python.git-stdin-use-shared-helper, la-stack.python.git-stdin-binary-transport + run_safe_command("ruff", ["check", "-"], input=payload) + # ok: la-stack.python.git-stdin-use-shared-helper, la-stack.python.git-stdin-binary-transport + subprocess.run(["formatter"], input=payload, text=True, encoding="utf-8") + # ok: la-stack.python.git-stdin-use-shared-helper, la-stack.python.git-stdin-binary-transport + process = subprocess.Popen(["formatter"], stdin=subprocess.PIPE, text=True, encoding="utf-8") + process.communicate(payload) + # Git commands without piped stdin do not need the input helper. + # ok: la-stack.python.git-stdin-use-shared-helper, la-stack.python.git-stdin-binary-transport + subprocess.Popen(["git", "status"], stdout=subprocess.PIPE, text=True) + + +def path_text_writes(path: Path, contents: str, newline_policy: str) -> None: + # The real extracted-notebook writer, before and after explicit LF output. + # ruleid: la-stack.python.text-writes-explicit-policy + path.write_text(contents, encoding="utf-8") + # ok: la-stack.python.text-writes-explicit-policy + path.write_text(contents, encoding="utf-8", newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + path.write_text(contents, newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + path.write_text(contents, encoding=None, newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + path.write_text(contents, encoding="utf-8", newline=None) + # ok: la-stack.python.text-writes-explicit-policy + path.write_text(contents, encoding="utf-8", newline="") + # ok: la-stack.python.text-writes-explicit-policy + path.write_text(contents, encoding="utf-8", newline=newline_policy) + # Nested reads and text transformations are not file writers. + # ok: la-stack.python.text-writes-explicit-policy + path.write_text(contents.strip(), encoding="utf-8", newline="\n") + # ok: la-stack.python.text-writes-explicit-policy + path.write_text(path.read_text(encoding="utf-8"), encoding="utf-8", newline="\n") + # ok: la-stack.python.text-writes-explicit-policy + path.write_text(open(path, encoding="utf-8").read(), encoding="utf-8", newline="\n") + # ok: la-stack.python.text-writes-explicit-policy + path.write_text(path.read_text(encoding=None), encoding="utf-8", newline="\n") + # ok: la-stack.python.text-writes-explicit-policy + path.write_text(open(path, encoding=None, newline=None).read(), encoding="utf-8", newline="\n") + + +def temporary_text_writes(path: Path) -> None: + # Production writers use both positional and keyword mode forms. + # ruleid: la-stack.python.text-writes-explicit-policy + tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) + # ok: la-stack.python.text-writes-explicit-policy + tempfile.NamedTemporaryFile("w", encoding="utf-8", newline="", dir=path.parent, delete=False) + # ruleid: la-stack.python.text-writes-explicit-policy + tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + dir=path.parent, + delete=False, + ) + # ok: la-stack.python.text-writes-explicit-policy + tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + newline="\n", + dir=path.parent, + delete=False, + ) + # ruleid: la-stack.python.text-writes-explicit-policy + named_file(mode="w", encoding="utf-8") + # ok: la-stack.python.text-writes-explicit-policy + named_file(mode="w", encoding="utf-8", newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + named_file(mode="w", encoding="utf-8", newline=None) + # ruleid: la-stack.python.text-writes-explicit-policy + tempfile.TemporaryFile("w+", encoding="utf-8") + # ok: la-stack.python.text-writes-explicit-policy + tempfile.TemporaryFile("w+", encoding="utf-8", newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + tempfile.TemporaryFile(mode="w+", encoding="utf-8") + # ok: la-stack.python.text-writes-explicit-policy + tempfile.TemporaryFile(mode="w+", encoding="utf-8", newline="\n") + + +def spooled_text_writes() -> None: + # SpooledTemporaryFile takes max_size before its positional mode argument. + # ruleid: la-stack.python.text-writes-explicit-policy + tempfile.SpooledTemporaryFile(1024, "w+", encoding="utf-8") + # ruleid: la-stack.python.text-writes-explicit-policy + tempfile.SpooledTemporaryFile(max_size=1024, mode="w+", encoding="utf-8") + # ruleid: la-stack.python.text-writes-explicit-policy + tempfile.SpooledTemporaryFile(1024, "w+", newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + tempfile.SpooledTemporaryFile(max_size=1024, mode="w+", newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + tempfile.SpooledTemporaryFile(1024, "w+", encoding=None, newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + tempfile.SpooledTemporaryFile(max_size=1024, mode="w+", encoding=None, newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + tempfile.SpooledTemporaryFile(1024, "w+", encoding="utf-8", newline=None) + # ruleid: la-stack.python.text-writes-explicit-policy + tempfile.SpooledTemporaryFile(max_size=1024, mode="w+", encoding="utf-8", newline=None) + # ok: la-stack.python.text-writes-explicit-policy + tempfile.SpooledTemporaryFile(1024, "w+", encoding="utf-8", newline="\n") + # ok: la-stack.python.text-writes-explicit-policy + tempfile.SpooledTemporaryFile(max_size=1024, mode="w+", encoding="utf-8", newline="") + + +def other_file_writers(path: Path, descriptor: int) -> None: + # ruleid: la-stack.python.text-writes-explicit-policy + open(path, "w", encoding="utf-8") + # ok: la-stack.python.text-writes-explicit-policy + open(path, "w", encoding="utf-8", newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + open(path, mode="a", encoding="utf-8") + # ok: la-stack.python.text-writes-explicit-policy + open(path, mode="a", encoding="utf-8", newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + path.open("x", encoding="utf-8") + # ok: la-stack.python.text-writes-explicit-policy + path.open("x", encoding="utf-8", newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + path.open(mode="r+", encoding="utf-8") + # ok: la-stack.python.text-writes-explicit-policy + path.open(mode="r+", encoding="utf-8", newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + path.open(mode="r+", encoding=None, newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + io.open(path, "wt", encoding="utf-8") + # ok: la-stack.python.text-writes-explicit-policy + io.open(path, "wt", encoding="utf-8", newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + io.open(path, mode="a+", encoding="utf-8") + # ok: la-stack.python.text-writes-explicit-policy + io.open(path, mode="a+", encoding="utf-8", newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + os.fdopen(descriptor, "w", encoding="utf-8") + # ok: la-stack.python.text-writes-explicit-policy + os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") + # ruleid: la-stack.python.text-writes-explicit-policy + os.fdopen(descriptor, mode="w", encoding="utf-8") + # ok: la-stack.python.text-writes-explicit-policy + os.fdopen(descriptor, mode="w", encoding="utf-8", newline="\n") + + +def byte_writes_and_reads_are_exempt(path: Path, descriptor: int) -> None: + # ok: la-stack.python.text-writes-explicit-policy + path.write_bytes(b"line\r\n") + # ok: la-stack.python.text-writes-explicit-policy + os.fdopen(descriptor, "wb") + # ok: la-stack.python.text-writes-explicit-policy + path.open("wb") + # ok: la-stack.python.text-writes-explicit-policy + open(path, "rb") + # ok: la-stack.python.text-writes-explicit-policy + tempfile.NamedTemporaryFile("w+b") + # ok: la-stack.python.text-writes-explicit-policy + tempfile.NamedTemporaryFile() + # ok: la-stack.python.text-writes-explicit-policy + tempfile.TemporaryFile(mode="wb") + # ok: la-stack.python.text-writes-explicit-policy + tempfile.SpooledTemporaryFile() + # ok: la-stack.python.text-writes-explicit-policy + tempfile.SpooledTemporaryFile(1024, "w+b") + # ok: la-stack.python.text-writes-explicit-policy + tempfile.SpooledTemporaryFile(max_size=1024, mode="w+b") + # ok: la-stack.python.text-writes-explicit-policy + path.open(encoding="utf-8") + # ok: la-stack.python.text-writes-explicit-policy + io.open(path, "r", encoding="utf-8") + # ok: la-stack.python.text-writes-explicit-policy + path.read_text(encoding="utf-8") diff --git a/tests/semgrep/scripts/tests/python_exceptions.py b/tests/semgrep/scripts/tests/python_exceptions.py index 4b598f1..2b5ef3f 100644 --- a/tests/semgrep/scripts/tests/python_exceptions.py +++ b/tests/semgrep/scripts/tests/python_exceptions.py @@ -1,6 +1,10 @@ import subprocess +from typing import TYPE_CHECKING from unittest.mock import MagicMock, Mock +if TYPE_CHECKING: + from pathlib import Path + def catches_broad_exception() -> None: try: @@ -36,6 +40,23 @@ def raises_specific_exception() -> None: raise RuntimeError("specific failure") +def implicit_path_read_text_encoding(path: Path) -> None: + # ruleid: la-stack.python.explicit-path-text-encoding-in-tests + path.read_text() + + +def implicit_path_write_text_encoding(path: Path) -> None: + # ruleid: la-stack.python.explicit-path-text-encoding-in-tests + path.write_text("Time: [1.0, 1.0, 1.0] µs\n") + + +def explicit_path_text_encoding(path: Path) -> None: + # ok: la-stack.python.explicit-path-text-encoding-in-tests + path.read_text(encoding="utf-8") + # ok: la-stack.python.explicit-path-text-encoding-in-tests + path.write_text("Time: [1.0, 1.0, 1.0] µs\n", encoding="utf-8") + + def adhoc_mock_stdout() -> None: # ruleid: la-stack.python.no-adhoc-completedprocess-mock result = Mock() @@ -69,7 +90,7 @@ def direct_subprocess_run() -> None: # ruleid: la-stack.python.no-untyped-defs-in-scripts -def missing_return_annotation(): +def missing_return_annotation(): # noqa: ANN201 - intentional Semgrep violation fixture return None diff --git a/uv.lock b/uv.lock index c9d750a..badb3f5 100644 --- a/uv.lock +++ b/uv.lock @@ -475,7 +475,7 @@ dev = [ { name = "actionlint-py", specifier = "==1.7.12.24" }, { name = "pytest", specifier = "==9.1.1" }, { name = "ruff", specifier = "==0.16.5" }, - { name = "semgrep", specifier = "==1.175.0" }, + { name = "semgrep", specifier = "==1.176.0" }, { name = "shellcheck-py", specifier = "==0.11.0.1" }, { name = "shfmt-py", specifier = "==4.1.0" }, { name = "ty", specifier = "==0.0.77" }, @@ -1056,7 +1056,7 @@ wheels = [ [[package]] name = "semgrep" -version = "1.175.0" +version = "1.176.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1087,15 +1087,15 @@ dependencies = [ { name = "urllib3" }, { name = "wcmatch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/15/69cb61bcad4fa6cb23fae5b7a12a398303266030b5a9127d51a79bfda2aa/semgrep-1.175.0.tar.gz", hash = "sha256:e8b14c91558f765b9dd155a99b0071bfe64f61577cda8eb4964132155232c1af", size = 507685, upload-time = "2026-08-26T17:10:26.526Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/85/89619e12d2d6b7143d412f799f783219de3a5f72d165436ac64f42062acd/semgrep-1.176.0.tar.gz", hash = "sha256:9c0506af2d9a81629907ce7b0cee572c07ef713aa27026a709b8ba7ba2147f34", size = 507672, upload-time = "2026-09-01T19:53:40.073Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/60/77041d572801fb5819f974e821211a298f73d3dc56128bfa2e5045345472/semgrep-1.175.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_10_14_x86_64.whl", hash = "sha256:82b5015b5c215ab0dcada25ccbbc39d57167d09b077e06abbd450958f5c29f78", size = 45854525, upload-time = "2026-08-26T17:11:45.704Z" }, - { url = "https://files.pythonhosted.org/packages/b7/82/1a169ee9977fe875845d2bfa1368f0c2bf8238bd1460801d7fef4facb6e5/semgrep-1.175.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl", hash = "sha256:54ffeeaad2ed9428e9bd40ccf55b2bf7778ce0b59bf848862234aaf6ada2a889", size = 49844382, upload-time = "2026-08-26T17:11:49.422Z" }, - { url = "https://files.pythonhosted.org/packages/30/68/4160f33df10bf7001e198ee296a6c4299eba86c8e8932007cc6f8110b964/semgrep-1.175.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_aarch64.whl", hash = "sha256:c54736be4dd41e269856695685203d676123c2e21145ecd6c26c438b8d1a41ca", size = 72552275, upload-time = "2026-08-26T17:11:53.192Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b5/3941ba37979a5f1744c79ae47139c871bc78d5e612f1b8ac38cb76f49731/semgrep-1.175.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_x86_64.whl", hash = "sha256:ac787bcef4fd4563f8cdd179392c743bded89ea38c0c81ff99913883b6cde5ad", size = 70486294, upload-time = "2026-08-26T17:11:57.144Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9d/42211326bdc625c0f4458190e427f6b5afb96f4299d57ba23a134d7c59a4/semgrep-1.175.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_aarch64.whl", hash = "sha256:6bd6480637aea992cb8d41635329fd4e3bd6dc6e9c124a80341f150c5caeca2d", size = 79456947, upload-time = "2026-08-26T17:12:01.347Z" }, - { url = "https://files.pythonhosted.org/packages/89/cb/23fb070cb9ecb0897f5d1cdf054336a3d2650e5a3920a9cc6d23dcf0e1df/semgrep-1.175.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_x86_64.whl", hash = "sha256:e8ecd7ee8ef1033c9635111c6e162e778834d61416968c5c1c5e7b7fba35c34e", size = 77048247, upload-time = "2026-08-26T17:12:06.03Z" }, - { url = "https://files.pythonhosted.org/packages/7c/e8/28a86e55596e94489aad29efe2d6b56f776ee29afeba63e762ad8f128ace/semgrep-1.175.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-win_amd64.whl", hash = "sha256:68a8d832eff6422c188f5d745e319ce36deccaac6effb77e3833b8930bc10425", size = 58042978, upload-time = "2026-08-26T17:12:10.091Z" }, + { url = "https://files.pythonhosted.org/packages/c2/89/d0276a8ed48c5e1c73c51451eaef2b7fd7fc6b25cda6168e9f99e861a98e/semgrep-1.176.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_10_14_x86_64.whl", hash = "sha256:631f53831a4657703196ec119f48efaaf546c8be192134d1f79cd959c3ee0448", size = 45918119, upload-time = "2026-09-01T19:55:29.562Z" }, + { url = "https://files.pythonhosted.org/packages/4b/71/f60dcbf69aedac0f9211c6717e30c870725b79aa1ed253b5d42de6065a29/semgrep-1.176.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl", hash = "sha256:d930fe18e1a49e8287afff194268ee423a765a2d4a5bcbbd580bdae504e8dd79", size = 49900866, upload-time = "2026-09-01T19:55:33.06Z" }, + { url = "https://files.pythonhosted.org/packages/90/14/79805eade15ae5e3b2b71f5bfba1cc0f062c17e4526959631ed61573f726/semgrep-1.176.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_aarch64.whl", hash = "sha256:ce1b606112aec85460359e556cc8f5ed66d85fe685d2e2632245401a78927eb4", size = 72643084, upload-time = "2026-09-01T19:55:48.713Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5f/dc2f52ef4d8af70931e08ac7756577aeb49674b60cc1adc172dbe7031020/semgrep-1.176.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_x86_64.whl", hash = "sha256:9d823543758b3c44916648b9dbffd253e883e3ae5d7500b19e1aa6cb31c0c457", size = 70575745, upload-time = "2026-09-01T19:55:53.397Z" }, + { url = "https://files.pythonhosted.org/packages/9b/99/251463bf6cab0dd5b46f5325110db970f7b7fcfe56398ed48e043f1bb059/semgrep-1.176.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_aarch64.whl", hash = "sha256:4ffd032cd7c337f316e8a6a6fde6c7110a674577cb0f7996db4cabe03eadfde5", size = 79569854, upload-time = "2026-09-01T19:55:58.518Z" }, + { url = "https://files.pythonhosted.org/packages/56/9c/260e263de67b26b19fb5e54581edf5e898a3387cfac823f41b42f8c6d6b3/semgrep-1.176.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_x86_64.whl", hash = "sha256:85de63294e7a7af8a016c906784c2f2849867df20dd5ea927bc58e5a97025302", size = 77155991, upload-time = "2026-09-01T19:56:04.495Z" }, + { url = "https://files.pythonhosted.org/packages/06/76/715a3125c39a98c4bb3143f1c6a4e1bc928533aed8b391c933fda85ae1a7/semgrep-1.176.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-win_amd64.whl", hash = "sha256:cbcb1b4ed7d98ced18cb211afd6b7a9b3437683181c4b00b19ebfe091e87bcba", size = 58117151, upload-time = "2026-09-01T19:56:10.447Z" }, ] [[package]]