utils/dep_freshness/parsers/pubspec.py
Krzysztof kuhy Rudnicki 2e1f3da6cf
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
Point a pubspec finding at the dependency, not the config block of the same name
Every Flutter app here declares `flutter_launcher_icons` twice: as a dev
dependency, and as a TOP-LEVEL configuration block for the same tool. The line
lookup broke ties by indentation alone, so the top-level block won and the
gate told you to fix a line that carries no version at all -- lyricanki
reported `pubspec.yaml:64` while the caret being complained about is at 48.

The pubspec parser now says which top-level keys may hold a version, and a key
found outside them is only a fallback. `environment` is in that list on
purpose: without it the `sdk:` inside a `flutter: {sdk: flutter}` block would
outrank the SDK constraint it is meant to find.

Only the pubspec parser passes the hint. JSON and TOML indent their section
headers differently, so they keep the old behaviour unchanged.

310 tests, 100% branch coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
2026-08-28 14:33:11 +02:00

110 lines
3.9 KiB
Python

"""`pubspec.yaml` (+ `pubspec.lock`) -> Dart/Flutter dependencies.
Three shapes live in the same map: a plain constraint (`http: ^1.6.0`), an SDK
package (`flutter: {sdk: flutter}`) and a git-tag dependency on `kuhyx/utils`,
whose "latest" is a tag rather than a registry version. `dependency_overrides`
is parsed too and always reported: an override is an unpinned dependency
wearing a disguise.
"""
from __future__ import annotations
from pathlib import Path
import yaml
from dep_freshness._tables import (
GITTAG,
PUB,
PUB_CARET_ALLOWED,
PUB_OVERRIDE_KEY,
PUB_SDK_PACKAGES,
TOOLCHAIN,
)
from dep_freshness.models import Dep
from dep_freshness.parsers._lines import index
from dep_freshness.versions import exact_pin
_SECTIONS = (("dependencies", False), ("dev_dependencies", True))
# Where a version may legitimately be declared. Everything else at top level is
# configuration -- and a Flutter app has a `flutter_launcher_icons:` config
# block sharing its name with the dev dependency, which without this hint wins
# the line lookup by being less indented and sends the fix to a line that
# carries no version at all.
_LINE_SECTIONS = (
"environment", "dependencies", "dev_dependencies", PUB_OVERRIDE_KEY,
)
def _load(path: Path) -> dict:
try:
data = yaml.safe_load(path.read_text(encoding="utf-8"))
except (OSError, yaml.YAMLError):
return {}
return data if isinstance(data, dict) else {}
def locked_versions(lock: Path) -> dict[str, str]:
"""`package -> resolved version` from a committed `pubspec.lock`.
A missing lock means "library, check the manifest alone" (four of the nine
shared libs gitignore theirs) — never a violation.
"""
data = _load(lock)
out: dict[str, str] = {}
for name, entry in (data.get("packages") or {}).items():
if isinstance(entry, dict) and entry.get("version"):
out[str(name)] = str(entry["version"])
return out
def _git_ref(spec: dict) -> str | None:
git = spec.get("git")
if isinstance(git, dict):
return str(git.get("ref") or "") or None
return None
def parse(path: Path) -> list[Dep]:
data = _load(path)
if not data:
return []
lines = index(path, _LINE_SECTIONS)
locks = locked_versions(path.with_name("pubspec.lock"))
deps: list[Dep] = []
sdk = ((data.get("environment") or {}) or {}).get("sdk")
if isinstance(sdk, str):
deps.append(Dep(
ecosystem=TOOLCHAIN, name="dart", constraint=sdk, path=path,
line=lines.get("sdk", 0), pinned=exact_pin(sdk), caret_ok=True,
))
sections = [*_SECTIONS, (PUB_OVERRIDE_KEY, False)]
for section, is_dev in sections:
for name, spec in (data.get(section) or {}).items():
name = str(name)
override = section == PUB_OVERRIDE_KEY
if isinstance(spec, dict) and (ref := _git_ref(spec)):
package, _, version = ref.rpartition("-v")
deps.append(Dep(
ecosystem=GITTAG, name=package or name, constraint=ref,
path=path, line=lines.get(name, 0), pinned=version or None,
dev=is_dev, override=override,
))
continue
if name in PUB_SDK_PACKAGES or (
isinstance(spec, dict) and "sdk" in spec
):
continue # ships with the SDK; no registry version exists
if isinstance(spec, dict):
continue # path/hosted dep: nothing comparable to a registry
constraint = "" if spec is None else str(spec)
deps.append(Dep(
ecosystem=PUB, name=name, constraint=constraint, path=path,
line=lines.get(name, 0), pinned=exact_pin(constraint),
locked=locks.get(name), dev=is_dev,
caret_ok=name in PUB_CARET_ALLOWED, override=override,
))
return deps