Skip to content

Fix walrus-binding, venv-pruning, fix-scope, scope-notice, and tuple-context bugs; seed the bundled typeshed pin#347

Merged
abdushakoor12 merged 12 commits into
mainfrom
fix/checker-bugs
Jul 24, 2026
Merged

Fix walrus-binding, venv-pruning, fix-scope, scope-notice, and tuple-context bugs; seed the bundled typeshed pin#347
abdushakoor12 merged 12 commits into
mainfrom
fix/checker-bugs

Conversation

@abdushakoor12

Copy link
Copy Markdown
Collaborator

TLDR

Fixes six reported bugs — four checker/CLI false-positive and file-safety defects, one silent-clean-run reporting gap, and the LSP config seed leaving fresh workspaces UNPINNED — with no conformance or benchmark movement (141/141, 0 FP).

Refs #333, Refs #334, Refs #337, Refs #339, Refs #341, Refs #343

What Was Added?

What Was Changed or Deleted?

  • basilisk fix default paths (crates/basilisk-cli/src/main.rs) — dropped the clap default_value = "." so a bare fix routes through effective_check_paths and touches exactly the configured [tool.basilisk] include roots that check reads, instead of walking (and rewriting) the entire working directory (Refs basilisk fix defaults to '.' and ignores include/exclude — traverses venv/ and hangs, while mutating files #333). Explicit CLI paths still win.
  • LSP config seeding (crates/basilisk-lsp/src/config_seed.rs) — the one-time strict-by-default seed now also stamps typeshed-commit = "<bundled sha>" (via bundled_commit_sha()) through the same validated build_configuration_patch transaction, so a freshly-opened workspace is pinned and reproducible — never UNPINNED — with no network access (Refs LSP config seeding never stamps typeshed-commit, leaving freshly-opened workspaces UNPINNED #343). One-time and never-resurrect guarantees preserved; the CLI still never seeds.
  • Generated conformance stamps regenerated at python/typing@39164cd (score unchanged); mutation baseline date refreshed after a green 144-mutant, 0-missed run.

How Do The Automated Tests Prove It Works?

Spec / Doc Changes

  • [LSPARCH-CONFIG-SEEDING] (LSP-ARCHITECTURE-SPEC) — seed now documented as rule tag + bundled typeshed pin, with the no-conflict and never-resurrect reasoning.
  • [CHKARCH-CLI-SCOPE-NOTICE] added and [CHKARCH-CONFIG-EXCLUDE] extended (CHECKER-ARCHITECTURE-SPEC) for the scope notice and structural venv pruning.
  • [TYPEINF-SPECIAL-LITERAL-CONTEXT] (CHECKER-TYPE-INFERENCE-SPEC) — tuple element positions added to literal-context propagation.
  • website/src/docs/configuration.md — documents the check/analyze rule-scope split next to rule-tags.
  • Generated READMEs/conformance report restamped at python/typing@39164cd.

Breaking Changes

  • None — basilisk fix's bare-invocation default narrows from . to the configured include roots (the documented, check-consistent behaviour), and text-mode check may print one additional advisory line; JSON output and exit codes are unchanged.

`literal_collection_assignable_to` ([TYPEINF-SPECIAL-LITERAL-CONTEXT])
had arms for Union/Optional/List/Set/Dict but none for Tuple, so a
declared `tuple[list[int], str]` fell through to `_ => None`. The caller
then fell back to the invariant `is_assignable_to`, where the bottom-up
`list[Never]` synthesised for `[]` fails invariance against `list[int]`
— a false `returns_compatibility` on `return [], "lit"`.

Add the Tuple arm plus a `tuple_literal_assignable_to` helper that
carries the declared element type inward per position, reusing the
existing `homogeneous_tuple_elem` / `is_unpacked_tuple_elem` shape
decomposition rather than re-deriving it. PEP 646 unpacked segments and
arity mismatches still return `None`, leaving the invariant fallback
untouched.

This closes the asymmetry with the assignment path, whose gate already
included `RhsKind::Tuple` — `x: tuple[list[int], str] = ([], "a")` was
accepted while the identical `return` was rejected.

Conformance is unchanged at 141/141, 0 missed, 0 false positives.

Refs #337
`basilisk check` filters every analyze-scope diagnostic at the CLI edge
([CHKARCH-COMMANDS]), so a project that grades non-`pep` rule tags `error`
could see "All checked. No issues found." while none of those rules were
ever evaluated. A silent clean run was indistinguishable from a clean
project — the same class of failure as a skipped CI gate reporting success.
One reported repo hid 66 configured errors this way for the life of its
pipeline.

A text-mode `check` now closes with one line naming how many rules the
configuration selected that this command does not run, and pointing at
`basilisk analyze` ([CHKARCH-CLI-SCOPE-NOTICE]).

- `analyze_selected_rules` is the counterpart to the adjacent
  `pep_disable_violations`: the non-`pep` rules a config resolves to a
  non-disabled severity. It keeps the bare-tree fast path used by
  `EffectiveRuleConfig::severity`, so conformance and benchmark runs never
  walk the rule catalog.
- The pipeline counts them across the base config and every per-directory
  config, so one file's config never speaks for the whole run. The union
  walk it shares with `pep_disable_config_error` is now a single
  `codes_across_configs` helper rather than two copies.
- The notice is a fact about the project, not boilerplate: a tree that
  selects no analyze-scope rule prints nothing extra, and `analyze` — which
  just ran them — never prints it.

Exit codes and the JSON contract are unchanged; machine consumers see no
new field. The issue's other two parts: `basilisk --help` already lists
`analyze` on this branch, and the check/analyze split is now documented
next to `rule-tags` in the configuration reference.

Conformance is unchanged at 141/141, 0 missed, 0 false positives.

Refs #334
`basilisk fix` declared `PATHS` with `#[arg(default_value = ".")]`, so
`paths` was never empty and `collect_and_fix` skipped
`effective_check_paths` — the [CHKARCH-CONFIG-INCLUDE] fallback that
`check`/`analyze` run. A no-args `fix` therefore walked the entire working
directory instead of `[tool.basilisk] include`.

`fix` mutates files, so that default is materially riskier than the same
default on a read-only command: in a project with a checked-out virtualenv
it rewrote third-party sources under `venv/site-packages` (and walked ~10k
files, appearing to hang). Drop the clap default and route the paths through
`effective_check_paths`, so explicit CLI paths still win and a bare
`basilisk fix` touches exactly what `basilisk check` reads.

Refs #333
Virtualenvs were skipped only because `venv`/`.venv`/`env`/`site-packages`
are literal entries in `DEFAULT_EXCLUDES`. Per [CHKARCH-CONFIG-EXCLUDE],
configuring `exclude` *replaces* that list wholesale, so any project that
sets its own patterns silently lost all venv protection — and `fix` mutates
files, so it rewrote installed third-party packages.

Add `basilisk_config::is_virtualenv_dir`, keyed on PEP 405's `pyvenv.cfg`
rather than on directory names, so `env/` and `.direnv/python-3.13/` are
recognised alongside `.venv/`. Both walkers call it at the exact point they
already skip hidden directories — the CLI walk behind
`check`/`fix`/`format`/`adopt`/`stubs`, and the LSP workspace scan — so the
editor and CLI still agree file-for-file.

The skip prunes traversal *into* a venv and never overrides an explicit
request: `basilisk check venv/lib/.../dep` still reports, mirroring the
walk's existing depth-0 root exemption.

Refs #341
`names_undefined` decided a return-name was defined by consulting
`FunctionInfo::all_local_assigns`, built by matching statement-shaped
binding forms — `Assign`, `AnnAssign`, `For`, `With`, imports, nested
`def`/`class`. A PEP 572 assignment expression binds from inside an
*expression* (`if item := prices.get(asset):` binds `item` from the `if`
test), so no statement match ever saw it and the rule reported valid code
as undefined.

A new `visitor::walrus` collects `Expr::Named` targets at two reaches.
`Any` feeds `all_local_assigns`: the target is bound somewhere in the
body. `Definite` feeds `unconditional_assigns`: only what Python must
evaluate once control reaches a statement, so branch bodies and
short-circuiting operands are pruned while the first operand of `and`/`or`,
a ternary's test, and a comprehension's outermost iterable are kept. Both
skip nested `def`/`class`/`lambda` scopes and both descend into
comprehensions, which PEP 572 exempts from the comprehension's own scope.

Without the `Definite` half the fix would merely trade `names_undefined`
for a fresh `names_unbound` false positive on `if hit := d.get(k): ...`
followed by a top-level `return hit`.

Conformance stays 141/141; `scope_tree` picks up walrus locals for
rename and scope-aware reference finding for free.

Refs #339
# Conflicts:
#	crates/basilisk-config/src/lib.rs
Opening an unconfigured workspace seeded only the strict-by-default rule
tag, so a freshly-opened repo sat permanently UNPINNED
([STUBRES-TYPESHED-WARN]) until the user separately ran
`basilisk typeshed download` — not reproducible out of the box.

`write_seed` (`crates/basilisk-lsp/src/config_seed.rs`) now builds a full
`ConfigurationUpdate` through the same validated, structure-preserving
editor transaction, stamping `typeshed-commit = bundled_commit_sha()`
alongside `[tool.basilisk.rule-tags] basilisk = "error"`. The bundled
commit is complete inside the binary, so the pin needs no network access
and cannot produce a NO SOURCE state; it suppresses UNPINNED without
changing which stubs are resolved. The seed only runs when the ancestor
walk found no `[tool.basilisk]` table, so no `typeshed-path` can exist
for the pin to conflict with.

One-time and never-resurrect guarantees are untouched: any existing
table still blocks the seed entirely, and a deleted pin is never
resurrected. The CLI still never seeds. [LSPARCH-CONFIG-SEEDING] is
updated to describe the stamped pin.

Refs #343
Regenerated by scripts/gen_conformance_reference.py during the local
scorer run: 141/141 (100%), 0 false positives, unchanged — only the
graded python/typing commit advanced.
abdushakoor12 and others added 2 commits July 23, 2026 12:20
Any patch that had to create the `tool` path rendered an explicit empty
`[tool]` header (and a rule-only seed also a bare `[tool.basilisk]`),
because `child_table_mut` inserted `Table::new()`, which toml_edit
renders explicitly. Tables the parser creates for dotted headers are
implicit, so a hand-written file never shows them — the writer now marks
the tables it creates the same way ([CONFIGEDITOR-SOURCES]).

toml_edit renders an implicit table the moment it holds direct entries,
so mutation targets are unaffected, and tables already present in the
document keep their explicit headers — the never-prune guarantee is
untouched (`removing_last_entry_keeps_empty_table` still passes).

The seed now renders exactly the shape #343 specified:

    [tool.basilisk]
    typeshed-commit = "<sha>"

    [tool.basilisk.rule-tags]
    basilisk = "error"

Refs #343
@abdushakoor12
abdushakoor12 merged commit 23b6e82 into main Jul 24, 2026
24 checks passed
@abdushakoor12
abdushakoor12 deleted the fix/checker-bugs branch July 24, 2026 06:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Empty container literal inside a returned tuple ignores the declared return type — tuple[list[Never], ...] false positive

2 participants