diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60f30066..cb84d395 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,10 @@ jobs: # `website` gates the site build: only files the published site is # actually built from (templates/assets + the benchmark CSVs it reads). case "$f" in - website/*|benchmarks/status/*) website=true ;; + # The site build, plus the checker rule sources the diagnostic + # data (website/src/_data/rules.json) and /errors/ pages are + # generated from — so a new/renamed rule re-runs the drift guard. + website/*|benchmarks/status/*|crates/basilisk-checker/src/rules/*) website=true ;; esac # `code` gates the heavy Rust/extension/mutation matrix. Everything # that never reaches the Rust toolchain is excluded: the whole site, @@ -113,6 +116,15 @@ jobs: working-directory: website run: npm ci + # The /errors/ pages and the rules reference are generated from the checker + # source ([WEBSITE-ERROR-PAGES]); fail if the committed data is stale so the + # pages the CLI deep-links to can never drift from the diagnostics it emits. + - name: Check generated diagnostic data is in sync with the checker + run: | + python3 scripts/gen_rules_reference.py --data /tmp/rules.json + diff -u website/src/_data/rules.json /tmp/rules.json \ + || { echo "::error::rules.json is stale — run: python3 scripts/gen_rules_reference.py --data"; exit 1; } + - name: Build site working-directory: website # GITHUB_TOKEN raises the GitHub API rate limit for _data/releases.js @@ -122,13 +134,16 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: npm run build - # Navigation smoke tests ([WEBSITE-E2E-SMOKE]). Both presets (Desktop - # Chrome + Pixel 5) run on Chromium, so only chromium is installed. + # Navigation smoke tests ([WEBSITE-E2E-SMOKE]) and CLI-screenshot render + # checks ([WEBSITE-SCREENSHOTS-VERIFY]). Both presets (Desktop Chrome + + # Pixel 5) run on Chromium, so only chromium is installed. The screenshots + # are committed, regenerated locally with `npm run screenshots` against the + # real binary — CI only verifies they render, it never captures them. - name: Install Playwright browser working-directory: website run: npx playwright install --with-deps chromium - - name: Run navigation smoke tests (desktop + mobile) + - name: Run navigation + screenshot smoke tests (desktop + mobile) working-directory: website # CI uses the stdout `list` reporter only — no HTML report, trace, # video or screenshot is produced or uploaded ([GITHUB-NO-ARTIFACTS]). diff --git a/CLAUDE.md b/CLAUDE.md index 0e051c5f..ee35b98d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -196,17 +196,39 @@ Please register before starting work. ## Generating CLI Screenshots (real `basilisk check` output) -Marketing/doc screenshots of CLI output must be **real screen captures of the actual binary**, never hand-typed code fences or synthetic renders (those drift and are usually inaccurate). Canonical location: `website/src/assets/images/` (referenced as `/assets/images/.png`). Rule screenshots are named after the code (`e0001.png` … `e0025.png`); the homepage demo pair is `cli-demo.png` (errors) + `cli-clean.png` (pass). +Marketing/doc screenshots of CLI output must be **real output of the actual binary**, never hand-typed code fences or synthetic renders (those drift and are usually inaccurate). Canonical location: `website/src/assets/images/` (referenced as `/assets/images/.png`). Rule screenshots are named after the code (`e0001.png` … `e0025.png`); the homepage demo pair is `cli-demo.png` (errors) + `cli-clean.png` (pass). -Process (macOS, Terminal.app + `screencapture` + ImageMagick): +These are now generated **automatically** — no manual Terminal.app / `screencapture` / ImageMagick. From `website/`: -1. **Verify the example first.** Many rule examples do NOT trigger the rule they claim — always run `basilisk check` on the snippet and confirm the *exact* target code appears before screenshotting. E.g. E0003/E0005 fire on empty collections (`data = []`), not plain literals; E0014 needs a single annotated assignment (`count: int = "zero"`); E0016 needs `@override` (else it's E0025); E0018 fires on an undefined name in a `return`. Craft minimal snippets that isolate the target code. -2. **No PII.** Run from a neutrally-named dir (`/tmp/basilisk-demo`, so the title bar reads "basilisk-demo", not the home-dir/username) and set a clean prompt (`export PS1='$ '`) so no username/host appears. Reference relative filenames so diagnostic paths stay clean (`e0001.py:1:13`). -3. **Drive Terminal deterministically.** Open an empty window, size it (`set number of columns/rows`), then run `cd /tmp/basilisk-demo; export PS1='$ '; clear` followed by `basilisk check .py` *in that window*. Resize BEFORE typing (resizing mid-type corrupts the line). -4. **Capture the exact window by CGWindowID** (not a screen region — overlapping windows contaminate region captures). Get the frontmost Terminal window id via JXA/CoreGraphics (`CGWindowListCopyWindowInfo`, first owner=="Terminal" layer 0), then `screencapture -x -o -l`. Close stale Terminal windows first so the wrong one isn't picked. -5. **Crop** with ImageMagick — flatten the rounded-corner alpha onto the terminal background, then trim: `magick raw.png -background 'srgb(30,30,30)' -alpha remove -alpha off -fuzz 6% -trim +repage out.png`. +```bash +npm run screenshots # regenerate every image +node screenshots/generate.mjs e0001 e0012 # a subset by name +BASILISK_BIN=../target/release/basilisk npm run screenshots # pin the binary +``` + +`screenshots/generate.mjs` runs the real `basilisk check --color always` on each snippet in `screenshots/shots.mjs` (in a throwaway, neutrally-named temp dir so paths read `e0001.py:1:13` with no PII), **asserts the documented diagnostic actually fires** (the snippet→code pairing lives in the manifest, so a checker change can't silently ship a misleading image), then renders the genuine coloured output in a faithful macOS Terminal window via Playwright. See `[WEBSITE-SCREENSHOTS]` (`docs/specs/WEBSITE-SCREENSHOTS-SPEC.md`). + +To add/change a screenshot, edit `screenshots/shots.mjs` (snippet + expected code) and rerun — never craft images by hand. The committed PNGs are regenerated locally when CLI output changes; CI only verifies they render (`website/tests/e2e/screenshots.spec.ts`, `[WEBSITE-SCREENSHOTS-VERIFY]`), never captures, per `[GITHUB-NO-ARTIFACTS]`. After regenerating, rebuild (`npm run build`) and confirm the images copy to `_site/assets/images/`. VSIX integration tests capture their own editor screenshots to the gitignored `vscode-extension/.screenshots/` (never committed, never a CI artifact). + +## Per-diagnostic error pages (`/errors/BSK-XXXX/`) + +Every diagnostic the CLI prints ends with `see: https://www.basilisk-python.dev/errors/BSK-XXXX` (the `docs_url` on each rule's `ErrorCode`). Those pages are **generated for all codes** from the checker source — see `[WEBSITE-ERROR-PAGES]` (`docs/specs/WEBSITE-ERROR-PAGES-SPEC.md`). The single source is `website/src/_data/rules.json`, produced by: + +```bash +python3 scripts/gen_rules_reference.py --data # writes website/src/_data/rules.json +``` + +It extracts the `//! BSK-XXXX:` summary + doc-comment body (prose and ```python examples) from each `crates/basilisk-checker/src/rules/*.rs`. **After adding or renaming a rule, rerun it** — CI fails otherwise: the website job regenerates and `diff`s `rules.json` (`[WEBSITE-ERROR-PAGES-DRIFT]`), and rule-source edits are classified as website changes so the guard runs. The same data drives the `/docs/rules/` table and counts (no hand-maintained code lists). Pages render via `website/src/errors/error.njk`; a worked-example screenshot appears automatically for any code present in `screenshots/shots.mjs`. + +## VS Code editor screenshots (`vscode-*.png`) + +Real screenshots of the extension running in VS Code (diagnostics, hover, quick-fix, activity panel) are captured automatically — see `[VSIX-EDITOR-SCREENSHOTS]` (`docs/specs/VSIX-EDITOR-SCREENSHOTS-SPEC.md`). From `vscode-extension/`, after `cargo build -p basilisk-cli -p basilisk-profiler-helper`: + +```bash +npm run screenshots:editor +``` -After generating, rebuild (`cd website && npm run build`) and confirm the images copy to `_site/assets/images/` and render. VSIX integration tests capture their own editor screenshots to the gitignored `vscode-extension/.screenshots/` (never committed, never a CI artifact — see [GITHUB-NO-ARTIFACTS]). +This stages the binary into the dev extension, copies `shipwright.json` (gitignored dev artifacts), launches the **headed** "Editor screenshots" suite with `BASILISK_SCREENSHOTS=1`, and a dependency-free CDP sidecar (`screenshot-watcher.mjs`, Node's built-in WebSocket — no Playwright) captures the window to `website/src/assets/images/vscode-*.png`. The suite is a no-op without the env flag, so normal `npm test` never opens these windows. To add one, add a `test(...)` that makes the feature visible and calls `takeWindowScreenshot(...)`. As with the CLI shots, PNGs are committed and regenerated locally; CI only verifies they render (`website/tests/e2e/screenshots.spec.ts`), per `[GITHUB-NO-ARTIFACTS]`. ## Architecture diff --git a/basilisk-zed/README.md b/basilisk-zed/README.md index 404cd6d3..b2e2ab0e 100644 --- a/basilisk-zed/README.md +++ b/basilisk-zed/README.md @@ -5,7 +5,7 @@ Zed editor extension for Basilisk — WASM-based Python type checking and language server integration.

- Basilisk in action — type checking, diagnostics, and refactoring in the editor + Basilisk in the Zed editor — Python type checking and diagnostics inline

## Role in Basilisk diff --git a/basilisk-zed/README.zh.md b/basilisk-zed/README.zh.md index 814199e8..f6cfbab2 100644 --- a/basilisk-zed/README.zh.md +++ b/basilisk-zed/README.zh.md @@ -7,7 +7,7 @@ Basilisk 的 Zed 编辑器扩展 —— 基于 WASM 的 Python 类型检查与语言服务器集成。

- Basilisk in action — type checking, diagnostics, and refactoring in the editor + Zed 编辑器中的 Basilisk —— 行内 Python 类型检查与诊断

## 在 Basilisk 中的角色 diff --git a/basilisk-zed/images/screenshot.png b/basilisk-zed/images/screenshot.png deleted file mode 120000 index bf34ce22..00000000 --- a/basilisk-zed/images/screenshot.png +++ /dev/null @@ -1 +0,0 @@ -../../website/src/assets/images/screenshot.png \ No newline at end of file diff --git a/basilisk-zed/images/zed-screenshot.png b/basilisk-zed/images/zed-screenshot.png new file mode 120000 index 00000000..5466c335 --- /dev/null +++ b/basilisk-zed/images/zed-screenshot.png @@ -0,0 +1 @@ +../../website/src/assets/images/zed-screenshot.png \ No newline at end of file diff --git a/docs/INDEX.md b/docs/INDEX.md index 2a65803e..0db2a92f 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -34,6 +34,9 @@ Specifications define the target behavior and architecture. They are the source | [LSP-TEST-INTEGRATION-SPEC.md](specs/LSP-TEST-INTEGRATION-SPEC.md) | Test discovery, execution, and editor integration — pytest/unittest, TestItem model, coverage overlay. | | [EXTENSION-ACTIVITY-PANEL-SPEC.md](specs/EXTENSION-ACTIVITY-PANEL-SPEC.md) | Cross-editor activity panel — module explorer, type health, feature dashboard (VS Code, Zed, Neovim). | | [WEBSITE-E2E-SPEC.md](specs/WEBSITE-E2E-SPEC.md) | Website navigation/e2e smoke tests (Playwright, desktop + mobile) — top-nav resolution, docs sidebar, and the mobile docs-submenu reachability guard. | +| [WEBSITE-SCREENSHOTS-SPEC.md](specs/WEBSITE-SCREENSHOTS-SPEC.md) | Automated CLI screenshots — `npm run screenshots` runs the real binary on each documented snippet, self-verifies the diagnostic fires, and renders it in a faithful Terminal window (no manual screencapture). | +| [WEBSITE-ERROR-PAGES-SPEC.md](specs/WEBSITE-ERROR-PAGES-SPEC.md) | Per-diagnostic `/errors/BSK-XXXX/` pages generated from the checker source so every CLI `see:` link resolves — with severity, explanation, worked example, drift guard, and render verification. | +| [VSIX-EDITOR-SCREENSHOTS-SPEC.md](specs/VSIX-EDITOR-SCREENSHOTS-SPEC.md) | Automated real VS Code editor screenshots (`npm run screenshots:editor`) — drives the extension headed, captures the window over CDP (no Playwright dep), embeds diagnostics/hover/quick-fix/activity-panel on the docs. | ## Plans diff --git a/docs/specs/VSIX-EDITOR-SCREENSHOTS-SPEC.md b/docs/specs/VSIX-EDITOR-SCREENSHOTS-SPEC.md new file mode 100644 index 00000000..c1fc6bcd --- /dev/null +++ b/docs/specs/VSIX-EDITOR-SCREENSHOTS-SPEC.md @@ -0,0 +1,80 @@ +# VS Code Editor Screenshots {#VSIX-EDITOR-SCREENSHOTS} + +**Version**: 0.1.0 +**Status**: Active +**License**: MIT + +--- + +## Purpose {#VSIX-EDITOR-SCREENSHOTS-PURPOSE} + +The website needs **real screenshots of the Basilisk extension running in VS Code** +— diagnostics with squiggles, the hover type popup, the Quick Fix menu, the +activity panel — not mockups. This spec defines an automated pipeline that drives +the actual extension in a headed VS Code instance and captures the real window, +so the marketing/docs editor screenshots are genuine and reproducible. + +It is the editor-side companion to the CLI pipeline ([WEBSITE-SCREENSHOTS]): both +produce committed PNGs from the real product and are verified (not captured) in +CI, honouring [GITHUB-NO-ARTIFACTS]. + +## Capture pipeline {#VSIX-EDITOR-SCREENSHOTS-PIPELINE} + +Prerequisite — build the binaries the dev extension resolves: + +```bash +cargo build -p basilisk-cli -p basilisk-profiler-helper +``` + +Then, from `vscode-extension/`: + +```bash +npm run screenshots:editor +``` + +`scripts/capture-screenshots.mjs` orchestrates one run: + +1. Stages the built binaries into `vscode-extension/bin//` via + `stage-runtime.mjs` (the same path the packaged VSIX and the extension's + shipwright resolver use) and mirrors the repo-root `shipwright.json` into the + extension (both are gitignored dev artifacts). +2. Compiles the extension and launches the **"Editor screenshots"** suite + (`src/test/suite/screenshots-capture.test.ts`) headed, with + `BASILISK_SCREENSHOTS=1`. The harness (`.vscode-test.mjs`) then adds + `--remote-debugging-port` so the window is reachable over CDP. +3. Runs the sidecar `scripts/screenshot-watcher.mjs`, which speaks the + Chrome DevTools Protocol over Node's **built-in WebSocket** (no Playwright + dependency, no browser download), forces a uniform Retina viewport + (1440×900 @2×), and captures the workbench page on demand. + +Each test drives a feature until it is visible, strips transient chrome +(`notifications.clearAll`, close the Chat auxiliary bar), then calls +`takeWindowScreenshot(name)` (`screenshot.ts`), which hands the sidecar a +`.signal` file and waits for the PNG. Output lands in +`website/src/assets/images/vscode-*.png`. + +The suite is a **no-op unless `BASILISK_SCREENSHOTS=1`** (it `skip()`s in +suiteSetup), so a normal `npm test` run never opens these windows or writes into +the repo. + +## Captured set {#VSIX-EDITOR-SCREENSHOTS-SET} + +| Image | Feature | Embedded on | +|---|---|---| +| `vscode-diagnostics.png` | Inline squiggles + Problems panel | `/docs/install-vscode/` | +| `vscode-hover.png` | Hover type popup | `/docs/quick-start/` | +| `vscode-quickfix.png` | Quick Fix / code-action menu | `/docs/refactoring/` | +| `vscode-module-explorer.png` | Basilisk activity panel | `/docs/` | + +Add a capture by adding a `test(...)` to the suite that makes the feature visible +and calls `takeWindowScreenshot('vscode-.png')`. + +## Verification {#VSIX-EDITOR-SCREENSHOTS-VERIFY} + +The committed PNGs are regenerated locally (the capture needs a headed VS Code and +the built binary — it does not run in CI). CI only verifies they render: +`website/tests/e2e/screenshots.spec.ts` visits each embedding page and asserts the +`vscode-*.png` is present and decodes to non-zero pixels — so a missing or +zero-byte capture fails the build. No screenshot is ever produced or uploaded by +CI ([GITHUB-NO-ARTIFACTS]); the legacy gitignored full-screen capture in +`screenshot.ts` (`captureScreenshot`) remains for local debugging only. diff --git a/docs/specs/WEBSITE-ERROR-PAGES-SPEC.md b/docs/specs/WEBSITE-ERROR-PAGES-SPEC.md new file mode 100644 index 00000000..f7671daa --- /dev/null +++ b/docs/specs/WEBSITE-ERROR-PAGES-SPEC.md @@ -0,0 +1,73 @@ +# Website: Per-Diagnostic Error Pages {#WEBSITE-ERROR-PAGES} + +**Version**: 0.1.0 +**Status**: Active +**License**: MIT + +--- + +## Purpose {#WEBSITE-ERROR-PAGES-PURPOSE} + +Every diagnostic Basilisk reports ends with a deep link: + +``` + = see: https://www.basilisk-python.dev/errors/BSK-E0001 +``` + +That URL is baked into each rule (`docs_url` on the `ErrorCode` in +`crates/basilisk-checker/src/rules/*.rs`). Before this spec **none of those pages +existed** — every "learn more" link the CLI printed (155 codes) was a 404. + +This spec defines a generated landing page for **every** diagnostic code at +`/errors/BSK-XXXX/`, built from the checker source so the pages can never drift +from the diagnostics the binary emits. It turns the tool's own output into a +navigable, explained reference. + +## Data generation {#WEBSITE-ERROR-PAGES-DATA} + +`scripts/gen_rules_reference.py --data` extracts one record per code from the +`//! BSK-XXXX: …` doc-comment header — and the prose/`​```python` examples beneath +it — on each rule module, and writes `website/src/_data/rules.json`: + +```json +{ "code", "severity", "summary", "summaryHtml", "body": [{type:"text"|"code"}], "group", "docsUrl" } +``` + +`docsUrl` is read from the rule's own `docs_url` literal, so the page URL and the +CLI link are guaranteed identical. The same data drives the complete reference +table on `/docs/rules/` and the headline counts (`_data/ruleStats.js`, +`_data/ruleGroups.js`), so the prose, the table, and the pages share one source. + +### Drift guard {#WEBSITE-ERROR-PAGES-DRIFT} + +The CI website job regenerates the data and `diff`s it against the committed +`rules.json`, failing if they differ; the change-classifier treats edits under +`crates/basilisk-checker/src/rules/` as website changes so adding or renaming a +rule re-runs the guard. A new rule therefore cannot ship without its page. + +## Pages {#WEBSITE-ERROR-PAGES-PAGES} + +`website/src/errors/error.njk` paginates `rules` (size 1) to emit +`/errors/{{ code }}/` for every record. Each page shows the code, a severity +badge, the summary, the doc-comment body (text + code blocks), a worked +`basilisk check` screenshot when one exists, how-to-handle guidance, and the +canonical `docsUrl`. `website/src/errors/index.njk` is a grouped, browsable +directory of all codes. Pages deliberately omit `eleventyNavigation` so the 160 +entries never flood the docs sidebar. + +### Worked examples {#WEBSITE-ERROR-PAGES-EXAMPLES} + +`_data/examples.js` maps a code to the screenshot that demonstrates it by reading +the screenshot manifest's `expect` field ([WEBSITE-SCREENSHOTS-MANIFEST]) — so the +mapping is correct even where the image stem and code differ (e.g. `e0011.png` +demonstrates `BSK-W0014`). Adding a verified shot to `screenshots/shots.mjs` and +regenerating is all it takes for a code's page to gain a real-output example. + +## Verification {#WEBSITE-ERROR-PAGES-VERIFY} + +`website/tests/e2e/errors.spec.ts` (same Playwright config as [WEBSITE-E2E-SMOKE]) +asserts: every code in `rules.json` has a built `/errors//index.html` +(≥ 155 — every CLI-linked code); a sampled page renders its code, title and +severity badge; the `/errors/` index links every code; and every worked-example +screenshot decodes on its page. With the drift guard above, this closes the loop +from checker source → data → page → render. diff --git a/docs/specs/WEBSITE-SCREENSHOTS-SPEC.md b/docs/specs/WEBSITE-SCREENSHOTS-SPEC.md new file mode 100644 index 00000000..f07ab565 --- /dev/null +++ b/docs/specs/WEBSITE-SCREENSHOTS-SPEC.md @@ -0,0 +1,100 @@ +# Website: Automated CLI Screenshots {#WEBSITE-SCREENSHOTS} + +**Version**: 0.1.0 +**Status**: Active +**License**: MIT + +--- + +## Purpose {#WEBSITE-SCREENSHOTS-PURPOSE} + +The marketing/docs site embeds real `basilisk check` output as PNGs: the homepage +before/after demo (`cli-demo.png`, `cli-clean.png`) and one image per documented +rule (`e0001.png` … `e0025.png`, referenced from `website/src/docs/rules/*.md`). + +Historically these were produced by a manual macOS process — drive Terminal.app, +`screencapture` a specific window by id, then crop with ImageMagick. That process +is slow, non-reproducible, easy to get wrong (snippets that don't actually trigger +the rule they claim), and leaks environment details (home dir, username) if run +carelessly. + +This spec defines a **fully automated, reproducible** replacement. A single +command runs the real binary on each documented snippet and renders its genuine, +coloured output inside a faithful macOS Terminal window — no manual capture, no +PII, and a built-in guard that every snippet still triggers the diagnostic it +documents. + +The images remain **real binary output**, honouring the project rule that +marketing/doc screenshots are never hand-typed code fences or synthetic renders: +the bytes shown are exactly what `basilisk check --color always` prints. + +## Generator {#WEBSITE-SCREENSHOTS-GENERATE} + +Run from `website/`: + +```bash +npm run screenshots # regenerate every image +node screenshots/generate.mjs e0001 e0012 # regenerate a subset by name +BASILISK_BIN=../target/release/basilisk npm run screenshots # pin the binary +``` + +`website/screenshots/generate.mjs` ([WEBSITE-SCREENSHOTS-GENERATE]) for each shot: + +1. Writes the snippet into a throwaway, neutrally-named temp directory + (`basilisk-demo-*`) so diagnostic paths read `e0001.py:1:13` — relative and + PII-free, satisfying the "no PII / clean prompt" rule without a custom shell. +2. Runs `basilisk check --color always ` in that directory. A non-zero exit + (any file with diagnostics) is expected; stdout is read off the thrown error. +3. Asserts the documented diagnostic code is present — see + [WEBSITE-SCREENSHOTS-MANIFEST]. If it is absent the image is **not** written and + generation fails loudly, so a checker behaviour change can never silently ship a + misleading screenshot. +4. Renders the output in a Terminal window ([WEBSITE-SCREENSHOTS-CHROME]) via + Playwright (Chromium, `deviceScaleFactor: 2` for crisp Retina output) and writes + `website/src/assets/images/.png`. + +The binary defaults to `basilisk` on `PATH`; override with `BASILISK_BIN`. The +generator is **not** run in CI — the PNGs are committed, and regenerated locally +when the CLI output changes. CI only verifies they render +([WEBSITE-SCREENSHOTS-VERIFY]), keeping with `[GITHUB-NO-ARTIFACTS]`. + +## Manifest {#WEBSITE-SCREENSHOTS-MANIFEST} + +`website/screenshots/shots.mjs` is the single source of truth for every CLI +screenshot. Each entry pairs the **exact** snippet shown in the docs with the +diagnostic code that snippet must produce (`expect`). The snippets are crafted to +isolate one rule — e.g. `e0001` keeps the `-> str` return annotation so only +`BSK-E0001` fires, not `BSK-E0002`. `e0011` documents the explicit-`Any` check and +asserts the warning code the binary actually emits (`BSK-W0014`); the home shots +assert the summary line (`Found 6 diagnostics`, `No issues found`). + +This is the automated form of the manual-process rule "many rule examples do NOT +trigger the rule they claim — always confirm the exact target code appears": the +assertion lives in code and runs on every regeneration. + +## Terminal chrome {#WEBSITE-SCREENSHOTS-CHROME} + +`website/screenshots/terminal.mjs` builds the window HTML and `ansi.mjs` converts +the binary's ANSI escapes to themed HTML. The binary emits a small, fixed SGR set — +reset, bold, and bold foreground red (errors), yellow (warnings), blue (gutters), +cyan (labels) — modelled exactly rather than as a general terminal. The window is a +120-column macOS Terminal ("basilisk-demo — -zsh") on the default dark profile +(`rgb(30, 30, 30)`), matching the original captures so regenerated images are +visually identical. + +## Render verification {#WEBSITE-SCREENSHOTS-VERIFY} + +`website/tests/e2e/screenshots.spec.ts` runs under the same Playwright config as +[WEBSITE-E2E-SMOKE] (desktop + mobile, against the production `_site/` build). It +imports the manifest so it can never drift from the generated set, and asserts: + +- The rule docs (`/docs/rules/missing-annotations/`, `/docs/rules/type-safety/`) + embed **every** `e00*` screenshot and each decodes to non-zero pixels. +- The homepage before/after demo embeds both `cli-demo.png` and `cli-clean.png`; + `cli-demo` (visible) renders immediately, and `cli-clean` renders after its tab + panel is revealed (it is lazy-loaded and initially hidden). +- No `/assets/images/*.png` request returns a non-200 status. + +A missing, zero-byte, or unreferenced screenshot fails CI rather than shipping a +broken image. As with [WEBSITE-E2E-NO-ARTIFACTS], CI emits only the stdout `list` +reporter — no report, trace, video or capture is uploaded. diff --git a/scripts/gen_rules_reference.py b/scripts/gen_rules_reference.py index ec4840e7..3d4a3927 100644 --- a/scripts/gen_rules_reference.py +++ b/scripts/gen_rules_reference.py @@ -2,20 +2,29 @@ """Generate the canonical diagnostic-code reference from the checker source. Single source of truth: the `//! BSK-E####: ` (and `BSK-W####`) -header on each rule module under crates/basilisk-checker/src/rules/. +header — and the doc-comment body beneath it — on each rule module under +crates/basilisk-checker/src/rules/. Usage: - python3 scripts/gen_rules_reference.py # print a Markdown table - python3 scripts/gen_rules_reference.py --json # emit JSON - python3 scripts/gen_rules_reference.py --check FILE # verify FILE contains + python3 scripts/gen_rules_reference.py # print a Markdown table + python3 scripts/gen_rules_reference.py --json # emit code->summary JSON + python3 scripts/gen_rules_reference.py --data [OUT] # write the rich rules + # data Eleventy consumes + # (default: website/src/ + # _data/rules.json) + python3 scripts/gen_rules_reference.py --check FILE # verify FILE contains # every current code -Run this after adding or renaming a rule, and paste the table into -website/src/docs/rules/index.md (between the REFERENCE markers). +The `--data` output drives both the complete reference table and the per-code +/errors/BSK-XXXX/ pages on the website, so the pages the CLI deep-links to +(`see: https://www.basilisk-python.dev/errors/BSK-EXXXX`) can never drift from +the checker. Run it after adding or renaming a rule. See +docs/specs/WEBSITE-ERROR-PAGES-SPEC.md [WEBSITE-ERROR-PAGES]. """ from __future__ import annotations +import html import json import re import sys @@ -23,35 +32,26 @@ ROOT = Path(__file__).resolve().parent.parent RULES_DIR = ROOT / "crates" / "basilisk-checker" / "src" / "rules" +DEFAULT_DATA_OUT = ROOT / "website" / "src" / "_data" / "rules.json" +ERRORS_BASE_URL = "https://www.basilisk-python.dev/errors" HEADER = re.compile(r"//!\s*(BSK-[EW]\d{4}):\s*(.*)") -CONT = re.compile(r"//!\s*(.*)") +DOC = re.compile(r"//!\s?(.*)") +DOCS_URL = re.compile(r'docs_url:\s*"([^"]+)"') +SPEC_REF = re.compile(r"^Implements ") - -def clean(text: str) -> str: - text = text.strip().rstrip(".").strip() - # collapse internal whitespace - return re.sub(r"\s+", " ", text) +# Coarse groups for filtering/badging on the website. Errors outside the two +# foundational ranges are all part of the broader type-system surface. +GROUPS = ( + ("E", 1, 9, "Missing Annotations"), + ("E", 10, 29, "Type Safety"), + ("E", 30, 9999, "Type System"), + ("W", 0, 9999, "Warnings"), +) -def extract() -> dict[str, str]: - """Map code -> description, taking the first header found per code.""" - codes: dict[str, str] = {} - for path in sorted(RULES_DIR.rglob("*.rs")): - lines = path.read_text(encoding="utf-8").splitlines() - for i, line in enumerate(lines): - m = HEADER.match(line.strip()) - if not m: - continue - code, desc = m.group(1), m.group(2) - # Stitch a wrapped description (next //! line) when the first - # line ends without sentence-final punctuation. - if desc and not desc.rstrip().endswith((".", "!", ")")): - nxt = CONT.match(lines[i + 1].strip()) if i + 1 < len(lines) else None - if nxt and not HEADER.match(lines[i + 1].strip()): - desc = f"{desc} {nxt.group(1)}" - codes.setdefault(code, clean(desc)) - return codes +def clean(text: str) -> str: + return re.sub(r"\s+", " ", text.strip().rstrip(".").strip()) def sort_key(code: str) -> tuple[int, int]: @@ -59,31 +59,149 @@ def sort_key(code: str) -> tuple[int, int]: return (0 if code[4] == "E" else 1, int(code[5:])) -def to_markdown(codes: dict[str, str]) -> str: +def group_for(code: str) -> str: + kind, num = code[4], int(code[5:]) + for gk, lo, hi, label in GROUPS: + if gk == kind and lo <= num <= hi: + return label + return "Type System" + + +def inline_html(text: str) -> str: + """Render a rustdoc line as safe inline HTML: intra-doc links unwrapped, + `code` spans and *emphasis* preserved.""" + text = re.sub(r"\[`?([^`\]]+)`?\]", r"\1", text) # [`Foo`] / [BSK-X] -> Foo + text = html.escape(text) + text = re.sub(r"`([^`]+)`", r"\1", text) + text = re.sub(r"(?\1", text) + return text + + +ENDS_SENTENCE = (".", "!", ")", ":") +FENCE = re.compile(r"^```(\w*)\s*$") + + +def is_text_line(line: str) -> bool: + return line != "" and not FENCE.match(line) and not SPEC_REF.match(line) + + +def parse_body(doc_lines: list[str]) -> list[dict]: + """Turn the doc-comment lines beneath a header into typed blocks: text + paragraphs (safe inline HTML) and fenced code blocks (raw, escaped by the + template). The spec-reference line is dropped.""" + blocks: list[dict] = [] + paragraph: list[str] = [] + code: list[str] | None = None + lang = "python" + + def flush_paragraph() -> None: + nonlocal paragraph + if paragraph: + blocks.append({"type": "text", "html": inline_html(" ".join(paragraph))}) + paragraph = [] + + for line in doc_lines: + fence = FENCE.match(line) + if code is not None: + if fence: + blocks.append({"type": "code", "lang": lang, "code": "\n".join(code)}) + code = None + else: + code.append(line) + continue + if fence: + flush_paragraph() + code = [] + lang = fence.group(1) or "text" + continue + if line == "": + flush_paragraph() + continue + if SPEC_REF.match(line): + continue + paragraph.append(line) + flush_paragraph() + if code: # unterminated fence — keep the content rather than drop it + blocks.append({"type": "code", "lang": lang, "code": "\n".join(code)}) + return blocks + + +def extract() -> list[dict]: + """One record per code: summary, body blocks, severity, group, docsUrl.""" + records: dict[str, dict] = {} + for path in sorted(RULES_DIR.rglob("*.rs")): + text = path.read_text(encoding="utf-8") + lines = text.splitlines() + file_docs_url = DOCS_URL.search(text) + for i, line in enumerate(lines): + m = HEADER.match(line.strip()) + if not m: + continue + code, summary = m.group(1), m.group(2) + if code in records: + continue + # The contiguous //! doc lines following the header line. + body_lines: list[str] = [] + for follow in lines[i + 1 :]: + doc = DOC.match(follow.strip()) + if doc is None: + break + body_lines.append(doc.group(1)) + # Stitch a summary that wrapped onto following doc lines (it ends + # without sentence-final punctuation) before they become body. + while ( + not summary.rstrip().endswith(ENDS_SENTENCE) + and body_lines + and is_text_line(body_lines[0]) + ): + summary = f"{summary} {body_lines.pop(0)}" + records[code] = { + "code": code, + "severity": "error" if code[4] == "E" else "warning", + "summary": clean(summary), + "summaryHtml": inline_html(clean(summary)), + "body": parse_body(body_lines), + "group": group_for(code), + "docsUrl": file_docs_url.group(1) + if file_docs_url + else f"{ERRORS_BASE_URL}/{code}", + } + return [records[c] for c in sorted(records, key=sort_key)] + + +def to_markdown(records: list[dict]) -> str: rows = ["| Code | Description |", "|---|---|"] - for code in sorted(codes, key=sort_key): - rows.append(f"| `{code}` | {codes[code]} |") + for r in records: + rows.append(f"| `{r['code']}` | {r['summary']} |") return "\n".join(rows) def main() -> int: - codes = extract() + records = extract() if "--json" in sys.argv: - print(json.dumps(codes, indent=2, sort_keys=True)) + print(json.dumps({r["code"]: r["summary"] for r in records}, indent=2)) + return 0 + if "--data" in sys.argv: + idx = sys.argv.index("--data") + out = Path(sys.argv[idx + 1]) if idx + 1 < len(sys.argv) else DEFAULT_DATA_OUT + out.write_text(json.dumps(records, indent=2) + "\n", encoding="utf-8") + errors = sum(r["severity"] == "error" for r in records) + warnings = len(records) - errors + print( + f"Wrote {len(records)} codes ({errors} errors, {warnings} warnings) -> {out}" + ) return 0 if "--check" in sys.argv: target = Path(sys.argv[sys.argv.index("--check") + 1]).read_text( encoding="utf-8" ) - missing = [c for c in codes if c not in target] + missing = [r["code"] for r in records if r["code"] not in target] if missing: - print( - f"MISSING {len(missing)} codes: {', '.join(sorted(missing, key=sort_key))}" - ) + print(f"MISSING {len(missing)} codes: {', '.join(missing)}") return 1 - print(f"OK: all {len(codes)} codes present") + print(f"OK: all {len(records)} codes present") return 0 - print(to_markdown(codes)) + print(to_markdown(records)) return 0 diff --git a/scripts/render-zed-mirror.sh b/scripts/render-zed-mirror.sh index 98ea54a4..e066e6d3 100755 --- a/scripts/render-zed-mirror.sh +++ b/scripts/render-zed-mirror.sh @@ -129,8 +129,8 @@ copy_extension_tree() { for item in "${COPY_ITEMS[@]}"; do if [[ -e "$src/$item" ]]; then # -L dereferences symlinks so the mirror is self-contained: e.g. - # images/screenshot.png links into website/, which does not exist in - # the standalone tree — copy the real file, not a dangling link. + # images/zed-screenshot.png links into website/, which does not exist + # in the standalone tree — copy the real file, not a dangling link. cp -RL "$src/$item" "$dest/$item" fi done diff --git a/vscode-extension/.vscode-test.mjs b/vscode-extension/.vscode-test.mjs index a5d572bc..6125190b 100644 --- a/vscode-extension/.vscode-test.mjs +++ b/vscode-extension/.vscode-test.mjs @@ -25,7 +25,16 @@ export default defineConfig({ // This enables whole-module analysis tests that write Python files to the // workspace root without opening them in the editor. workspaceFolder: path.join(__dirname, 'test-fixtures', 'workspace'), - launchArgs: ['--disable-extensions', '--user-data-dir', userDataDir], + launchArgs: [ + '--disable-extensions', + '--user-data-dir', userDataDir, + // [VSIX-EDITOR-SCREENSHOTS]: when capturing website screenshots, expose + // the headed VS Code window over CDP so the Playwright watcher + // (screenshot-watcher.mjs) can grab it. No effect on normal test runs. + ...(process.env.BASILISK_SCREENSHOTS + ? [`--remote-debugging-port=${process.env.BASILISK_SCREENSHOT_CDP_PORT ?? '9229'}`] + : []), + ], // Coverage: tell c8 where compiled sources live. Without this, // @vscode/test-cli defaults to 'src' (TypeScript sources), so // include patterns like 'out/**/*.js' resolve against src/ and diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 66a7cc9b..b7a5c8f9 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -980,7 +980,8 @@ "test": "vscode-test", "test:shipwright": "node scripts/verify-shipwright.mjs manifest", "vscode:prepublish": "npm run sync:shipwright && npm run compile", - "package": "npm run sync:shipwright && vsce package" + "package": "npm run sync:shipwright && vsce package", + "screenshots:editor": "node scripts/capture-screenshots.mjs" }, "dependencies": { "@nimblesite/shipwright-vscode": "^0.10.0", diff --git a/vscode-extension/scripts/capture-screenshots.mjs b/vscode-extension/scripts/capture-screenshots.mjs new file mode 100644 index 00000000..25dab002 --- /dev/null +++ b/vscode-extension/scripts/capture-screenshots.mjs @@ -0,0 +1,59 @@ +// Implements [VSIX-EDITOR-SCREENSHOTS]: one command to regenerate the website's +// VS Code editor screenshots. Stages the built binary into the dev extension, +// copies the shipwright manifest, launches the CDP screenshot sidecar, and runs +// the (otherwise-skipped) "Editor screenshots" suite headed with +// BASILISK_SCREENSHOTS=1 so the sidecar captures each feature. +// +// Prerequisite: the binaries must be built — +// cargo build -p basilisk-cli -p basilisk-profiler-helper +// +// Usage (from vscode-extension/): npm run screenshots:editor +// See docs/specs/VSIX-EDITOR-SCREENSHOTS-SPEC.md. + +import { spawn, spawnSync } from "node:child_process"; +import { copyFileSync, existsSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const extensionRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const repoRoot = resolve(extensionRoot, ".."); +const CDP_PORT = process.env.BASILISK_SCREENSHOT_CDP_PORT ?? "9229"; + +const run = (cmd, args, opts = {}) => { + const result = spawnSync(cmd, args, { stdio: "inherit", cwd: repoRoot, ...opts }); + if (result.status !== 0) { + throw new Error(`${cmd} ${args.join(" ")} exited with ${result.status ?? result.signal}`); + } +}; + +// 1. Stage the runtime binaries into the dev extension's bin// (the +// same path the packaged VSIX and the extension's shipwright resolver use). +if (!existsSync(join(repoRoot, "target", "debug", "basilisk"))) { + throw new Error("missing target/debug/basilisk — run: cargo build -p basilisk-cli -p basilisk-profiler-helper"); +} +run("node", [join(extensionRoot, "scripts", "stage-runtime.mjs"), "target/debug"]); + +// 2. The extension reads shipwright.json from its own root; at dev time it only +// lives at the repo root (packaging copies it in). Mirror it (gitignored). +copyFileSync(join(repoRoot, "shipwright.json"), join(extensionRoot, "shipwright.json")); + +// 3. Compile the extension + tests. +run("npm", ["run", "compile"], { cwd: extensionRoot }); + +// 4. Launch the CDP screenshot sidecar. +const env = { ...process.env, BASILISK_SCREENSHOTS: "1", BASILISK_SCREENSHOT_CDP_PORT: CDP_PORT }; +const watcher = spawn("node", [join(extensionRoot, "scripts", "screenshot-watcher.mjs")], { + stdio: "inherit", + cwd: extensionRoot, + env, +}); + +// 5. Run only the screenshot suite, headed, with the sidecar attached. +const test = spawnSync("npx", ["vscode-test", "--grep", "Editor screenshots"], { + stdio: "inherit", + cwd: extensionRoot, + env, +}); + +watcher.kill(); +process.exit(test.status ?? 1); diff --git a/vscode-extension/scripts/screenshot-watcher.mjs b/vscode-extension/scripts/screenshot-watcher.mjs new file mode 100644 index 00000000..68a2038e --- /dev/null +++ b/vscode-extension/scripts/screenshot-watcher.mjs @@ -0,0 +1,106 @@ +// Implements [VSIX-EDITOR-SCREENSHOTS]: dependency-free sidecar that captures the +// real VS Code window for the website. Runs alongside the VSIX suite only when +// BASILISK_SCREENSHOTS=1. The harness launches VS Code with +// --remote-debugging-port; this watcher speaks the Chrome DevTools Protocol over +// Node's built-in WebSocket (no Playwright / browser download), watches for +// `.signal` files written by takeWindowScreenshot() in screenshot.ts, captures +// the workbench page, and writes the PNG. See docs/specs/VSIX-EDITOR-SCREENSHOTS-SPEC.md. + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// vscode-extension/scripts -> repo root is two levels up. +const repoRoot = path.resolve(__dirname, "../../"); +const outputDir = path.resolve(repoRoot, "website/src/assets/images"); +const CDP_PORT = Number.parseInt(process.env.BASILISK_SCREENSHOT_CDP_PORT ?? "9229", 10); +const POLL_MS = 200; +const TIMEOUT_MS = 600_000; + +fs.mkdirSync(outputDir, { recursive: true }); +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +// Find the VS Code workbench page target and return its debugger WebSocket URL. +async function findWorkbenchSocket(deadline) { + while (Date.now() < deadline) { + try { + const res = await fetch(`http://127.0.0.1:${CDP_PORT}/json/list`); + if (res.ok) { + const targets = await res.json(); + const page = targets.find( + (t) => t.type === "page" && /workbench\.(esm\.)?html|workbench\.html/.test(t.url ?? ""), + ) ?? targets.find((t) => t.type === "page"); + if (page?.webSocketDebuggerUrl) return page.webSocketDebuggerUrl; + } + } catch { + /* endpoint not up yet */ + } + await sleep(500); + } + throw new Error(`VS Code workbench page not found on CDP port ${CDP_PORT}`); +} + +// Minimal CDP client over the built-in WebSocket: correlates responses by id. +function connect(wsUrl) { + const ws = new WebSocket(wsUrl); + const pending = new Map(); + let nextId = 1; + ws.addEventListener("message", (event) => { + const msg = JSON.parse(typeof event.data === "string" ? event.data : event.data.toString()); + if (msg.id !== undefined && pending.has(msg.id)) { + const { resolve, reject } = pending.get(msg.id); + pending.delete(msg.id); + msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result); + } + }); + const ready = new Promise((resolve, reject) => { + ws.addEventListener("open", () => resolve()); + ws.addEventListener("error", () => reject(new Error("CDP socket error"))); + }); + const send = (method, params = {}) => + new Promise((resolve, reject) => { + const id = nextId++; + pending.set(id, { resolve, reject }); + ws.send(JSON.stringify({ id, method, params })); + }); + return { ws, ready, send }; +} + +async function main() { + console.log(`[screenshots] waiting for VS Code CDP on port ${CDP_PORT}...`); + const wsUrl = await findWorkbenchSocket(Date.now() + 120_000); + const { ws, ready, send } = connect(wsUrl); + await ready; + await send("Page.enable"); + // Force a uniform, Retina-crisp viewport so every screenshot is the same size + // regardless of the headed window's actual dimensions. + await send("Emulation.setDeviceMetricsOverride", { + width: 1440, + height: 900, + deviceScaleFactor: 2, + mobile: false, + }); + await sleep(800); + console.log("[screenshots] connected; watching for signal files..."); + + const start = Date.now(); + while (Date.now() - start < TIMEOUT_MS) { + for (const signal of fs.readdirSync(outputDir).filter((f) => f.endsWith(".signal"))) { + const signalPath = path.join(outputDir, signal); + const requested = fs.readFileSync(signalPath, "utf8").trim(); + const tempPath = path.join(outputDir, signal.replace(/\.signal$/, "")); + const { data } = await send("Page.captureScreenshot", { format: "png", captureBeyondViewport: false }); + fs.writeFileSync(tempPath, Buffer.from(data, "base64")); + fs.unlinkSync(signalPath); + console.log(`[screenshots] ${requested} (${Math.round(fs.statSync(tempPath).size / 1024)}KB)`); + } + await sleep(POLL_MS); + } + ws.close(); +} + +main().catch((error) => { + console.error("[screenshots] failed:", error.message); + process.exit(1); +}); diff --git a/vscode-extension/src/test/suite/screenshot.ts b/vscode-extension/src/test/suite/screenshot.ts index 8ace6273..3e695f58 100644 --- a/vscode-extension/src/test/suite/screenshot.ts +++ b/vscode-extension/src/test/suite/screenshot.ts @@ -20,6 +20,60 @@ import { execFile } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; +/** + * Directory for the *committed* website editor screenshots. Resolves to + * `website/src/assets/images/` (`__dirname` is `out/test/suite`, four levels up + * is the repo root). + */ +function websiteImageDir(): string { + return path.resolve(__dirname, '..', '..', '..', '..', 'website', 'src', 'assets', 'images'); +} + +async function sleep(ms: number): Promise { + await new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +/** + * Capture the real VS Code window for the website via the Playwright CDP sidecar + * ([VSIX-EDITOR-SCREENSHOTS], screenshot-watcher.mjs). Writes a `.signal` file + * into the website image dir and waits for the sidecar to produce the PNG, then + * renames it into place. + * + * No-op unless `BASILISK_SCREENSHOTS=1` — so normal test runs are unaffected and + * never write into the repo. Call after assertions prove the feature is visible. + * + * @param filename final PNG name, e.g. `vscode-diagnostics.png`. + */ +export async function takeWindowScreenshot(filename: string): Promise { + if (process.env.BASILISK_SCREENSHOTS === undefined) { + return; + } + const dir = websiteImageDir(); + fs.mkdirSync(dir, { recursive: true }); + const tempFilename = `${filename}.tmp-${process.pid.toString()}.png`; + const tempPath = path.join(dir, tempFilename); + const signalPath = path.join(dir, `${tempFilename}.signal`); + const outPath = path.join(dir, filename); + if (fs.existsSync(tempPath)) { + fs.rmSync(tempPath, { force: true }); + } + fs.writeFileSync(signalPath, filename, 'utf8'); + + const deadline = Date.now() + 20_000; + while (Date.now() < deadline) { + if (fs.existsSync(tempPath)) { + fs.renameSync(tempPath, outPath); + // eslint-disable-next-line no-console + console.log(`[screenshot] wrote ${filename}`); + return; + } + await sleep(100); + } + throw new Error(`screenshot sidecar did not produce ${filename} within 20s`); +} + /** * Directory where screenshots are written. Resolves to * `vscode-extension/.screenshots/` (gitignored). `__dirname` is diff --git a/vscode-extension/src/test/suite/screenshots-capture.test.ts b/vscode-extension/src/test/suite/screenshots-capture.test.ts new file mode 100644 index 00000000..401e6fdd --- /dev/null +++ b/vscode-extension/src/test/suite/screenshots-capture.test.ts @@ -0,0 +1,140 @@ +// Implements [VSIX-EDITOR-SCREENSHOTS]: captures the real VS Code editor for the +// website. Each test drives a Basilisk feature until it is visible, then asks the +// CDP sidecar (scripts/screenshot-watcher.mjs) to grab the window. +// +// The whole suite is a NO-OP unless BASILISK_SCREENSHOTS=1 (it skips in +// suiteSetup), so normal `npm test` runs are unaffected and nothing is written +// into the repo. Drive it with `npm run screenshots:editor`, which builds + +// stages the binary, copies shipwright.json, launches the sidecar, and runs only +// this file. See docs/specs/VSIX-EDITOR-SCREENSHOTS-SPEC.md. + +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +import { + SUITE_SETUP_TIMEOUT_MS, + closeAllEditors, + findBasiliskBinary, + openPythonFile, + waitForDiagnostics, + waitForLspReady, +} from './test-helpers'; +import { takeWindowScreenshot } from './screenshot'; + +async function sleep(ms: number): Promise { + await new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +// Strip transient chrome that clutters a marketing screenshot: the +// "--disable-extensions"/git toasts and the Chat auxiliary bar. +async function prepareWindow(): Promise { + await vscode.commands.executeCommand('notifications.clearAll'); + await vscode.commands.executeCommand('workbench.action.closeAuxiliaryBar'); + await sleep(400); +} + +// A file that triggers several distinct Basilisk diagnostics, so the editor and +// Problems panel look representative. +const DEMO_SOURCE = `def process(data): + return data.upper() + + +class User: + def __init__(self, name, age): + self.name = name + self.age = age +`; + +suite('Editor screenshots', function () { + let tmpDir: string; + + suiteSetup(async function () { + this.timeout(SUITE_SETUP_TIMEOUT_MS); + if (process.env.BASILISK_SCREENSHOTS === undefined) { + this.skip(); + } + if (findBasiliskBinary() === undefined) { + throw new Error('Basilisk binary not found. Build with: cargo build -p basilisk-cli'); + } + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'basilisk-shots-')); + await waitForLspReady(); + await closeAllEditors(); + }); + + suiteTeardown(async () => { + await closeAllEditors(); + if (tmpDir !== undefined && tmpDir !== '' && fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + test('diagnostics + Problems panel', async function () { + this.timeout(60_000); + const { uri } = await openPythonFile(tmpDir, 'diagnostics.py', DEMO_SOURCE); + await waitForDiagnostics(uri); + // Surface the squiggles in the Problems panel for a complete picture. + await vscode.commands.executeCommand('workbench.actions.view.problems'); + await sleep(1200); + await prepareWindow(); + await takeWindowScreenshot('vscode-diagnostics.png'); + await vscode.commands.executeCommand('workbench.action.closePanel'); + }); + + test('hover with type information', async function () { + this.timeout(60_000); + const { doc } = await openPythonFile( + tmpDir, + 'hover.py', + 'def greet(name: str) -> str:\n return f"Hello, {name}"\n', + ); + const editor = await vscode.window.showTextDocument(doc, { preview: false }); + // Position on the `greet` function name and request the hover popup. + const pos = new vscode.Position(0, 5); + editor.selection = new vscode.Selection(pos, pos); + editor.revealRange(new vscode.Range(pos, pos)); + await sleep(500); + await vscode.commands.executeCommand('editor.action.showHover'); + await prepareWindow(); + await sleep(300); + await vscode.commands.executeCommand('editor.action.showHover'); + await sleep(1200); + await takeWindowScreenshot('vscode-hover.png'); + }); + + test('quick fix code actions', async function () { + this.timeout(60_000); + const { uri, doc } = await openPythonFile( + tmpDir, + 'quickfix.py', + 'def process(data):\n return data\n', + ); + await waitForDiagnostics(uri); + const editor = await vscode.window.showTextDocument(doc, { preview: false }); + const pos = new vscode.Position(0, 12); // on the unannotated `data` parameter + editor.selection = new vscode.Selection(pos, pos); + editor.revealRange(new vscode.Range(pos, pos)); + await sleep(500); + await vscode.commands.executeCommand('editor.action.quickFix'); + await prepareWindow(); + await sleep(300); + await vscode.commands.executeCommand('editor.action.quickFix'); + await sleep(1200); + await takeWindowScreenshot('vscode-quickfix.png'); + await vscode.commands.executeCommand('hideSuggestWidget'); + }); + + test('module explorer activity panel', async function () { + this.timeout(60_000); + await openPythonFile(tmpDir, 'explorer.py', DEMO_SOURCE); + await vscode.commands.executeCommand('workbench.view.extension.basilisk-explorer'); + await sleep(800); + await vscode.commands.executeCommand('basilisk.refreshModuleExplorer'); + await sleep(1800); + await prepareWindow(); + await takeWindowScreenshot('vscode-module-explorer.png'); + }); +}); diff --git a/website/package.json b/website/package.json index 43ec3da4..c84aff43 100644 --- a/website/package.json +++ b/website/package.json @@ -8,6 +8,7 @@ "build": "eleventy", "start": "eleventy --serve --watch", "clean": "rm -rf _site", + "screenshots": "node screenshots/generate.mjs", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui" }, diff --git a/website/screenshots/ansi.mjs b/website/screenshots/ansi.mjs new file mode 100644 index 00000000..85cc0d1e --- /dev/null +++ b/website/screenshots/ansi.mjs @@ -0,0 +1,72 @@ +// Implements [WEBSITE-SCREENSHOTS-ANSI]: faithful ANSI SGR → HTML conversion for +// the exact escape sequences `basilisk check --color always` emits. See +// docs/specs/WEBSITE-SCREENSHOTS-SPEC.md. +// +// basilisk uses a small, fixed palette: reset (0), bold (1), and bold foreground +// red (31, errors), yellow (33, warnings), blue (34, gutters), cyan (36, labels). +// We model exactly that set rather than a general 256-colour terminal, so the +// output is deterministic and matches a real macOS Terminal window pixel-for-pixel. + +// Colours tuned to match macOS Terminal's default dark profile as it renders the +// real binary — the values our committed reference PNGs were captured with. +const FOREGROUND = { + default: "#d6d6d6", // unstyled text (the echoed source line) + bold: "#f4f4f4", // bold, no colour (diagnostic message) + 31: "#ff6b5e", // red — error / summary + 33: "#e8c062", // yellow — warning + 34: "#7d8cff", // blue — `-->`, `|`, `=`, line numbers + 36: "#4ec9d4", // cyan — help / note / see labels +}; + +const ESCAPE_PATTERN = /\x1b\[([0-9;]*)m/g; + +const escapeHtml = (text) => + text + .replace(/&/g, "&") + .replace(//g, ">"); + +const initialState = () => ({ bold: false, color: null }); + +// Fold one SGR parameter list into the running style state. +const applyParams = (state, params) => { + const codes = params === "" ? [0] : params.split(";").map(Number); + return codes.reduce((next, code) => { + if (code === 0) return initialState(); + if (code === 1) return { ...next, bold: true }; + if (code >= 30 && code <= 37) return { ...next, color: code }; + if (code >= 90 && code <= 97) return { ...next, color: code - 60 }; + return next; + }, state); +}; + +const colorFor = (state) => { + if (state.color !== null && FOREGROUND[state.color]) return FOREGROUND[state.color]; + return state.bold ? FOREGROUND.bold : FOREGROUND.default; +}; + +const wrap = (text, state) => { + if (text === "") return ""; + const weight = state.bold ? "700" : "400"; + return `${escapeHtml(text)}`; +}; + +/** + * Convert a string containing basilisk's ANSI escape sequences into themed HTML. + * Unstyled runs still emit a span so every glyph carries the terminal foreground. + */ +export const ansiToHtml = (raw) => { + let html = ""; + let state = initialState(); + let cursor = 0; + + for (const match of raw.matchAll(ESCAPE_PATTERN)) { + html += wrap(raw.slice(cursor, match.index), state); + state = applyParams(state, match[1]); + cursor = match.index + match[0].length; + } + html += wrap(raw.slice(cursor), state); + return html; +}; + +export const TERMINAL_FOREGROUND = FOREGROUND; diff --git a/website/screenshots/generate.mjs b/website/screenshots/generate.mjs new file mode 100644 index 00000000..a8dead53 --- /dev/null +++ b/website/screenshots/generate.mjs @@ -0,0 +1,91 @@ +// Implements [WEBSITE-SCREENSHOTS-GENERATE]: regenerate every CLI screenshot on +// the site from the real `basilisk` binary, with no manual Terminal/screencapture +// step. See docs/specs/WEBSITE-SCREENSHOTS-SPEC.md. +// +// For each shot it writes the snippet to a throwaway, neutrally-named directory +// (so diagnostic paths read `e0001.py:1:13` with no PII), runs +// `basilisk check --color always ` there, asserts the documented code +// actually fires, renders the output inside a macOS Terminal window via Playwright, +// and writes website/src/assets/images/.png at 2× for crisp Retina display. +// +// Usage: node screenshots/generate.mjs (regenerate all) +// node screenshots/generate.mjs e0001 e0012 (regenerate a subset) +// BASILISK_BIN=../target/release/basilisk node screenshots/generate.mjs + +import { chromium } from "@playwright/test"; +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { SHOTS } from "./shots.mjs"; +import { buildTerminalHtml, WINDOW_SELECTOR } from "./terminal.mjs"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const OUTPUT_DIR = path.resolve(here, "../src/assets/images"); +const BIN = process.env.BASILISK_BIN ?? "basilisk"; +const SCALE = 2; + +const stripAnsi = (text) => text.replace(/\x1b\[[0-9;]*m/g, ""); + +// Run `basilisk check` in `cwd`. A non-zero exit is expected whenever diagnostics +// are reported, so we read the captured stdout off the thrown error too. +const runChecker = (file, cwd) => { + try { + return execFileSync(BIN, ["check", "--color", "always", file], { + cwd, + encoding: "utf8", + maxBuffer: 8 * 1024 * 1024, + }); + } catch (error) { + if (typeof error.stdout === "string" && error.stdout.length > 0) return error.stdout; + throw new Error(`basilisk failed for ${file}: ${error.stderr || error.message}`); + } +}; + +const captureShot = async (page, shot, workDir) => { + fs.writeFileSync(path.join(workDir, shot.file), shot.code); + const output = runChecker(shot.file, workDir); + + if (!stripAnsi(output).includes(shot.expect)) { + throw new Error( + `${shot.name}: expected "${shot.expect}" in output but it was absent — ` + + `the snippet no longer triggers the documented diagnostic.\n${stripAnsi(output)}`, + ); + } + + await page.setContent( + buildTerminalHtml({ command: `basilisk check ${shot.file}`, ansiOutput: output }), + { waitUntil: "load" }, + ); + const target = OUTPUT_DIR + path.sep + `${shot.name}.png`; + await page.locator(WINDOW_SELECTOR).screenshot({ path: target }); + const kb = Math.round(fs.statSync(target).size / 1024); + console.log(` ✓ ${shot.name}.png (${kb} KB) [${shot.expect}]`); +}; + +const main = async () => { + const requested = new Set(process.argv.slice(2)); + const shots = requested.size === 0 ? SHOTS : SHOTS.filter((s) => requested.has(s.name)); + if (shots.length === 0) throw new Error(`no shots matched: ${[...requested].join(", ")}`); + + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "basilisk-demo-")); + console.log(`Generating ${shots.length} screenshot(s) → src/assets/images/`); + + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 1200, height: 2400 }, deviceScaleFactor: SCALE }); + try { + for (const shot of shots) await captureShot(page, shot, workDir); + } finally { + await browser.close(); + fs.rmSync(workDir, { recursive: true, force: true }); + } + console.log("Done."); +}; + +main().catch((error) => { + console.error(`screenshots: ${error.message}`); + process.exit(1); +}); diff --git a/website/screenshots/shots.mjs b/website/screenshots/shots.mjs new file mode 100644 index 00000000..810225b0 --- /dev/null +++ b/website/screenshots/shots.mjs @@ -0,0 +1,320 @@ +// Implements [WEBSITE-SCREENSHOTS-MANIFEST]: the single source of truth for every +// CLI screenshot on the site. See docs/specs/WEBSITE-SCREENSHOTS-SPEC.md. +// +// Each entry pairs the EXACT snippet shown in the docs with the diagnostic code +// that snippet must produce. The generator runs the real `basilisk` binary on the +// snippet and refuses to write the image unless `expect` appears in the output — +// this is the automated form of the "verify the example actually triggers the +// rule" rule, so a checker behaviour change can never silently produce a +// misleading screenshot. +// +// `name` is the output PNG stem (website/src/assets/images/.png) and matches +// the reference used by the docs page (e.g. `e0001` → e0001.png). + +// Rule screenshots — the "# Error" snippet from docs/rules/*.md, crafted so that +// exactly the documented rule fires (e.g. e0001 keeps `-> str` so only E0001, +// not E0002, is reported). +const RULE_SHOTS = [ + { + name: "e0001", + expect: "BSK-E0001", + code: `def process(data) -> str: + return data.upper() +`, + }, + { + name: "e0002", + expect: "BSK-E0002", + code: `def get_user(user_id: int): + return {"id": user_id} +`, + }, + { + name: "e0003", + expect: "BSK-E0003", + code: `data = [] +`, + }, + { + name: "e0004", + expect: "BSK-E0004", + code: `def log(*args, **kwargs) -> None: + print(args, kwargs) +`, + }, + { + name: "e0005", + expect: "BSK-E0005", + code: `class Registry: + entries = [] +`, + }, + { + name: "e0010", + expect: "BSK-E0010", + code: `from legacy_module import process_data +`, + }, + { + name: "e0011", + expect: "BSK-W0014", + code: `from typing import Any + + +def handle(data: Any) -> bool: + return True +`, + }, + { + name: "e0012", + expect: "BSK-E0012", + code: `def greet(name: str) -> str: + return f"Hello, {name}" + + +greet(42) +`, + }, + { + name: "e0013", + expect: "BSK-E0013", + code: `def get_count() -> int: + return "many" +`, + }, + { + name: "e0014", + expect: "BSK-E0014", + code: `count: int = "zero" +`, + }, + { + name: "e0015", + expect: "BSK-E0015", + code: `x: dict[str] = {} +`, + }, + { + name: "e0016", + expect: "BSK-E0016", + code: `from typing import override + + +class Base: + def process(self, data: str) -> str: + return data + + +class Child(Base): + @override + def process(self, data: int) -> str: + return str(data) +`, + }, + { + name: "e0018", + expect: "BSK-E0018", + code: `def f() -> int: + return missing_local +`, + }, + { + name: "e0019", + expect: "BSK-E0019", + code: `def check(flag: bool) -> str: + if flag: + result = "yes" + return result +`, + }, + { + name: "e0025", + expect: "BSK-E0025", + code: `class Base: + def process(self) -> str: + return "base" + + +class Child(Base): + def process(self) -> str: + return "child" +`, + }, + { + name: "e0017", + expect: "BSK-E0017", + code: `class Base: + x: int + + +class Child(Base): + x: str +`, + }, + { + name: "e0020", + expect: "BSK-E0020", + code: `from typing import overload + + +@overload +def f(x: int) -> int: ... +@overload +def f(x: str) -> str: ... +`, + }, + { + name: "e0023", + expect: "BSK-E0023", + code: `def classify(x: int | str) -> str: + match x: + case int(): + return "number" +`, + }, + { + name: "e0026", + expect: "BSK-E0026", + code: `from typing import TypeVar + +T = TypeVar("T", int) +`, + }, + { + name: "e0027", + expect: "BSK-E0027", + code: `from typing import Generic, TypeVar + +T = TypeVar("T") + + +class Box(Generic[T, T]): + ... +`, + }, + { + name: "e0029", + expect: "BSK-E0029", + code: `from typing import TypedDict + + +class Movie(TypedDict): + title: str + + def play(self) -> None: + ... +`, + }, + { + name: "e0031", + expect: "BSK-E0031", + code: `from typing import cast + +x = cast(int) +`, + }, + { + name: "e0033", + expect: "BSK-E0033", + code: `reveal_type() +`, + }, + { + name: "e0040", + expect: "BSK-E0040", + code: `from enum import Enum + + +class Base(Enum): + A = 1 + + +class Sub(Base): + B = 2 +`, + }, + { + name: "e0041", + expect: "BSK-E0041", + code: `def add(x: int, y: int) -> int: + return x + y + + +add(1) +`, + }, + { + name: "e0099", + expect: "BSK-E0099", + code: `from typing import Protocol + + +class P(Protocol): + def f(self) -> None: ... + + +P() +`, + }, + { + name: "e0115", + expect: "BSK-E0115", + code: `from warnings import deprecated + + +@deprecated("use bar") +def foo() -> None: ... + + +foo() +`, + }, +]; + +// Homepage before/after demo. `bad.py` must report exactly six errors and +// `good.py` must be clean — these mirror the source shown in website/src/index.njk. +const HOME_SHOTS = [ + { + name: "cli-demo", + file: "bad.py", + expect: "Found 6 diagnostics", + code: `def process(data): + return data.upper() + +class User: + def __init__(self, name, age): + self.name = name + self.age = age + + def greet(self): + return f"Hello, {self.name}" +`, + }, + { + name: "cli-clean", + file: "good.py", + expect: "No issues found", + code: `def process(data: str) -> str: + return data.upper() + + +class User: + name: str + age: int + + def __init__(self, name: str, age: int) -> None: + self.name = name + self.age = age + + def greet(self) -> str: + return f"Hello, {self.name}" +`, + }, +]; + +// A rule shot's source file is named after the image stem (e0001 → e0001.py); a +// home shot carries its own filename so the prompt reads `basilisk check bad.py`. +export const SHOTS = [ + ...RULE_SHOTS.map((shot) => ({ ...shot, file: `${shot.name}.py` })), + ...HOME_SHOTS, +]; diff --git a/website/screenshots/terminal.mjs b/website/screenshots/terminal.mjs new file mode 100644 index 00000000..caf4e155 --- /dev/null +++ b/website/screenshots/terminal.mjs @@ -0,0 +1,109 @@ +// Implements [WEBSITE-SCREENSHOTS-CHROME]: the macOS Terminal.app window chrome +// (traffic-light buttons, folder + title bar, dark body) the CLI screenshots are +// framed in. See docs/specs/WEBSITE-SCREENSHOTS-SPEC.md. +// +// This reproduces in HTML what the old manual process captured with +// Terminal.app + screencapture: a 120-column window titled "basilisk-demo — -zsh" +// on the default dark profile, so regenerated images are visually identical to +// the originals but fully reproducible and PII-free. + +import { ansiToHtml } from "./ansi.mjs"; + +// 120-column window, matching the original `120×26` captures. Width is fixed in +// `ch` so every screenshot lines up at the same column width; height is content. +const COLUMNS = 120; +const TITLE = "basilisk-demo — -zsh"; + +// macOS "open folder" Finder glyph, inlined so rendering needs no network/font. +const FOLDER_ICON = ``; + +const STYLE = ` + * { margin: 0; padding: 0; box-sizing: border-box; } + html, body { background: transparent; } + body { padding: 24px; display: inline-block; } + .window { + display: inline-block; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 22px 70px rgba(0, 0, 0, 0.55); + font-family: "SF Mono", "Menlo", "Monaco", "Consolas", monospace; + } + .titlebar { + position: relative; + height: 30px; + display: flex; + align-items: center; + padding: 0 12px; + background: linear-gradient(#3c3c3e, #303032); + border-bottom: 1px solid #1f1f21; + } + .lights { display: flex; gap: 8px; } + .light { width: 13px; height: 13px; border-radius: 50%; } + .light.close { background: #ff5f57; border: 0.5px solid #e0443e; } + .light.min { background: #febc2e; border: 0.5px solid #dea123; } + .light.expand { background: #28c840; border: 0.5px solid #1aab29; } + .title { + position: absolute; + left: 0; right: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font: 500 13px -apple-system, "SF Pro Text", "Helvetica Neue", sans-serif; + color: #c7c7c9; + pointer-events: none; + } + .body { + background: rgb(30, 30, 30); + color: #d6d6d6; + padding: 14px 18px 16px; + font-size: 12px; + line-height: 1.5; + width: ${COLUMNS}ch; + } + .body pre { + font-family: inherit; + font-size: inherit; + white-space: pre-wrap; + word-break: break-word; + tab-size: 4; + } + .prompt { color: #d6d6d6; } +`; + +// One Terminal "size" suffix per window, e.g. "120×26", computed from the lines +// shown so the title bar reads like a real session. +const sizeSuffix = (lineCount) => `${COLUMNS}×${Math.max(lineCount, 26)}`; + +/** + * Build a complete HTML document for one screenshot. + * + * @param {string} command - the command echoed after the prompt, e.g. "basilisk check e0001.py". + * @param {string} ansiOutput - raw stdout from the binary, including ANSI escapes. + */ +export const buildTerminalHtml = ({ command, ansiOutput }) => { + const outputHtml = ansiToHtml(ansiOutput.replace(/\n+$/, "")); + const lineCount = ansiOutput.split("\n").length + 3; + const body = `$ ${command}\n${outputHtml}\n$ `; + + return ` + + +
+
+
+ + + +
+
${FOLDER_ICON}${TITLE} — ${sizeSuffix(lineCount)}
+
+
${body}
+
+`; +}; + +export const WINDOW_SELECTOR = ".window"; diff --git a/website/src/_data/examples.js b/website/src/_data/examples.js new file mode 100644 index 00000000..0fbc33e7 --- /dev/null +++ b/website/src/_data/examples.js @@ -0,0 +1,17 @@ +// Implements [WEBSITE-ERROR-PAGES-EXAMPLES]: map each diagnostic code to the +// worked-example screenshot that demonstrates it, so /errors// can embed +// the real `basilisk check` output. See docs/specs/WEBSITE-ERROR-PAGES-SPEC.md. +// +// The screenshot manifest is the single source of truth: each rule shot records +// the exact code it triggers in `expect` (e.g. e0011 → BSK-W0014), so we key off +// that rather than the filename to stay correct even where they differ. +import { SHOTS } from "../../screenshots/shots.mjs"; + +const RULE_SHOT = /^e\d+$/; +const RULE_CODE = /^BSK-[EW]\d{4}$/; + +export default Object.fromEntries( + SHOTS.filter((shot) => RULE_SHOT.test(shot.name) && RULE_CODE.test(shot.expect)).map( + (shot) => [shot.expect, shot.name], + ), +); diff --git a/website/src/_data/ruleGroups.js b/website/src/_data/ruleGroups.js new file mode 100644 index 00000000..d8a57313 --- /dev/null +++ b/website/src/_data/ruleGroups.js @@ -0,0 +1,17 @@ +// Implements [WEBSITE-ERROR-PAGES]: group the diagnostic codes for the error +// reference directory. Done in data (not a Nunjucks selectattr, which does not +// filter reliably here) so /errors/ lists each group exactly once, in order. +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const rules = JSON.parse(readFileSync(join(here, "rules.json"), "utf8")); + +const ORDER = ["Missing Annotations", "Type Safety", "Type System", "Warnings"]; + +export default ORDER.map((group) => ({ + group, + id: group.toLowerCase().replace(/\s+/g, "-"), + items: rules.filter((rule) => rule.group === group), +})).filter((group) => group.items.length > 0); diff --git a/website/src/_data/ruleStats.js b/website/src/_data/ruleStats.js new file mode 100644 index 00000000..eb4e711c --- /dev/null +++ b/website/src/_data/ruleStats.js @@ -0,0 +1,12 @@ +// Implements [WEBSITE-ERROR-PAGES]: headline counts for the rules overview, kept +// in sync with the checker source so the prose can never drift from the table. +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const rules = JSON.parse(readFileSync(join(here, "rules.json"), "utf8")); + +const errors = rules.filter((rule) => rule.severity === "error").length; + +export default { total: rules.length, errors, warnings: rules.length - errors }; diff --git a/website/src/_data/rules.json b/website/src/_data/rules.json new file mode 100644 index 00000000..6b356a7a --- /dev/null +++ b/website/src/_data/rules.json @@ -0,0 +1,3491 @@ +[ + { + "code": "BSK-E0001", + "severity": "error", + "summary": "Missing parameter type annotation", + "summaryHtml": "Missing parameter type annotation", + "body": [], + "group": "Missing Annotations", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0001" + }, + { + "code": "BSK-E0002", + "severity": "error", + "summary": "Missing return type annotation", + "summaryHtml": "Missing return type annotation", + "body": [], + "group": "Missing Annotations", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0002" + }, + { + "code": "BSK-E0003", + "severity": "error", + "summary": "Missing variable type annotation", + "summaryHtml": "Missing variable type annotation", + "body": [ + { + "type": "text", + "html": "Fires when a module-level variable has no type annotation. In strict mode every module-level binding must carry an explicit annotation so that Basilisk can verify downstream usage and generate accurate stubs." + } + ], + "group": "Missing Annotations", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0003" + }, + { + "code": "BSK-E0004", + "severity": "error", + "summary": "Missing `*args` / `**kwargs` type annotation", + "summaryHtml": "Missing *args / **kwargs type annotation", + "body": [ + { + "type": "text", + "html": "Every variadic positional parameter (*args) and variadic keyword parameter (**kwargs) must carry an explicit type annotation in strict Basilisk code." + } + ], + "group": "Missing Annotations", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0004" + }, + { + "code": "BSK-E0005", + "severity": "error", + "summary": "Missing class attribute type annotation", + "summaryHtml": "Missing class attribute type annotation", + "body": [ + { + "type": "text", + "html": "Every class attribute declared in the class body must have an explicit type annotation. Without one, Basilisk cannot verify assignments to the attribute and cannot produce accurate stub types." + }, + { + "type": "text", + "html": "Enum subclasses and Protocol subclasses are exempt: Enum members have metaclass-synthesised Literal... types, and Protocol attributes are interface specifications rather than concrete class variables." + } + ], + "group": "Missing Annotations", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0005" + }, + { + "code": "BSK-E0010", + "severity": "error", + "summary": "Unresolved import", + "summaryHtml": "Unresolved import", + "body": [ + { + "type": "text", + "html": "Fires when an import cannot be resolved and the module is not part of the Python standard library. When uv package-registry context is available the diagnostic message explains why the import failed (not installed, transitive-only, needs sync, wrong Python version). Without that context a generic fallback message is used." + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0010" + }, + { + "code": "BSK-E0011", + "severity": "error", + "summary": "Return type mismatch", + "summaryHtml": "Return type mismatch", + "body": [ + { + "type": "text", + "html": "Emitted as an Error when the literal value returned by a function is clearly incompatible with the declared return type annotation (e.g. returning an int literal from a -> str function)." + }, + { + "type": "text", + "html": "The explicit-Any warning used to share this code; it now lives under its own warning code (BSK-W0014) so the opinionated style nudge can be silenced independently of this genuine type-safety error." + }, + { + "type": "code", + "lang": "python", + "code": "# BAD (return type mismatch)\ndef count() -> str:\n return 42 # E: int literal is not assignable to str\n\n# GOOD\ndef count() -> int:\n return 42" + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0011" + }, + { + "code": "BSK-E0012", + "severity": "error", + "summary": "Argument type mismatch at a call site", + "summaryHtml": "Argument type mismatch at a call site", + "body": [ + { + "type": "text", + "html": "When a function is called with a literal argument whose type is clearly incompatible with the declared parameter annotation, Basilisk reports the mismatch. The check mirrors the literal-kind vs annotation comparison used by BSK-E0014." + }, + { + "type": "code", + "lang": "python", + "code": "def add(x: int, y: int) -> int:\n return x + y\n\nresult: int = add(\"hello\", \"world\") # str literals for int params \u2192 E0012" + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0012" + }, + { + "code": "BSK-E0013", + "severity": "error", + "summary": "Return type mismatch \u2014 inferred return type incompatible with annotation", + "summaryHtml": "Return type mismatch \u2014 inferred return type incompatible with annotation", + "body": [ + { + "type": "text", + "html": "When a function has a return type annotation, the inferred return type must be assignable to the declared type. This extends the original -> None check to handle all return type mismatches using the inference system." + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0013" + }, + { + "code": "BSK-E0014", + "severity": "error", + "summary": "Assignment type incompatibility (literal mismatches)", + "summaryHtml": "Assignment type incompatibility (literal mismatches)", + "body": [ + { + "type": "text", + "html": "Detects annotated module-level variables where the declared type and the literal kind of the right-hand side are clearly incompatible, for example:" + }, + { + "type": "code", + "lang": "python", + "code": "count: int = \"hello\" # str literal assigned to int annotation \u2192 E0014\nlabel: str = 42 # int literal assigned to str annotation \u2192 E0014\nflag: bool = \"yes\" # str literal assigned to bool annotation \u2192 E0014\nratio: float = \"1.5\" # str literal assigned to float annotation \u2192 E0014" + }, + { + "type": "text", + "html": "The check is performed by extracting the annotation text from the source around the variable's name span and comparing it against the RHS kind." + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0014" + }, + { + "code": "BSK-E0015", + "severity": "error", + "summary": "Invalid type argument count or form", + "summaryHtml": "Invalid type argument count or form", + "body": [ + { + "type": "text", + "html": "Certain generic types accept a fixed number of type arguments. This rule catches the most common violations detectable from source text alone:" + }, + { + "type": "text", + "html": "| Annotation pattern | Expected args | Error condition | |---|---|---| | list... | exactly 1 | 0 or 2+ args | | set... | exactly 1 | 0 or 2+ args | | frozenset... | exactly 1 | 0 or 2+ args | | type... | exactly 1 | 0 or 2+ args | | Type... | exactly 1 | 0 or 2+ args | | dict... | exactly 2 | 0, 1, or 3+ args | | Callable... | exactly 2 | wrong count or invalid form |" + }, + { + "type": "text", + "html": "For Callable, the first argument must be a parameter list int, str, bare ellipsis ..., a ParamSpec, or Concatenate.... The second argument (return type) must not be a list literal." + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0015" + }, + { + "code": "BSK-E0016", + "severity": "error", + "summary": "Incompatible method override", + "summaryHtml": "Incompatible method override", + "body": [ + { + "type": "text", + "html": "When a class method marked with @override has a different parameter signature or return type than the corresponding method in a same-module base class, Basilisk reports an incompatible override." + }, + { + "type": "text", + "html": "The check compares annotation text extracted from the source for non-self parameters and the return type. The self/cls parameter is always skipped since its type naturally differs between base and child class." + }, + { + "type": "code", + "lang": "python", + "code": "class Base:\n def process(self: Base, data: str) -> str: ...\n\nclass Child(Base):\n @override\n def process(self: Child, data: int) -> int: ... # E0016" + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0016" + }, + { + "code": "BSK-E0017", + "severity": "error", + "summary": "Incompatible class attribute override", + "summaryHtml": "Incompatible class attribute override", + "body": [ + { + "type": "text", + "html": "When a child class declares an attribute that also exists in a same-module base class but with a different type annotation, Basilisk reports an incompatible override." + }, + { + "type": "code", + "lang": "python", + "code": "class Base:\n count: int = 0\n\nclass Child(Base):\n count: str = \"zero\" # annotation changed from int to str \u2192 E0017" + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0017" + }, + { + "code": "BSK-E0018", + "severity": "error", + "summary": "Undefined variable used in a return statement", + "summaryHtml": "Undefined variable used in a return statement", + "body": [ + { + "type": "text", + "html": "Flags any name referenced in a return expression \u2014 bare (return x), the base of an attribute/subscript chain (return x.y), a call argument, or the **callee of a call** (return x()) \u2014 that is not defined in scope. A name is considered defined if it is a parameter, a local assignment (=, for, with), a module-level function, class, variable, or import, an enclosing scope's binding, a cross-module imported symbol, or a builtin." + }, + { + "type": "code", + "lang": "python", + "code": "def compute() -> int:\n return undefined_name # never defined \u2192 E0018\n return undefined_fn() # undefined callee \u2192 E0018" + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0018" + }, + { + "code": "BSK-E0019", + "severity": "error", + "summary": "Unbound variable on some code paths", + "summaryHtml": "Unbound variable on some code paths", + "body": [ + { + "type": "text", + "html": "When a function contains a return <name> statement and the name is assigned in the function body, but only inside conditional branches (e.g. if, while, try), it may be unbound when the return is reached on other paths." + }, + { + "type": "code", + "lang": "python", + "code": "def maybe_assign(flag: bool) -> int:\n if flag:\n result = 42\n return result # result may be unbound if flag is False \u2192 E0019" + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0019" + }, + { + "code": "BSK-E0020", + "severity": "error", + "summary": "Missing `@overload` implementation", + "summaryHtml": "Missing @overload implementation", + "body": [ + { + "type": "text", + "html": "When a function name is defined multiple times and every definition carries the @overload decorator, there is no concrete implementation body. Python's typing.overload protocol requires exactly one implementation function without @overload." + }, + { + "type": "text", + "html": "This rule fires once per overload group that lacks a plain implementation." + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0020" + }, + { + "code": "BSK-E0021", + "severity": "error", + "summary": "Overlapping `@overload` signatures", + "summaryHtml": "Overlapping @overload signatures", + "body": [ + { + "type": "text", + "html": "Within a group of @overload functions for the same name, every overload must be distinguishable. This rule uses a structural heuristic: two overloads are considered overlapping when they have the same parameter count AND identical parameter names in the same order." + }, + { + "type": "text", + "html": "A diagnostic is emitted for the later overload in each conflicting pair, pointing at its name span." + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0021" + }, + { + "code": "BSK-E0022", + "severity": "error", + "summary": "Unhashable type used as a dict key", + "summaryHtml": "Unhashable type used as a dict key", + "body": [ + { + "type": "text", + "html": "Lists, sets, and plain dicts are not hashable and cannot be used as dictionary keys at runtime. Basilisk detects these statically." + }, + { + "type": "code", + "lang": "python", + "code": "def bad_key() -> None:\n mapping = {[1, 2]: \"value\"} # list as key \u2192 E0022" + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0022" + }, + { + "code": "BSK-E0023", + "severity": "error", + "summary": "Non-exhaustive `match` statement", + "summaryHtml": "Non-exhaustive match statement", + "body": [ + { + "type": "text", + "html": "A value-dispatch match statement that has no irrefutable branch may fail to handle certain runtime values, leading to a silent fall-through (Python does not raise an error for unmatched match subjects). Basilisk treats this as an error in strict mode." + }, + { + "type": "text", + "html": "Two cases are not flagged, matching the reference checkers: a bare capture case name: (no guard) is irrefutable \u2014 like case _:, it makes the match exhaustive; a structural match (sequence/mapping patterns) decomposes open-ended shapes \u2014 e.g. narrowing a tuple union of mixed arity \u2014 where a catch-all is not required for correctness." + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0023" + }, + { + "code": "BSK-E0024", + "severity": "error", + "summary": "Invalid type form \u2014 numeric literal used as type annotation", + "summaryHtml": "Invalid type form \u2014 numeric literal used as type annotation", + "body": [ + { + "type": "text", + "html": "Type annotations must be type expressions, not literal values. Using a number such as 42, 3.14, or True as a type annotation is always a mistake (it is valid Python syntax but meaningless as a type)." + }, + { + "type": "code", + "lang": "python", + "code": "def f(x: 42) -> 0: # both parameter and return annotation are literals\n ..." + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0024" + }, + { + "code": "BSK-E0025", + "severity": "error", + "summary": "Missing `@override` decorator", + "summaryHtml": "Missing @override decorator", + "body": [ + { + "type": "text", + "html": "When a class overrides a method that is also defined in one of its base classes (both defined within the same module), the overriding method must carry the @override decorator (PEP 698 / typing.override)." + }, + { + "type": "text", + "html": "The check is limited to base classes that appear in the same source module, because Basilisk cannot inspect the base class body without resolving cross-module imports in Phase 1." + }, + { + "type": "text", + "html": "Protocol implementations are exempt: when a class satisfies a Protocol contract, it is expected to define the protocol methods without @override." + }, + { + "type": "text", + "html": "Version gate (issue #171): @override (PEP 698 / typing.override) was introduced in Python 3.12, so suggesting it on an older configured target is a false positive \u2014 the decorator cannot be imported there. E0025 is silent when the configured python_version is below 3.12." + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0025" + }, + { + "code": "BSK-E0026", + "severity": "error", + "summary": "`TypeVar` declared with exactly one constraint", + "summaryHtml": "TypeVar declared with exactly one constraint", + "body": [ + { + "type": "text", + "html": "PEP 484 requires a TypeVar to have either zero constraints (unconstrained) or two or more constraints. A single constraint makes no sense because it would be equivalent to using the type directly." + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0026" + }, + { + "code": "BSK-E0027", + "severity": "error", + "summary": "Duplicate `TypeVar` in a `Generic[...]` base", + "summaryHtml": "Duplicate TypeVar in a Generic... base", + "body": [ + { + "type": "text", + "html": "Each type parameter in GenericT1, T2, ... must be unique. GenericT, T is an error per PEP 484." + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0027" + }, + { + "code": "BSK-E0029", + "severity": "error", + "summary": "Method defined inside a `TypedDict` class", + "summaryHtml": "Method defined inside a TypedDict class", + "body": [ + { + "type": "text", + "html": "TypedDict classes (PEP 589) are restricted to key declarations only. Defining methods (other than __init__ which is synthesised) is an error." + } + ], + "group": "Type Safety", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0029" + }, + { + "code": "BSK-E0030", + "severity": "error", + "summary": "Non-default `TypeVar` follows a default `TypeVar` in `Generic[...]`", + "summaryHtml": "Non-default TypeVar follows a default TypeVar in Generic...", + "body": [ + { + "type": "text", + "html": "PEP 696 \u00a7Ordering defines two ordering rules for type parameters in Generic...:" + }, + { + "type": "text", + "html": "1. Once a TypeVar with a default= argument appears, all subsequent type variables must also have defaults." + }, + { + "type": "text", + "html": "2. A TypeVar with a default= cannot immediately follow a TypeVarTuple in Generic... because it would be ambiguous whether a type argument should be bound to the TypeVarTuple or the defaulted TypeVar. (ParamSpec with a default is allowed after a TypeVarTuple.)" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0030" + }, + { + "code": "BSK-E0031", + "severity": "error", + "summary": "Invalid `cast()` call", + "summaryHtml": "Invalid cast() call", + "body": [ + { + "type": "text", + "html": "typing.cast(typ, val) must be called with exactly two positional arguments, and the first argument must be a type expression, not a value literal." + }, + { + "type": "text", + "html": "- cast() \u2014 too few arguments - cast(1, x) \u2014 first argument is a literal, not a type - cast(int, x, y) \u2014 too many arguments" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0031" + }, + { + "code": "BSK-E0032", + "severity": "error", + "summary": "Invalid keyword argument in `TypedDict` class definition", + "summaryHtml": "Invalid keyword argument in TypedDict class definition", + "body": [ + { + "type": "text", + "html": "TypedDict class syntax only accepts total=True/False as a keyword argument. Using metaclass= or any unrecognised keyword is an error per PEP 589." + }, + { + "type": "text", + "html": "Also fires when a TypedDict inherits from a non-TypedDict class (other than Generic...), which is forbidden." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0032" + }, + { + "code": "BSK-E0033", + "severity": "error", + "summary": "Invalid `reveal_type()` call", + "summaryHtml": "Invalid reveal_type() call", + "body": [ + { + "type": "text", + "html": "reveal_type(expr) must be called with exactly one positional argument." + }, + { + "type": "text", + "html": "- reveal_type() \u2014 too few arguments (0 given) - reveal_type(a, b) \u2014 too many arguments (2 given)" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0033" + }, + { + "code": "BSK-E0034", + "severity": "error", + "summary": "`@final` decorator violations", + "summaryHtml": "@final decorator violations", + "body": [ + { + "type": "text", + "html": "Three violations are detected:" + }, + { + "type": "text", + "html": "1. **Inheriting from a @final class** \u2014 a class decorated with @final cannot be subclassed." + }, + { + "type": "text", + "html": "2. **@final on a non-method function** \u2014 @final is only valid on methods defined inside a class body, not on module-level functions." + }, + { + "type": "text", + "html": "3. **Overriding a @final method** \u2014 a method decorated with @final in a base class cannot be overridden in a subclass." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0034" + }, + { + "code": "BSK-E0035", + "severity": "error", + "summary": "`Required` / `NotRequired` used in an invalid context", + "summaryHtml": "Required / NotRequired used in an invalid context", + "body": [ + { + "type": "text", + "html": "PEP 655 and the typing spec restrict RequiredT and NotRequiredT to:" + }, + { + "type": "text", + "html": "- Annotations of TypedDict fields" + }, + { + "type": "text", + "html": "Using them outside of a TypedDict body (in regular classes, function parameters, variable annotations, etc.) is an error." + }, + { + "type": "text", + "html": "Additionally, nesting Required or NotRequired inside each other is forbidden even within a TypedDict." + }, + { + "type": "code", + "lang": "python", + "code": "class NotTypedDict:\n x: Required[int] # E0035 \u2014 not a TypedDict\n\ndef func(x: NotRequired[int]) -> None: # E0035 \u2014 not a TypedDict field\n ...\n\nclass TD(TypedDict):\n a: Required[Required[int]] # E0035 \u2014 nested Required" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0035" + }, + { + "code": "BSK-E0036", + "severity": "error", + "summary": "`ClassVar` used in an invalid context", + "summaryHtml": "ClassVar used in an invalid context", + "body": [ + { + "type": "text", + "html": "PEP 526 and the typing spec restrict ClassVarT to:" + }, + { + "type": "text", + "html": "- Annotations of class body attributes (class variables)" + }, + { + "type": "text", + "html": "Using ClassVar outside a class body (in function parameters, return types, local variable annotations, or module-level variable annotations) is an error. Additionally, nesting ClassVar inside another type constructor (e.g. FinalClassVar[int] or listClassVar[int]) is forbidden." + }, + { + "type": "text", + "html": "Note: AnnotatedClassVar[T, ...] is a valid exception." + }, + { + "type": "text", + "html": "This rule also validates ClassVar argument correctness: - ClassVar accepts at most one argument - The argument must be a valid type (not a literal or runtime variable) - The argument must not contain TypeVar, ParamSpec, or TypeVarTuple" + }, + { + "type": "text", + "html": "Additionally, ClassVar attributes cannot be assigned via instances." + }, + { + "type": "code", + "lang": "python", + "code": "class MyClass:\n bad9: Final[ClassVar[int]] = 3 # E0036 \u2014 ClassVar cannot be nested\n bad10: list[ClassVar[int]] = [] # E0036 \u2014 ClassVar cannot be nested\n\n def method1(self, a: ClassVar[int]): # E0036 \u2014 ClassVar not allowed here\n x: ClassVar[str] = \"\" # E0036 \u2014 ClassVar not allowed here\n self.xx: ClassVar[str] = \"\" # E0036 \u2014 ClassVar not allowed here\n\n def method2(self) -> ClassVar[int]: # E0036 \u2014 ClassVar not allowed here\n ...\n\nbad11: ClassVar[int] = 3 # E0036 \u2014 ClassVar not allowed at module level\nbad12: TypeAlias = ClassVar[str] # E0036 \u2014 ClassVar not allowed here" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0036" + }, + { + "code": "BSK-E0037", + "severity": "error", + "summary": "Invalid `TypedDict(...)` functional-syntax call", + "summaryHtml": "Invalid TypedDict(...) functional-syntax call", + "body": [ + { + "type": "text", + "html": "The TypedDict(name, {...}) functional syntax has several constraints:" + }, + { + "type": "text", + "html": "1. The second positional argument must be a dict literal {...}. 2. All keys in the dict literal must be string literals. 3. The first positional argument (the declared name) must match the variable name on the left-hand side of the assignment. 4. Only total= is recognised as a keyword argument; anything else is an error." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0037" + }, + { + "code": "BSK-E0038", + "severity": "error", + "summary": "Invalid `TypedDict` inheritance", + "summaryHtml": "Invalid TypedDict inheritance", + "body": [ + { + "type": "text", + "html": "PEP 589 and the typing spec place constraints on TypedDict inheritance:" + }, + { + "type": "text", + "html": "1. A TypedDict cannot inherit from both a TypedDict and a non-TypedDict base class (except Generic)." + }, + { + "type": "text", + "html": "2. A TypedDict subclass cannot change the type of a field declared in a parent TypedDict class. PEP 705 refines this for the ReadOnly, Required, and NotRequired qualifiers: - A writable (non-ReadOnly) item may not be redeclared ReadOnly. - A required item may not be redeclared as not-required. - A writable item's value type is invariant; a ReadOnly item's value type may be narrowed to a subtype." + }, + { + "type": "text", + "html": "3. Multiple TypedDict inheritance is not allowed when two bases declare the same field with conflicting types or qualifiers." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0038" + }, + { + "code": "BSK-E0039", + "severity": "error", + "summary": "Invalid `assert_type()` call", + "summaryHtml": "Invalid assert_type() call", + "body": [ + { + "type": "text", + "html": "assert_type(expr, Type) must be called with exactly 2 positional arguments." + }, + { + "type": "text", + "html": "- assert_type() \u2014 too few arguments (0 given) - assert_type(x) \u2014 too few arguments (1 given) - assert_type(x, int, extra) \u2014 too many arguments (3 given)" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0039" + }, + { + "code": "BSK-E0040", + "severity": "error", + "summary": "Invalid Enum subclassing", + "summaryHtml": "Invalid Enum subclassing", + "body": [ + { + "type": "text", + "html": "An Enum class with one or more defined members is implicitly final and cannot be subclassed. Only Enum subclasses with no members can be used as bases for other Enum classes." + }, + { + "type": "code", + "lang": "python", + "code": "class Color(Enum):\n RED = 1\n GREEN = 2\n\nclass ExtendedColor(Color): # E \u2014 Color has members and is implicitly final\n BLUE = 3" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0040" + }, + { + "code": "BSK-E0041", + "severity": "error", + "summary": "Too few arguments in a function call", + "summaryHtml": "Too few arguments in a function call", + "body": [ + { + "type": "text", + "html": "When a function is called with fewer positional arguments than it has required parameters (parameters without default values), Basilisk reports a missing-argument error. Handles overloaded functions by checking all overload signatures." + }, + { + "type": "text", + "html": "Also validates constructor calls: when a class is instantiated and the metaclass __call__ passes through arguments (uses *args, **kwargs), the __new__ or __init__ method signature is checked for missing required arguments." + }, + { + "type": "code", + "lang": "python", + "code": "def func1(a: int, b: str) -> None: ...\n\nfunc1() # E: missing required arguments\nfunc1(1) # E: missing required argument `b`" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0041" + }, + { + "code": "BSK-E0042", + "severity": "error", + "summary": "PEP 695 type parameter syntax mixed with traditional `TypeVars`", + "summaryHtml": "PEP 695 type parameter syntax mixed with traditional TypeVars", + "body": [ + { + "type": "text", + "html": "PEP 695 introduced a new syntax for declaring type parameters (class FooT and def fooT()). When a class or function uses this new syntax, it must not reference traditional TypeVar instances from an outer scope in its base classes or parameter annotations." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import TypeVar\n\nK = TypeVar(\"K\")\n\nclass ClassA[V](dict[K, V]): # E: traditional TypeVar K used with PEP 695 syntax\n ..." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0042" + }, + { + "code": "BSK-E0043", + "severity": "error", + "summary": "Non-TypeVar argument in `Generic[...]` or `Protocol[...]`", + "summaryHtml": "Non-TypeVar argument in Generic... or Protocol...", + "body": [ + { + "type": "text", + "html": "PEP 484 requires that all arguments to Generic... and Protocol... be type variable names (TypeVar, TypeVarTuple, or ParamSpec). Passing a concrete type (e.g. Genericint) is a type error." + }, + { + "type": "code", + "lang": "python", + "code": "class Bad1(Generic[int]): ... # E \u2014 `int` is not a TypeVar\nclass Bad2(Protocol[int]): ... # E \u2014 `int` is not a TypeVar" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0043" + }, + { + "code": "BSK-E0044", + "severity": "error", + "summary": "`Final` used in an invalid position", + "summaryHtml": "Final used in an invalid position", + "body": [ + { + "type": "text", + "html": "PEP 591 restricts FinalT to:" + }, + { + "type": "text", + "html": "- Module-level variable annotations (x: Finalint = 1) - Class body attribute annotations (VALUE: Finalint = 1) - Instance attribute annotations in __init__ (self.x: Finalint = 1)" + }, + { + "type": "text", + "html": "The following are all errors:" + }, + { + "type": "text", + "html": "1. Final used in a function parameter annotation 2. Final nested inside another type constructor (e.g. listFinal[int]) 3. FinalClassVar[...] or ClassVarFinal[...] \u2014 mutually exclusive 4. FinalT1, T2 \u2014 more than one type argument 5. Bare Final (no type arg, no initializer) at module level" + }, + { + "type": "code", + "lang": "python", + "code": "x: list[Final[int]] = [] # E \u2014 Final nested in list\ndef f(x: Final[int]): ... # E \u2014 Final in param\nVALUE2: ClassVar[Final] = 1 # E \u2014 Final with ClassVar\nBAD1: Final # E \u2014 bare Final, no assignment\nBAD2: Final[str, int] = \"\" # E \u2014 too many type args" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0044" + }, + { + "code": "BSK-E0045", + "severity": "error", + "summary": "Invalid first argument to `Annotated[...]`", + "summaryHtml": "Invalid first argument to Annotated...", + "body": [ + { + "type": "text", + "html": "PEP 593 requires that the first argument to Annotated... be a valid type expression. The following are errors:" + }, + { + "type": "text", + "html": "- List literals: Annotated[int, str, ""] - Tuple literals: Annotated((int, str),), "" - Dict literals: Annotated{"a": "b"}, "" - List comprehensions: Annotated[x for x in ..., ""] - Lambda calls: Annotated(lambda: int)(), "" - Conditional expressions: Annotatedint if cond else str, "" - Boolean literals: AnnotatedTrue, "" - Integer literals: Annotated1, "" - Binary boolean operators: Annotatedlist or set, "" - F-strings: Annotatedf"...", "" - Subscript-into-subscript: Annotated[int0, ""]" + }, + { + "type": "text", + "html": "Additionally, Annotatedint with fewer than 2 arguments is an error, and calling Annotated directly (bare or parameterized) is always invalid." + }, + { + "type": "code", + "lang": "python", + "code": "Bad1: Annotated[[int, str], \"\"] # E \u2014 list literal not valid type\nBad9: Annotated[True, \"\"] # E \u2014 bool literal not valid type\nBad13: Annotated[int] # E \u2014 requires at least two arguments\nAnnotated() # E \u2014 Annotated is not callable\nSmallInt(1) # E \u2014 TypeAlias is not callable" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0045" + }, + { + "code": "BSK-E0046", + "severity": "error", + "summary": "Enum member annotated with an explicit type", + "summaryHtml": "Enum member annotated with an explicit type", + "body": [ + { + "type": "text", + "html": "In an Enum class, members should NOT carry explicit type annotations. If an attribute inside an Enum class body has both a type annotation and an assigned value, it is treated as an annotated member \u2014 which is an error because the type checker infers a LiteralEnumClass.member type for all members automatically." + }, + { + "type": "text", + "html": "A type annotation without an assigned value (e.g. genus: str) is a **non-member attribute** and is valid." + }, + { + "type": "code", + "lang": "python", + "code": "from enum import Enum\n\nclass Pet(Enum):\n genus: str # OK \u2014 non-member attribute (annotation only, no value)\n CAT = \"felis\" # OK \u2014 member without annotation\n DOG: int = 2 # E \u2014 member with explicit type annotation" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0046" + }, + { + "code": "BSK-E0047", + "severity": "error", + "summary": "Invalid type expression in annotation", + "summaryHtml": "Invalid type expression in annotation", + "body": [ + { + "type": "text", + "html": "PEP 484 requires that annotations contain valid type expressions. Only certain expression forms are valid as types:" + }, + { + "type": "text", + "html": "- Names (int, str, MyClass) - Subscripts (listint, dictstr, int) - Binary-or unions (int | str) - String literals (forward references) - None - ... (Ellipsis, in Callable signatures)" + }, + { + "type": "text", + "html": "The following are invalid and should be flagged:" + }, + { + "type": "text", + "html": "- List literals: int, str - Dict literals: {} - Tuple literals: (int, str) - List comprehensions: int for i in range(1) - Lambda expressions (called or uncalled) - Conditional expressions: int if cond else str - Boolean binary operators: int or str, int and str - F-string literals: f"int" - Explicit function calls like eval(...) - Negative numeric literals (positive are caught by E0024) - Names that refer to module objects (import types \u2192 types is a module, not a type) - Names that refer to unannotated literal variables (var1 = 3 \u2192 var1 is int, not a type)" + }, + { + "type": "code", + "lang": "python", + "code": "def f(x: [int, str]): ... # E \u2014 list literal not a type\ndef g(x: int if True else str): ... # E \u2014 conditional not a type\ny: {} = {} # E \u2014 dict literal not a type" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0047" + }, + { + "code": "BSK-E0048", + "severity": "error", + "summary": "Invalid right-hand side for a `TypeAlias` annotation", + "summaryHtml": "Invalid right-hand side for a TypeAlias annotation", + "body": [ + { + "type": "text", + "html": "PEP 613 requires that the RHS of an explicit TypeAlias annotation must be a valid type expression. The following are errors:" + }, + { + "type": "text", + "html": "- List literals: x: TypeAlias = int, str - Tuple literals: x: TypeAlias = ((int, str),) - Dict literals: x: TypeAlias = {"a": "b"} - List comprehensions: x: TypeAlias = int for i in range(1) - Lambda calls: x: TypeAlias = (lambda: int)() - Conditional expressions: x: TypeAlias = int if cond else str - Boolean literals: x: TypeAlias = True - Integer literals: x: TypeAlias = 1 - Binary boolean operators: x: TypeAlias = list or set - F-strings: x: TypeAlias = f"..." - Subscript-into-subscript: x: TypeAlias = int0 - Runtime calls: x: TypeAlias = eval("int")" + }, + { + "type": "code", + "lang": "python", + "code": "from typing import TypeAlias\nBadTypeAlias2: TypeAlias = [int, str] # E \u2014 list literal\nBadTypeAlias10: TypeAlias = True # E \u2014 bool literal" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0048" + }, + { + "code": "BSK-E0049", + "severity": "error", + "summary": "Multiple unbounded tuple components in a single tuple type", + "summaryHtml": "Multiple unbounded tuple components in a single tuple type", + "body": [ + { + "type": "text", + "html": "A tuple... type annotation may contain at most one unbounded component. An unbounded component is: - tupleT, ... \u2014 a starred subscript where the inner tuple is variadic - Ts / *<Name> \u2014 a starred TypeVarTuple unpack - Unpacktuple[T, ...] \u2014 the legacy unpack form" + }, + { + "type": "text", + "html": "For example, tupletuple[str, ..., tupleint, ...] is invalid because it has two unbounded components." + }, + { + "type": "code", + "lang": "python", + "code": "t: tuple[*tuple[str, ...], *tuple[int, ...]] # E \u2014 two unbounded components\nt: tuple[*tuple[str, ...], *Ts] # E \u2014 two unbounded components\nt: tuple[*tuple[str, ...], str] # OK \u2014 only one unbounded" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0049" + }, + { + "code": "BSK-E0050", + "severity": "error", + "summary": "Invalid `NewType(...)` call", + "summaryHtml": "Invalid NewType(...) call", + "body": [ + { + "type": "text", + "html": "PEP 484 places restrictions on NewType:" + }, + { + "type": "text", + "html": "- The string name must match the variable it is assigned to - The base type must be a proper concrete class - NewType accepts exactly two arguments" + }, + { + "type": "code", + "lang": "python", + "code": "from typing import NewType\nGoodName = NewType(\"BadName\", int) # E: name mismatch\nBadNewType6 = NewType(\"BadNewType6\", int, int) # E: too many arguments\nBadNewType7 = NewType(\"BadNewType7\", Any) # E: cannot be Any" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0050" + }, + { + "code": "BSK-E0051", + "severity": "error", + "summary": "Invalid `Literal` parameterization", + "summaryHtml": "Invalid Literal parameterization", + "body": [ + { + "type": "text", + "html": "PEP 586 restricts what values may appear inside Literal.... Only these are legal: - Integer literals (decimal, hex, binary, octal; optionally signed) - String literals (str and bytes) - Boolean literals (True, False) - None - Enum member access (Color.RED) - Nested Literal..." + }, + { + "type": "text", + "html": "Everything else is illegal, including: - Arithmetic / unary expressions (3 + 4, ~5, not False) - Function calls ("foo".replace(...)) - Containers ((1, 2), {"a": "b"}) - Type objects, TypeVars, Any (Literalint, LiteralT) - Float literals (3.14) - Ellipsis (...) - Bare Literal with no arguments - Variables and function objects" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0051" + }, + { + "code": "BSK-E0052", + "severity": "error", + "summary": "Assignment to attribute of a frozen dataclass instance, or invalid frozen/non-frozen dataclass inheritance", + "summaryHtml": "Assignment to attribute of a frozen dataclass instance, or invalid frozen/non-frozen dataclass inheritance", + "body": [ + { + "type": "text", + "html": "@dataclass(frozen=True) instances are immutable \u2014 their attributes cannot be reassigned after construction. Additionally, a frozen dataclass cannot inherit from a non-frozen one, and vice versa." + }, + { + "type": "code", + "lang": "python", + "code": "@dataclass(frozen=True)\nclass Point:\n x: float\n\np = Point(1.0)\np.x = 2.0 # E: dataclass is frozen\n\n@dataclass # E: non-frozen cannot inherit from frozen\nclass Sub(Point):\n pass" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0052" + }, + { + "code": "BSK-E0053", + "severity": "error", + "summary": "`assert_type()` type mismatch", + "summaryHtml": "assert_type() type mismatch", + "body": [ + { + "type": "text", + "html": "assert_type(expr, Type) is a static-analysis directive that verifies the inferred type of expr equals Type. When the resolver can determine both sides and they do not match, this rule emits an error." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import assert_type\n\ndef f(a: int | str) -> None:\n assert_type(a, int) # E \u2014 int | str is not int" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0053" + }, + { + "code": "BSK-E0054", + "severity": "error", + "summary": "`Final` type qualifier annotation violations", + "summaryHtml": "Final type qualifier annotation violations", + "body": [ + { + "type": "text", + "html": "Detects violations of PEP 591's rules for the Final qualifier, beyond the positional errors handled by E0044. Specifically:" + }, + { + "type": "text", + "html": "1. **Class attribute Final without init** \u2014 ID2: Final / ID3: Finalint in a class body without an initializer and not assigned in __init__." + }, + { + "type": "text", + "html": "2. **Instance Final outside __init__** \u2014 self.id3: Final = 1 in a method other than __init__." + }, + { + "type": "text", + "html": "3. **Re-assignment to already-initialized Final** \u2014 self.ID5 = 0 when ID5: Finalint = 0 is already given a value in the class body." + }, + { + "type": "text", + "html": "4. **Modification of Final class attribute** \u2014 self.ID7 = 0 / self.ID7 += 1 when ID7 is declared Final in the class body." + }, + { + "type": "text", + "html": "5. **Module-level Final re-assignment** \u2014 RATE = 300 after RATE: Final = 3000." + }, + { + "type": "text", + "html": "6. **Class attribute re-assignment** \u2014 ClassB.DEFAULT_ID = 0 when DEFAULT_ID is declared Final in ClassB." + }, + { + "type": "text", + "html": "7. **Subclass override of Final** \u2014 BORDER_WIDTH = 2.5 in a subclass when the parent declares BORDER_WIDTH: Final = 2.5." + }, + { + "type": "text", + "html": "8. **Function-local Final modification** \u2014 x += 1 when x: Final = 3, or walrus/for/with/tuple-unpack on a Final variable." + }, + { + "type": "text", + "html": "9. **Global Final modification** \u2014 global ID1; ID1 = 2 inside a function when ID1 is a module-level Final." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0054" + }, + { + "code": "BSK-E0055", + "severity": "error", + "summary": "Invalid `TypeVar` / `TypeVarTuple` / `ParamSpec` keyword argument combination", + "summaryHtml": "Invalid TypeVar / TypeVarTuple / ParamSpec keyword argument combination", + "body": [ + { + "type": "text", + "html": "PEP 484 / PEP 695 forbid certain combinations of keyword arguments in TypeVar(...) calls, and PEP 646 / PEP 612 restrict what kwargs TypeVarTuple and ParamSpec accept:" + }, + { + "type": "text", + "html": "1. covariant=True and contravariant=True together \u2014 a TypeVar cannot be both covariant and contravariant. 2. infer_variance=True with covariant=True or contravariant=True \u2014 when variance is inferred, the explicit flags are redundant and disallowed. 3. Constraints (2+ positional type args) combined with bound= \u2014 a TypeVar may have one or the other, but not both. 4. TypeVarTuple and ParamSpec do not support covariant, contravariant, bound, or type constraint arguments." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import TypeVar, TypeVarTuple\nT1 = TypeVar(\"T1\", covariant=True, contravariant=True) # E\nT2 = TypeVar(\"T2\", covariant=True, infer_variance=True) # E\nT3 = TypeVar(\"T3\", str, int, bound=\"int\") # E\nTs = TypeVarTuple(\"Ts\", covariant=True) # E\nTs2 = TypeVarTuple(\"Ts2\", int, float) # E" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0055" + }, + { + "code": "BSK-E0056", + "severity": "error", + "summary": "Mutation of `ReadOnly` `TypedDict` fields", + "summaryHtml": "Mutation of ReadOnly TypedDict fields", + "body": [ + { + "type": "text", + "html": "Fields marked as ReadOnly in TypedDicts cannot be mutated through: - Direct assignment: td"key" = value - .update() calls" + }, + { + "type": "code", + "lang": "python", + "code": "from typing import TypedDict\nfrom typing_extensions import ReadOnly\n\nclass Config(TypedDict):\n name: str\n version: ReadOnly[str]\n\ncfg: Config = {\"name\": \"test\", \"version\": \"1.0\"}\ncfg[\"version\"] = \"2.0\" # E0056\ncfg.update(version=\"2.0\") # E0056" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0056" + }, + { + "code": "BSK-E0057", + "severity": "error", + "summary": "Invalid RHS in a PEP 695 `type X = rhs` statement", + "summaryHtml": "Invalid RHS in a PEP 695 type X = rhs statement", + "body": [ + { + "type": "text", + "html": "PEP 695 requires the RHS of a type statement to be a valid type expression. The same restrictions as TypeAlias (BSK-E0048) apply." + }, + { + "type": "code", + "lang": "python", + "code": "type BadAlias1 = [int, str] # E \u2014 list literal\ntype BadAlias2 = True # E \u2014 bool literal\ntype BadAlias3 = 1 # E \u2014 int literal" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0057" + }, + { + "code": "BSK-E0058", + "severity": "error", + "summary": "`Annotated[...]` requires at least two arguments", + "summaryHtml": "Annotated... requires at least two arguments", + "body": [ + { + "type": "text", + "html": "PEP 593 requires Annotated to be subscripted with at least two arguments: a type and one or more metadata values. Annotatedint with only a single argument is a type error." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Annotated\nbad: Annotated[int] # E \u2014 only one argument" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0058" + }, + { + "code": "BSK-E0059", + "severity": "error", + "summary": "Access to `__match_args__` on a dataclass with `match_args=False`", + "summaryHtml": "Access to __match_args__ on a dataclass with match_args=False", + "body": [ + { + "type": "text", + "html": "When @dataclass(match_args=False) is specified, Python does **not** generate the __match_args__ class variable. Accessing ClassName.__match_args__ on such a class is an AttributeError at runtime and a static type error." + }, + { + "type": "code", + "lang": "python", + "code": "from dataclasses import dataclass\n\n@dataclass(match_args=False)\nclass DC4:\n x: int\n\nDC4.__match_args__ # E: attribute not generated" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0059" + }, + { + "code": "BSK-E0060", + "severity": "error", + "summary": "Invalid ordering comparison of dataclass instances", + "summaryHtml": "Invalid ordering comparison of dataclass instances", + "body": [ + { + "type": "text", + "html": "When @dataclass(order=True), Python synthesizes __lt__, __le__, __gt__, and __ge__ methods. These methods raise TypeError at runtime if the other operand is not an instance of the **same** class. Comparing two order=True dataclass instances of different types with <, <=, >, or >= is therefore a type error." + }, + { + "type": "text", + "html": "Additionally, when a class does NOT have order=True (including dataclass_transform classes with order=False), ordering comparisons are not supported at all because __lt__ etc. are never synthesized." + }, + { + "type": "code", + "lang": "python", + "code": "from dataclasses import dataclass\n\n@dataclass(order=True)\nclass DC1:\n a: str\n\n@dataclass(order=True)\nclass DC2:\n a: str\n\ndc1 = DC1(\"x\")\ndc2 = DC2(\"y\")\n\nif dc1 < dc2: # E: incompatible types\n pass" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0060" + }, + { + "code": "BSK-E0061", + "severity": "error", + "summary": "`assert_type` with `Literal[Enum.MEMBER]` on enum-typed param", + "summaryHtml": "assert_type with LiteralEnum.MEMBER on enum-typed param", + "body": [ + { + "type": "text", + "html": "This rule detects when assert_type() is used with a LiteralEnum.MEMBER type on a parameter that is already typed as the enum itself. This is redundant and indicates a misunderstanding of enum typing semantics." + }, + { + "type": "code", + "lang": "python", + "code": "from enum import Enum\nfrom typing import assert_type, Literal\n\nclass Status(Enum):\n ACTIVE = 1\n INACTIVE = 2\n\ndef process(status: Status) -> None:\n assert_type(status, Literal[Status.ACTIVE]) # E0061 \u2014 redundant narrowing\n assert_type(status, Status) # OK \u2014 correct usage" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0061" + }, + { + "code": "BSK-E0062", + "severity": "error", + "summary": "`-> NoReturn` / `-> Never` function can fall through", + "summaryHtml": "-> NoReturn / -> Never function can fall through", + "body": [ + { + "type": "text", + "html": "A function declared with a return type of NoReturn or Never must unconditionally raise an exception or call another NoReturn function on every code path. If the function can reach the end of its body without raising (e.g. via an if without an else), the annotation is wrong." + }, + { + "type": "code", + "lang": "python", + "code": "import sys\nfrom typing import NoReturn\n\ndef stop() -> NoReturn: # OK \u2014 always raises\n raise RuntimeError(\"no way\")\n\ndef bad(x: int) -> NoReturn: # E \u2014 can fall through when x == 0\n if x != 0:\n sys.exit(1)" + }, + { + "type": "text", + "html": "## Conservative scope" + }, + { + "type": "text", + "html": "The check is conservative: it only flags a function when **all** of the following hold:" + }, + { + "type": "text", + "html": "1. The declared return type is exactly NoReturn or Never (checked by extracting the annotation text from the span). 2. The function body is not a stub (... or pass). 3. The last top-level statement is **not** a raise statement and is **not** a standalone call expression (which may itself be NoReturn)." + }, + { + "type": "text", + "html": "This avoids false positives for valid patterns such as raise RuntimeError(...) or sys.exit(1) as the terminating statement." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0062" + }, + { + "code": "BSK-E0063", + "severity": "error", + "summary": "Non-hashable dataclass assigned to a `Hashable`-annotated variable", + "summaryHtml": "Non-hashable dataclass assigned to a Hashable-annotated variable", + "body": [ + { + "type": "text", + "html": "A @dataclass with eq=True (the default) sets __hash__ to None unless the class is frozen=True, uses unsafe_hash=True, or explicitly defines a __hash__ method. Assigning such an instance to a variable annotated Hashable is a type error." + }, + { + "type": "code", + "lang": "python", + "code": "from dataclasses import dataclass\nfrom typing import Hashable\n\n@dataclass\nclass DC1:\n a: int\n\nv: Hashable = DC1(0) # E \u2014 DC1.__hash__ is None\n\n@dataclass(eq=True, frozen=True)\nclass DC2:\n a: int\n\nv2: Hashable = DC2(0) # OK \u2014 frozen dataclasses are hashable" + }, + { + "type": "text", + "html": "PEP 557 specifies the __hash__ synthesis rules: - If eq is true and frozen is false, __hash__ is set to None. - If eq is true and frozen is true, Python synthesises a __hash__. - If unsafe_hash is true, Python synthesises a __hash__ regardless. - If eq is false, __hash__ is left untouched (inherited from parent). - If the class defines __hash__ explicitly, that definition is used." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0063" + }, + { + "code": "BSK-E0064", + "severity": "error", + "summary": "Invalid argument in a `NamedTuple` constructor call", + "summaryHtml": "Invalid argument in a NamedTuple constructor call", + "body": [ + { + "type": "text", + "html": "When a NamedTuple is instantiated using keyword arguments, Basilisk validates each argument against the field names and field types declared in the NamedTuple(...) definition." + }, + { + "type": "text", + "html": "Two kinds of violation are caught:" + }, + { + "type": "text", + "html": "1. **Unknown field** \u2014 a keyword whose name is not among the declared fields. 2. **Type mismatch** \u2014 a keyword whose literal value is incompatible with the declared field type (e.g. passing a str literal for an int field)." + }, + { + "type": "code", + "lang": "python", + "code": "X: Final = \"x\"\nY: Final = \"y\"\nN = NamedTuple(\"N\", [(X, int), (Y, int)])\n\nN(x=3, y=4) # OK\nN(a=1) # E: unknown field `a`\nN(x=\"\", y=\"\") # E: field `x` expects `int` but got `str`" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0064" + }, + { + "code": "BSK-E0065", + "severity": "error", + "summary": "Access to an `int`-only attribute on a `float`-typed parameter", + "summaryHtml": "Access to an int-only attribute on a float-typed parameter", + "body": [ + { + "type": "text", + "html": "The Python typing spec (PEP 484 / typing spec \u00a7Special cases for float and complex) states that int is not a subtype of float for static type-checking purposes. Attributes such as numerator and denominator are defined on int but NOT on float. Accessing them on a parameter declared as float is therefore a static type error." + }, + { + "type": "text", + "html": "The check is deliberately conservative \u2014 it only fires on **top-level** statements inside a function body, skipping any access inside an if/for/while/match/ with/try block. This means that accesses protected by an isinstance guard (where the parameter has been narrowed to int) are never flagged." + }, + { + "type": "code", + "lang": "python", + "code": "def func1(f: float):\n f.numerator # E \u2014 float does not have .numerator\n\n if not isinstance(f, float):\n f.numerator # OK \u2014 narrowed to int inside the branch" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0065" + }, + { + "code": "BSK-E0066", + "severity": "error", + "summary": "Enum member value incompatible with `_value_` type annotation", + "summaryHtml": "Enum member value incompatible with _value_ type annotation", + "body": [ + { + "type": "text", + "html": "When an enum class declares _value_: T (annotation-only, no value), all member values assigned in the class body must be compatible with T. Additionally, if self._value_ = param appears in __init__, the parameter's type annotation must be compatible with the declared _value_: T." + }, + { + "type": "code", + "lang": "python", + "code": "from enum import Enum\n\nclass Color(Enum):\n _value_: int\n RED = 1 # OK \u2014 int matches int\n GREEN = \"green\" # E \u2014 str is not compatible with int\n\nclass Planet(Enum):\n _value_: str\n\n def __init__(self, value: int, mass: float, radius: float):\n self._value_ = value # E \u2014 int is not compatible with str" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0066" + }, + { + "code": "BSK-E0067", + "severity": "error", + "summary": "Non-member referenced in `Literal[EnumClass.X]` annotation", + "summaryHtml": "Non-member referenced in LiteralEnumClass.X annotation", + "body": [ + { + "type": "text", + "html": "The LiteralEnumClass.X type is only valid when X is an actual enum member. Using it with a non-member (a method, property, lambda, nested class, private attribute, or nonmember()-wrapped attribute) is a type error." + }, + { + "type": "code", + "lang": "python", + "code": "from enum import Enum, nonmember\nfrom typing import Literal\n\nclass Pet4(Enum):\n CAT = 1\n converter = lambda x: str(x) # Non-member (lambda)\n\n def speak(self) -> None: ... # Non-member (method)\n\nconverter: Literal[Pet4.converter] # E \u2014 converter is not an enum member\nspeak: Literal[Pet4.speak] # E \u2014 speak is not an enum member" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0067" + }, + { + "code": "BSK-E0068", + "severity": "error", + "summary": "`Literal[\"EnumClass.MEMBER\"]` (string) used where `Literal[EnumClass.MEMBER]` (enum member reference) is required", + "summaryHtml": "Literal"EnumClass.MEMBER" (string) used where LiteralEnumClass.MEMBER (enum member reference) is required", + "body": [ + { + "type": "text", + "html": "A quoted string like "Color.RED" is a str literal \u2014 it is NOT the same as the enum member Color.RED. When a variable is declared as LiteralColor.RED but assigned from a parameter typed as Literal"Color.RED", the types are incompatible." + }, + { + "type": "code", + "lang": "python", + "code": "from enum import Enum\nfrom typing import Literal\n\nclass Color(Enum):\n RED = 1\n\ndef func2(a: Literal[Color.RED]) -> None:\n x1: Literal[\"Color.RED\"] = a # E \u2014 string literal != enum member" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0068" + }, + { + "code": "BSK-E0069", + "severity": "error", + "summary": "Dataclass constructor argument violations", + "summaryHtml": "Dataclass constructor argument violations", + "body": [ + { + "type": "text", + "html": "Reports errors when: - A positional argument is passed to a keyword-only dataclass field - A keyword argument targets a field with init=False (not part of __init__)" + }, + { + "type": "code", + "lang": "python", + "code": "from dataclasses import dataclass, KW_ONLY\n\n@dataclass\nclass Point:\n x: float\n _: KW_ONLY\n y: float = 0.0\n\nPoint(1.0) # OK \u2014 x positional, y uses default\nPoint(1.0, 2.0) # E \u2014 y is keyword-only, cannot be passed positionally" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0069" + }, + { + "code": "BSK-E0070", + "severity": "error", + "summary": "`Never` type compatibility violations", + "summaryHtml": "Never type compatibility violations", + "body": [ + { + "type": "text", + "html": "Detects type compatibility errors involving the Never bottom type:" + }, + { + "type": "text", + "html": "1. Assigning a parameter typed ContainerNever to a local annotated ContainerT where T is not Never or Any (invariant violation) 2. Returning ClassCNever() from a function annotated -> ClassCU where the class's type parameter is invariant (not covariant)" + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Never, Any, Generic, TypeVar\n\nT = TypeVar(\"T\")\nU = TypeVar(\"U\")\n\ndef func(c: list[Never]):\n v: list[int] = c # E0070 \u2014 list is invariant, list[Never] != list[int]\n\nclass ClassC(Generic[T]):\n pass\n\ndef func2(x: U) -> ClassC[U]:\n return ClassC[Never]() # E0070 \u2014 ClassC is invariant" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0070" + }, + { + "code": "BSK-E0071", + "severity": "error", + "summary": "Historical positional-only parameter violations", + "summaryHtml": "Historical positional-only parameter violations", + "body": [ + { + "type": "text", + "html": "Before PEP 570 (Python 3.8), the convention for marking parameters as positional-only was to prefix their names with __ (double underscore) without a trailing __. Type checkers must support this historical mechanism." + }, + { + "type": "text", + "html": "Two violations are detected:" + }, + { + "type": "text", + "html": "1. **PositionalOnlyAfterKeyword**: A __-prefixed positional-only parameter appears after a regular positional-or-keyword parameter in a function that does not use PEP 570 / syntax." + }, + { + "type": "text", + "html": "2. **KeywordPassedToPositionalOnly**: A __-prefixed keyword argument is passed at a call site (e.g. f(__x=3)), which is invalid because __x is positional-only and cannot be passed by keyword." + }, + { + "type": "code", + "lang": "python", + "code": "def f1(__x: int) -> None: ...\n\nf1(__x=3) # E \u2014 __x is positional-only\n\ndef f2(x: int, __y: int) -> None: ... # E \u2014 __y after positional-or-keyword x" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0071" + }, + { + "code": "BSK-E0072", + "severity": "error", + "summary": "No matching overload for subscript indexing", + "summaryHtml": "No matching overload for subscript indexing", + "body": [ + { + "type": "text", + "html": "When a class defines overloaded __getitem__ methods and a module-level subscript expression (e.g. b"") passes an argument whose type is incompatible with all overload signatures, Basilisk reports the error." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import overload\n\nclass Bytes:\n @overload\n def __getitem__(self, __i: int) -> int: ...\n @overload\n def __getitem__(self, __s: slice) -> bytes: ...\n def __getitem__(self, __i_or_s: int | slice) -> int | bytes: ...\n\nb = Bytes()\nb[\"\"] # E0072 -- no overload of __getitem__ accepts str" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0072" + }, + { + "code": "BSK-E0073", + "severity": "error", + "summary": "`NamedTuple`-to-tuple type incompatibility", + "summaryHtml": "NamedTuple-to-tuple type incompatibility", + "body": [ + { + "type": "text", + "html": "When a NamedTuple instance is assigned to a variable annotated with a fixed-length tuple... type, Basilisk verifies:" + }, + { + "type": "text", + "html": "1. The element count matches the number of fields in the NamedTuple. 2. Each element type in the tuple annotation is compatible with the corresponding NamedTuple field type (with covariance)." + }, + { + "type": "code", + "lang": "python", + "code": "class Point(NamedTuple):\n x: int\n y: int\n units: str = \"meters\"\n\np = Point(x=1, y=2, units=\"inches\")\nv1: tuple[int, int, str] = p # OK\nv2: tuple[int, int] = p # E -- too few elements (2 vs 3 fields)\nv3: tuple[int, str, str] = p # E -- incompatible element type" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0073" + }, + { + "code": "BSK-E0074", + "severity": "error", + "summary": "Constructor call type mismatch with specialized generic class", + "summaryHtml": "Constructor call type mismatch with specialized generic class", + "body": [ + { + "type": "text", + "html": "When a generic class is called with explicit type arguments (e.g. Class1int(1.0)), Basilisk substitutes the type parameters into the __new__ method signature and checks that the provided arguments are compatible." + }, + { + "type": "text", + "html": "This rule covers two cases:" + }, + { + "type": "text", + "html": "1. **Argument type mismatch after substitution**: The __new__ method has a parameter typed with a type variable (e.g. x: T), and after substituting the type argument (e.g. T=int), the provided argument is incompatible (e.g. 1.0 is float, not int)." + }, + { + "type": "text", + "html": "2. **Explicit cls parameter type mismatch**: The __new__ method has an explicitly typed cls parameter (e.g. cls: typeClass11[int]), and the class is called with different type arguments (e.g. Class11str())." + }, + { + "type": "code", + "lang": "python", + "code": "class Class1(Generic[T]):\n def __new__(cls, x: T) -> Self:\n return super().__new__(cls)\n\nClass1[int](1.0) # E: float is not compatible with int" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0074" + }, + { + "code": "BSK-E0075", + "severity": "error", + "summary": "Incompatible type for `Self`-typed attribute", + "summaryHtml": "Incompatible type for Self-typed attribute", + "body": [ + { + "type": "text", + "html": "When a class declares an attribute annotated with Self (or Self | None, OptionalSelf, etc.), that attribute's type is bound to the concrete subclass at each usage site. Passing or assigning a parent-class instance where the subclass is expected is a type error." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Self, TypeVar, Generic\nfrom dataclasses import dataclass\n\nT = TypeVar(\"T\")\n\n@dataclass\nclass LinkedList(Generic[T]):\n value: T\n next: Self | None = None\n\n@dataclass\nclass OrdinalLinkedList(LinkedList[int]):\n def ordinal_value(self) -> str:\n return str(self.value)\n\nxs = OrdinalLinkedList(value=1, next=LinkedList[int](value=2)) # E\nxs.next = LinkedList[int](value=3, next=None) # E" + }, + { + "type": "text", + "html": "Specification: <https://typing.readthedocs.io/en/latest/spec/generics.html#use-in-attribute-annotations>" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0075" + }, + { + "code": "BSK-E0076", + "severity": "error", + "summary": "Overload union expansion failure", + "summaryHtml": "Overload union expansion failure", + "body": [ + { + "type": "text", + "html": "When a function-body call passes a union-typed argument to an overloaded function and, after expanding the union, at least one member fails to match any overload signature, Basilisk reports the error." + }, + { + "type": "code", + "lang": "python", + "code": "@overload\ndef example(x: int, y: str, z: int) -> str: ...\n@overload\ndef example(x: int, y: int, z: int) -> int: ...\ndef example(x: int, y: int | str, z: int) -> int | str:\n return 1\n\ndef check(v: int | str) -> None:\n example(v, v, 1) # E -- str not assignable to int in any overload" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0076" + }, + { + "code": "BSK-E0077", + "severity": "error", + "summary": "Protocol `Self`-return conformance violation", + "summaryHtml": "Protocol Self-return conformance violation", + "body": [ + { + "type": "text", + "html": "When a Protocol declares a method returning Self, any class passed where that protocol is expected must have the corresponding method return Self or the class itself. If the method returns a completely different type (e.g. int or a different class), the class does not satisfy the protocol." + }, + { + "type": "code", + "lang": "python", + "code": "class ShapeProtocol(Protocol):\n def set_scale(self, scale: float) -> Self: ...\n\nclass BadReturn:\n def set_scale(self, scale: float) -> int:\n return 42\n\ndef accepts(s: ShapeProtocol) -> None: ...\n\ndef main(bad: BadReturn) -> None:\n accepts(bad) # E \u2014 BadReturn.set_scale returns int, not Self" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0077" + }, + { + "code": "BSK-E0078", + "severity": "error", + "summary": "`Self` type violations in generics", + "summaryHtml": "Self type violations in generics", + "body": [ + { + "type": "text", + "html": "This rule detects two kinds of Self type violations:" + }, + { + "type": "text", + "html": "1. **Return type mismatch**: A method (or classmethod) annotated -> Self returns a concrete class constructor call (e.g. return Shape()) instead of self, cls(), or another Self-compatible expression. In a subclass, Self resolves to the subclass type, so returning the parent class constructor is a type error." + }, + { + "type": "text", + "html": "2. **Self is not subscriptable**: Self cannot be parameterized (e.g. Selfint). It already captures the full generic specialization of the enclosing class." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Self\n\nclass Shape:\n def method2(self) -> Self:\n return Shape() # E \u2014 should return self, not Shape()\n\n @classmethod\n def cls_method2(cls) -> Self:\n return Shape() # E \u2014 should return cls(), not Shape()\n\nclass Container(Generic[T]):\n def foo(self, other: Self[int]) -> None: # E \u2014 Self is not subscriptable\n pass" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0078" + }, + { + "code": "BSK-E0079", + "severity": "error", + "summary": "Module assigned to incompatible protocol type", + "summaryHtml": "Module assigned to incompatible protocol type", + "body": [ + { + "type": "text", + "html": "When a module object is assigned to a variable typed as a Protocol, the module's public interface must be compatible with the protocol. This rule detects assignments of the form:" + }, + { + "type": "code", + "lang": "python", + "code": "import some_module\n\nclass MyProtocol(Protocol):\n timeout: str\n\nx: MyProtocol = some_module # E \u2014 some_module.timeout is int, not str" + }, + { + "type": "text", + "html": "This is a simplified check: if the annotation names a class that inherits from Protocol and the RHS is a module name, the assignment is flagged when the module is known to be incompatible." + }, + { + "type": "text", + "html": "Specification: <https://typing.readthedocs.io/en/latest/spec/protocol.html#modules-as-implementations-of-protocols>" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0079" + }, + { + "code": "BSK-E0080", + "severity": "error", + "summary": "`TypeVar` upper bound violation at call site", + "summaryHtml": "TypeVar upper bound violation at call site", + "body": [ + { + "type": "text", + "html": "When a function parameter is annotated with a TypeVar that has an upper bound (e.g. bound=Sized), and the call site passes a literal value whose type does not satisfy that bound, Basilisk reports the violation." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Sized, TypeVar\n\nST = TypeVar(\"ST\", bound=Sized)\n\ndef longer(x: ST, y: ST) -> ST:\n if len(x) > len(y):\n return x\n return y\n\nlonger(3, 3) # E -- int does not implement Sized (__len__)" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0080" + }, + { + "code": "BSK-E0081", + "severity": "error", + "summary": "`TypeVarTuple` unpack minimum type argument violation", + "summaryHtml": "TypeVarTuple unpack minimum type argument violation", + "body": [ + { + "type": "text", + "html": "When a function parameter has a type annotation containing a TypeVarTuple unpack pattern like ArrayBatch, *tuple[Any, ..., Channels], the type has fixed prefix and suffix type arguments around a variadic middle. Any value passed to that parameter must have at least prefix_count + suffix_count type arguments." + }, + { + "type": "code", + "lang": "python", + "code": "Ts = TypeVarTuple(\"Ts\")\n\nclass Array(Generic[*Ts]): ...\n\ndef process(x: Array[Batch, *tuple[Any, ...], Channels]) -> None: ...\n\ndef func(z: Array[Batch]):\n process(z) # E -- Array[Batch] has 1 type arg, need at least 2" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0081" + }, + { + "code": "BSK-E0082", + "severity": "error", + "summary": "`TypeVarTuple` callable/tuple argument mismatch", + "summaryHtml": "TypeVarTuple callable/tuple argument mismatch", + "body": [ + { + "type": "text", + "html": "When a constructor (or function) links two parameters via a TypeVarTuple -- one as Callable[Ts, R] and the other as tupleTs -- passing a known function as the callable infers the expected element types for the tuple. If the tuple literal has elements whose types do not match the inferred order, Basilisk reports the mismatch." + }, + { + "type": "code", + "lang": "python", + "code": "Ts = TypeVarTuple(\"Ts\")\n\nclass Process:\n def __init__(self, target: Callable[[*Ts], None], args: tuple[*Ts]) -> None: ...\n\ndef func1(arg1: int, arg2: str) -> None: ...\n\nProcess(target=func1, args=(0, \"\")) # OK\nProcess(target=func1, args=(\"\", 0)) # E -- str, int does not match int, str" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0082" + }, + { + "code": "BSK-E0083", + "severity": "error", + "summary": "`TypeVarTuple` must be unpacked with `*` operator", + "summaryHtml": "TypeVarTuple must be unpacked with * operator", + "body": [ + { + "type": "text", + "html": "When a TypeVarTuple is used in a generic class base list or as a direct type annotation, it must be unpacked using the * operator. Using a TypeVarTuple without unpacking is invalid per PEP 646." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Generic, TypeVarTuple\n\nTs = TypeVarTuple(\"Ts\")\n\n# BAD\nclass Cls(Generic[Ts]): # E: TypeVarTuple must be unpacked with *\n ...\n\ndef f(*args: Ts) -> None: # E: TypeVarTuple must be unpacked with *\n ...\n\n# GOOD\nclass Cls2(Generic[*Ts]): # OK\n ...\n\ndef f2(*args: *Ts) -> None: # OK\n ..." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0083" + }, + { + "code": "BSK-E0084", + "severity": "error", + "summary": "`TypeVarTuple` variance/bounds/constraints violation", + "summaryHtml": "TypeVarTuple variance/bounds/constraints violation", + "body": [ + { + "type": "text", + "html": "TypeVarTuple does not support specification of variance, bounds, or constraints. Using these parameters with TypeVarTuple is invalid." + }, + { + "type": "code", + "lang": "python", + "code": "# BAD\nTs = TypeVarTuple(\"Ts\", covariant=True) # E: TypeVarTuple does not support variance\nTs = TypeVarTuple(\"Ts\", int, float) # E: TypeVarTuple does not support constraints\nTs = TypeVarTuple(\"Ts\", bound=int) # E: TypeVarTuple does not support bounds\n\n# GOOD\nTs = TypeVarTuple(\"Ts\") # OK" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0084" + }, + { + "code": "BSK-E0085", + "severity": "error", + "summary": "`TypeVarTuple` argument count mismatch", + "summaryHtml": "TypeVarTuple argument count mismatch", + "body": [ + { + "type": "text", + "html": "When a constructor with TypeVarTuple parameters is called, the number of arguments must match the expected count inferred from the TypeVarTuple." + }, + { + "type": "code", + "lang": "python", + "code": "Ts = TypeVarTuple(\"Ts\")\n\nclass Array(Generic[*Ts]):\n def __init__(self, shape: tuple[*Ts]) -> None: ...\n\nArray[Height, Width]((Height(1), Width(2))) # OK\nArray[Height, Width](Height(1)) # E: expected 2 arguments, got 1" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0085" + }, + { + "code": "BSK-E0086", + "severity": "error", + "summary": "Multiple `TypeVarTuple` unpacks in generic or tuple type", + "summaryHtml": "Multiple TypeVarTuple unpacks in generic or tuple type", + "body": [ + { + "type": "text", + "html": "Only a single TypeVarTuple unpack (*Ts) may appear in a type parameter list or in a tuple... type expression." + }, + { + "type": "code", + "lang": "python", + "code": "# BAD \u2014 multiple TypeVarTuples in class\nclass Array3(Generic[*Ts1, *Ts2]): # E\n ...\n\n# BAD \u2014 multiple unpacks in tuple type\nTA5 = tuple[T1, *Ts, T2, *Ts] # E\nTA6 = tuple[T1, *Ts, T2, *tuple[int, ...]] # E\n\n# GOOD\nclass Array(Generic[*Ts]): ...\nTA1 = tuple[*Ts, T1, T2] # OK \u2014 single unpack" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0086" + }, + { + "code": "BSK-E0087", + "severity": "error", + "summary": "Reserved for future PEP 695 type parameter checks", + "summaryHtml": "Reserved for future PEP 695 type parameter checks", + "body": [ + { + "type": "text", + "html": "PEP 695 type parameter bound and constraint validation is handled by BSK-E0089. This module is reserved for any future distinct PEP 695 violations." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0087" + }, + { + "code": "BSK-E0088", + "severity": "error", + "summary": "`TypedDict` runtime violation", + "summaryHtml": "TypedDict runtime violation", + "body": [ + { + "type": "text", + "html": "PEP 589 defines constraints on what you can do with TypedDict type objects at runtime:" + }, + { + "type": "text", + "html": "- TypedDict type objects cannot be used in isinstance() tests." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import TypedDict\n\nclass Movie(TypedDict):\n name: str\n year: int\n\nmovie: Movie = {\"name\": \"Blade Runner\", \"year\": 1982}\n\nif isinstance(movie, Movie): # E \u2014 TypedDict cannot be used in isinstance\n ..." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0088" + }, + { + "code": "BSK-E0089", + "severity": "error", + "summary": "Invalid PEP 695 type parameter bound or constraint", + "summaryHtml": "Invalid PEP 695 type parameter bound or constraint", + "body": [ + { + "type": "text", + "html": "PEP 695 introduced a new syntax for declaring type parameters in class and function definitions. The bound/constraint expression after : is restricted to specific forms; invalid forms are caught by this rule." + }, + { + "type": "code", + "lang": "python", + "code": "# BAD\nclass Foo[T: [str, int]]: # E: list literal is not a valid bound\n ...\n\nclass Bar[T: ()]: # E: constraint tuple must have two or more types\n ...\n\nclass Baz[T: (str,)]: # E: constraint tuple must have two or more types\n ...\n\nt1 = (bytes, str)\nclass Qux[T: t1]: # E: constraint must be a literal tuple expression\n ...\n\nclass Bad[T: (3, bytes)]: # E: 3 is not a valid type expression\n ..." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0089" + }, + { + "code": "BSK-E0090", + "severity": "error", + "summary": "Invalid tuple type syntax", + "summaryHtml": "Invalid tuple type syntax", + "body": [ + { + "type": "text", + "html": "Validates tuple type annotations according to PEP 646 rules:" + }, + { + "type": "text", + "html": "- tupleT, ... must have exactly one type before ... - tuple... is invalid (must specify a type) - tupleT, ..., U is invalid (... can only appear at the end) - tupleT, U, ... is invalid (can't have multiple fixed types before ...) - Invalid unpack patterns like tuple*tuple[str, ...]" + }, + { + "type": "code", + "lang": "python", + "code": "t1: tuple[int, ...] # OK\nt2: tuple[int, int, ...] # E \u2014 multiple fixed types before ...\nt3: tuple[...] # E \u2014 missing type before ...\nt4: tuple[..., int] # E \u2014 ... must be at the end\nt5: tuple[int, ..., int] # E \u2014 ... must be at the end\nt6: tuple[*tuple[str], ...] # E \u2014 invalid unpack pattern" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0090" + }, + { + "code": "BSK-E0091", + "severity": "error", + "summary": "Incompatible `TypeVar` bound or constraint with its default", + "summaryHtml": "Incompatible TypeVar bound or constraint with its default", + "body": [ + { + "type": "text", + "html": "PEP 696 specifies two constraints on TypeVar defaults:" + }, + { + "type": "text", + "html": "1. If both bound and default are specified, the default must be a subtype of the bound. The numeric subtype hierarchy is bool <: int <: float <: complex." + }, + { + "type": "text", + "html": "2. For constrained TypeVars, the default must be one of the constraints exactly. (Even a subtype is disallowed \u2014 float is a subtype of complex but if the constraints are float, str and the default is complex, that is an error.)" + }, + { + "type": "code", + "lang": "python", + "code": "from typing import TypeVar\n\nOk1 = TypeVar(\"Ok1\", bound=float, default=int) # OK \u2014 int <: float\nInvalid1 = TypeVar(\"Invalid1\", bound=str, default=int) # E \u2014 int is not <: str\n\nOk2 = TypeVar(\"Ok2\", float, str, default=float) # OK\nInvalid2 = TypeVar(\"Invalid2\", float, str, default=int) # E \u2014 int not in {float, str}" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0091" + }, + { + "code": "BSK-E0092", + "severity": "error", + "summary": "Wrong number of type arguments to a generic class or type alias", + "summaryHtml": "Wrong number of type arguments to a generic class or type alias", + "body": [ + { + "type": "text", + "html": "When a user-defined generic class has both required (non-default) and optional (defaulted) type parameters, the minimum number of type arguments that must be supplied when subscripting the class is the count of required parameters." + }, + { + "type": "text", + "html": "Also detects when too many type arguments are supplied to a user-defined generic class (one that has no TypeVarTuple and therefore a fixed maximum arity), or to a TypeAlias that has a fixed number of free type variables." + }, + { + "type": "text", + "html": "Additionally detects when a class that has fully specialised its generic base (e.g. class Foo(Barint)) is subscripted further, since it has no free type variables." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Generic, TypeVar, TypeAlias\nfrom typing_extensions import TypeVar as TypeVarExt\n\nT1 = TypeVar(\"T1\")\nT2 = TypeVar(\"T2\")\nDefaultStrT = TypeVarExt(\"DefaultStrT\", default=str)\n\nclass AllTheDefaults(Generic[T1, T2, DefaultStrT]): ...\n\nAllTheDefaults[int] # E \u2014 1 arg but at least 2 required\nAllTheDefaults[int, str] # OK\nAllTheDefaults[int, str, bytes] # OK\n\nclass LinkedList(Generic[T]): ...\n\nLinkedList[int, str] # E \u2014 2 args but at most 1 allowed\n\nMyAlias: TypeAlias = LinkedList[T2]\nMyAlias[int, str] # E \u2014 2 args but at most 1 allowed for the alias\n\nclass Foo(LinkedList[int]): ...\nFoo[str] # E \u2014 Foo has no free type variables" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0092" + }, + { + "code": "BSK-E0093", + "severity": "error", + "summary": "Invalid key or value type in `TypedDict` assignment", + "summaryHtml": "Invalid key or value type in TypedDict assignment", + "body": [ + { + "type": "text", + "html": "PEP 589 defines TypedDict as a typed dict with a fixed set of keys and associated types. This rule detects:" + }, + { + "type": "text", + "html": "1. Subscript assignments with invalid (non-existent) keys. 2. Subscript assignments where the value type is incompatible with the declared field type. 3. Annotated dict-literal assignments that contain invalid keys or are missing required keys." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import TypedDict\n\nclass Movie(TypedDict):\n name: str\n year: int\n\nmovie: Movie = {\"name\": \"Blade Runner\", \"year\": 1982}\n\nmovie[\"director\"] = \"Ridley Scott\" # E: invalid key\nmovie[\"year\"] = \"1982\" # E: wrong value type\nmovie2: Movie = {\"title\": \"Blade Runner\", \"year\": 1982} # E: invalid/missing keys" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0093" + }, + { + "code": "BSK-E0094", + "severity": "error", + "summary": "`Self` type used in an invalid location", + "summaryHtml": "Self type used in an invalid location", + "body": [ + { + "type": "text", + "html": "PEP 673 defines Self as a special type that refers to the current class. It is only valid in specific locations:" + }, + { + "type": "text", + "html": "- Method parameter annotations (including self and cls) - Method return type annotations - Class variable annotations inside the class body - Nested within other types at those locations" + }, + { + "type": "text", + "html": "Invalid locations (detected here):" + }, + { + "type": "text", + "html": "- Return types or parameter annotations of module-level functions - Module-level variable annotations (bar: Self) - TypeAlias definitions whose RHS contains Self - Base class expressions (class Foo(BarSelf) or class Foo(Self)) - @staticmethod method annotations (no self to bind to) - Method annotations in metaclasses (classes inheriting from type) - Return type annotation when self is explicitly annotated with a TypeVar (e.g. def f(self: TFoo2) -> Self: \u2014 binding is ambiguous)" + }, + { + "type": "code", + "lang": "python", + "code": "# E \u2014 not within a class\ndef foo(bar: Self) -> Self: ...\nbar: Self\n\nclass Base:\n @staticmethod\n def make() -> Self: ... # E \u2014 staticmethod has no Self binding\n\nclass MyMeta(type):\n def __new__(cls, *args: Any) -> Self: ... # E \u2014 metaclass" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0094" + }, + { + "code": "BSK-E0095", + "severity": "error", + "summary": "`InitVar` field validation in dataclasses", + "summaryHtml": "InitVar field validation in dataclasses", + "body": [ + { + "type": "text", + "html": "Detects two categories of InitVar violations:" + }, + { + "type": "text", + "html": "1. **__post_init__ signature mismatch**: A dataclass with InitVar fields must declare a __post_init__ method whose parameters (after self) match the InitVar fields in count and type." + }, + { + "type": "text", + "html": "2. **Access to InitVar fields as instance attributes**: InitVarT fields are constructor-only parameters passed to __post_init__; they are not stored as instance attributes and cannot be accessed as instance.field." + }, + { + "type": "code", + "lang": "python", + "code": "from dataclasses import InitVar, dataclass\n\n@dataclass\nclass DC1:\n x: InitVar[int]\n y: InitVar[str]\n\n def __post_init__(self, x: int, y: int) -> None: # E: y should be str\n pass\n\ndc1 = DC1(1, \"\")\ndc1.x # E: cannot access InitVar field as attribute" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0095" + }, + { + "code": "BSK-E0096", + "severity": "error", + "summary": "Type mismatch between a dataclass `field(default_factory=\u2026)` and the field's declared type annotation", + "summaryHtml": "Type mismatch between a dataclass field(default_factory=\u2026) and the field's declared type annotation", + "body": [ + { + "type": "text", + "html": "When a dataclass field uses field(default_factory=T) where T is a known callable that constructs instances of a simple built-in type, but the field's annotation declares a different incompatible built-in type, Basilisk reports an error." + }, + { + "type": "code", + "lang": "python", + "code": "from dataclasses import dataclass, field\n\n@dataclass\nclass DC:\n a: int = field(default_factory=str) # E: str() \u2192 str, not int" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0096" + }, + { + "code": "BSK-E0097", + "severity": "error", + "summary": "Protocol method sets self-attributes not declared in the Protocol", + "summaryHtml": "Protocol method sets self-attributes not declared in the Protocol", + "body": [ + { + "type": "text", + "html": "When a Protocol class defines a method (including __init__/__new__) that assigns to self.attr where attr is not a declared member of the Protocol, this is a violation: per the typing spec, "additional attributes only defined in the body of a method by assignment via self are not allowed". Protocol members must be explicitly declared at the class level." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Protocol\n\nclass MyProto(Protocol):\n x: int\n def __init__(self) -> None:\n self.y = 0 # E \u2014 `y` is not declared in the Protocol\n def method(self) -> None:\n self.z: int = 0 # E \u2014 `z` is not declared in the Protocol" + }, + { + "type": "text", + "html": "@staticmethod/@classmethod members have no instance receiver, so their first parameter is not self and is not analysed here." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0097" + }, + { + "code": "BSK-E0098", + "severity": "error", + "summary": "Non-Protocol base class in a Protocol definition", + "summaryHtml": "Non-Protocol base class in a Protocol definition", + "body": [ + { + "type": "text", + "html": "Per PEP 544, a Protocol class may only inherit from other Protocol classes (with the exception of object). Inheriting from a non-Protocol concrete class is a violation." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Protocol\n\nclass Base:\n x: int = 0\n\nclass BadProto(Base, Protocol): # E \u2014 Base is not a Protocol\n def method(self) -> int: ..." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0098" + }, + { + "code": "BSK-E0099", + "severity": "error", + "summary": "Direct instantiation of a Protocol class", + "summaryHtml": "Direct instantiation of a Protocol class", + "body": [ + { + "type": "text", + "html": "Protocol classes define structural interfaces and cannot be instantiated directly. Only concrete classes that satisfy the protocol may be instantiated." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Protocol\n\nclass MyProto(Protocol):\n def method(self) -> int: ...\n\nobj = MyProto() # E \u2014 cannot instantiate a Protocol" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0099" + }, + { + "code": "BSK-E0100", + "severity": "error", + "summary": "Augmented assignment widens `Literal` type", + "summaryHtml": "Augmented assignment widens Literal type", + "body": [ + { + "type": "text", + "html": "When a function parameter is annotated with Literal..., augmented assignment (+=, -=, etc.) effectively reassigns the variable to a widened type (e.g. int instead of Literal3, 4, 5), violating the declared Literal constraint." + }, + { + "type": "code", + "lang": "python", + "code": "def func(a: Literal[3, 4, 5]):\n a += 3 # E0100 \u2014 augmented assign widens Literal type" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0100" + }, + { + "code": "BSK-E0101", + "severity": "error", + "summary": "`TypeGuard` or `TypeIs` on method with no narrowing parameter", + "summaryHtml": "TypeGuard or TypeIs on method with no narrowing parameter", + "body": [ + { + "type": "text", + "html": "The typing spec requires that a TypeGuard or TypeIs function must have at least one user-facing parameter to narrow. When a method returns TypeGuardX or TypeIsX but only has self or cls, there is no parameter to narrow and the guard is invalid." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0101" + }, + { + "code": "BSK-E0102", + "severity": "error", + "summary": "Invalid `TypeVar` default referencing another `TypeVar`", + "summaryHtml": "Invalid TypeVar default referencing another TypeVar", + "body": [ + { + "type": "text", + "html": "PEP 696 specifies constraints on TypeVar defaults that reference other TypeVars:" + }, + { + "type": "text", + "html": "1. **Ordering**: When TypeVar T2 has default=T1, T1 must appear before T2 in generic parameter list 2. **Outer scope references**: TypeVar cannot use a TypeVar from outer scope as default 3. **Bound compatibility**: When T2 has default=T1, T1's bound must be a subtype of T2's bound 4. **Constraint superset**: When T2 has default=T1 and T2 has constraints, T1's constraints must be a subset of T2's constraints" + }, + { + "type": "code", + "lang": "python", + "code": "from typing import TypeVar\n\n# Ordering violation\nT2 = TypeVar(\"T2\", default=T1) # E \u2014 T1 not defined yet\nT1 = TypeVar(\"T1\")\n\n# Outer scope violation\nclass Outer:\n T1 = TypeVar(\"T1\")\n class Inner:\n T2 = TypeVar(\"T2\", default=T1) # E \u2014 T1 from outer scope\n\n# Bound compatibility violation\nX1 = TypeVar(\"X1\", bound=int)\nInvalid1 = TypeVar(\"Invalid1\", default=X1, bound=str) # E \u2014 int is not a subtype of str\n\n# Constraint superset violation\nY1 = TypeVar(\"Y1\", int, str)\nInvalid2 = TypeVar(\"Invalid2\", bool, complex, default=Y1) # E \u2014 {bool, complex} is not a superset of {int, str}" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0102" + }, + { + "code": "BSK-E0103", + "severity": "error", + "summary": "Tuple index out of bounds", + "summaryHtml": "Tuple index out of bounds", + "body": [ + { + "type": "text", + "html": "When a fixed-length tupleT1, T2, ... variable is indexed with a literal integer or a LiteralN-typed variable that is outside the valid range [-len, len), this is a static error." + }, + { + "type": "code", + "lang": "python", + "code": "v: tuple[int, str, list[bool]] = (3, \"hi\", [True])\nv[4] # E0103 \u2014 index 4 out of range for 3-element tuple\nv[-4] # E0103 \u2014 index -4 out of range for 3-element tuple" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0103" + }, + { + "code": "BSK-E0104", + "severity": "error", + "summary": "Cyclical type alias reference", + "summaryHtml": "Cyclical type alias reference", + "body": [ + { + "type": "text", + "html": "A TypeAlias-annotated assignment whose RHS contains a forward-reference string that resolves back to the alias itself (directly or through a chain of mutual references) creates an infinite type that cannot be resolved." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import TypeAlias, Union\n\n# Direct self-reference \u2014 the Union *only* wraps itself and a base type,\n# producing an infinitely expanding alias:\nRecursiveUnion: TypeAlias = Union[\"RecursiveUnion\", int] # E\n\n# Mutual reference \u2014 two aliases reference each other:\nA: TypeAlias = Union[\"B\", int]\nB: TypeAlias = Union[\"A\", str] # E" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0104" + }, + { + "code": "BSK-E0105", + "severity": "error", + "summary": "Invalid attribute access on bounded type variable", + "summaryHtml": "Invalid attribute access on bounded type variable", + "body": [ + { + "type": "text", + "html": "When a PEP 695 type parameter has a bound (e.g., T: str), attribute accesses on parameters typed as T must be valid for the bound type." + }, + { + "type": "code", + "lang": "python", + "code": "class C[T: str]:\n def method(self, x: T):\n x.capitalize() # OK - str has capitalize\n x.is_integer() # E - str does NOT have is_integer" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0105" + }, + { + "code": "BSK-E0106", + "severity": "error", + "summary": "Protocol class used where `type[Proto]` is expected", + "summaryHtml": "Protocol class used where typeProto is expected", + "body": [ + { + "type": "text", + "html": "The typing spec states: "Variables and parameters annotated with TypeProto accept only concrete (non-protocol) subtypes of Proto."" + }, + { + "type": "text", + "html": "Passing the Protocol class itself (rather than a concrete subtype) violates this constraint." + }, + { + "type": "code", + "lang": "python", + "code": "class Proto(Protocol):\n def meth(self) -> int: ...\n\nclass Concrete:\n def meth(self) -> int: return 42\n\ndef fun(cls: type[Proto]) -> int:\n return cls().meth()\n\nfun(Proto) # E0106 \u2014 Protocol class passed to type[Proto]\nfun(Concrete) # OK \u2014 concrete subtype\n\nvar: type[Proto]\nvar = Proto # E0106 \u2014 Protocol class assigned to type[Proto]\nvar = Concrete # OK" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0106" + }, + { + "code": "BSK-E0107", + "severity": "error", + "summary": "Variance incompatibility in base class parameterisation", + "summaryHtml": "Variance incompatibility in base class parameterisation", + "body": [ + { + "type": "text", + "html": "When a class inherits from a generic base class (directly or through a type alias), the TypeVar arguments must have compatible variance with the corresponding type parameters declared by the base class." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Generic, TypeVar\n\nT = TypeVar(\"T\") # invariant\nT_co = TypeVar(\"T_co\", covariant=True)\n\nclass Base(Generic[T]): ...\n\nclass Bad(Base[T_co]): ... # E \u2014 invariant param gets covariant arg" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0107" + }, + { + "code": "BSK-E0108", + "severity": "error", + "summary": "Dataclass slots violations", + "summaryHtml": "Dataclass slots violations", + "body": [ + { + "type": "text", + "html": "Reports errors when: - self.attr = value assigns to an attribute not in __slots__ inside a class with @dataclass(slots=True) or a manual __slots__ definition. - ClassName.__slots__ or ClassName().__slots__ is accessed on a dataclass that does not define __slots__ (neither via slots=True nor a manual __slots__ assignment)." + }, + { + "type": "code", + "lang": "python", + "code": "@dataclass(slots=True)\nclass DC:\n x: int\n def __init__(self):\n self.y = 3 # E: \"y\" is not in __slots__\n\n@dataclass\nclass DC2:\n a: int\nDC2.__slots__ # E: __slots__ not defined" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0108" + }, + { + "code": "BSK-E0109", + "severity": "error", + "summary": "`TypeVar` bound violation at call site", + "summaryHtml": "TypeVar bound violation at call site", + "body": [ + { + "type": "text", + "html": "When a function has a parameter typed with a TypeVar that has a bound, and a call passes an argument whose type is not a subtype of that bound, this rule reports the mismatch." + }, + { + "type": "code", + "lang": "python", + "code": "TLiteral = TypeVar(\"TLiteral\", bound=LiteralString)\n\ndef literal_identity(s: TLiteral) -> TLiteral:\n return s\n\ndef func5(s: str):\n literal_identity(s) # E \u2014 str is not a subtype of LiteralString" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0109" + }, + { + "code": "BSK-E0110", + "severity": "error", + "summary": "Protocol variance violation", + "summaryHtml": "Protocol variance violation", + "body": [ + { + "type": "text", + "html": "Detects when a Protocol class declares TypeVars with incorrect variance based on how they are used in method signatures:" + }, + { + "type": "text", + "html": "- A TypeVar used only in output positions (return types) should be covariant. - A TypeVar used only in input positions (parameters) should be contravariant. - A covariant TypeVar used in input position is a violation. - A contravariant TypeVar used in output position is a violation." + }, + { + "type": "text", + "html": "__init__ and __new__ methods are exempt from variance inference." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0110" + }, + { + "code": "BSK-E0111", + "severity": "error", + "summary": "Constructor call errors via `__init__` method", + "summaryHtml": "Constructor call errors via __init__ method", + "body": [ + { + "type": "text", + "html": "Detects several categories of constructor call errors when a class defines or inherits __init__:" + }, + { + "type": "text", + "html": "1. **Specialized generic argument mismatch** (L21): Calling Classint(1.0) when __init__ expects x: T and T=int, but 1.0 is float." + }, + { + "type": "text", + "html": "2. **Self type incompatibility** (L42): Passing a base-class instance where Self in __init__ demands a subclass instance." + }, + { + "type": "text", + "html": "3. **Explicit self annotation mismatch** (L56): __init__ annotates self as Class4int but the constructor is called as Class4str()." + }, + { + "type": "text", + "html": "4. **Class-scoped TypeVars in self annotation** (L107): Using class-scoped type variables in a reordered self annotation is invalid." + }, + { + "type": "text", + "html": "5. **No custom __init__ with arguments** (L130): Classes inheriting only from object (no custom __init__ or __new__) cannot accept arguments." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0111" + }, + { + "code": "BSK-E0112", + "severity": "error", + "summary": "TypeGuard/TypeIs return type incompatibility in callable arguments", + "summaryHtml": "TypeGuard/TypeIs return type incompatibility in callable arguments", + "body": [ + { + "type": "text", + "html": "When a function returning TypeGuardX or TypeIsX is passed as an argument where the expected callable return type is NOT bool, this rule reports the mismatch. TypeGuard and TypeIs are subtypes of bool in callable context, so passing them where Callable..., bool is expected is valid, but passing them where e.g. Callable..., str is expected is an error." + }, + { + "type": "code", + "lang": "python", + "code": "def takes_callable_str(f: Callable[[object], str]) -> None: ...\ndef simple_typeguard(val: object) -> TypeGuard[int]: ...\n\ntakes_callable_str(simple_typeguard) # E0112 \u2014 TypeGuard is bool, not str" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0112" + }, + { + "code": "BSK-E0113", + "severity": "error", + "summary": "`TypeIs` narrows to a type inconsistent with the input type", + "summaryHtml": "TypeIs narrows to a type inconsistent with the input type", + "body": [ + { + "type": "text", + "html": "Per the typing spec: "It is an error to narrow to a type that is not consistent with the input type." For TypeIs, the narrowed type must be a subtype of the input type." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0113" + }, + { + "code": "BSK-E0114", + "severity": "error", + "summary": "Protocol `isinstance`/`issubclass` violations", + "summaryHtml": "Protocol isinstance/issubclass violations", + "body": [ + { + "type": "text", + "html": "Per PEP 544: - A protocol can be used as the second argument to isinstance() or issubclass() **only** if it is decorated with @runtime_checkable. - issubclass() can only be used with **non-data** protocols (protocols that define only methods, not data attributes)." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Protocol, runtime_checkable\n\nclass Proto1(Protocol):\n name: str\n\n@runtime_checkable\nclass Proto2(Protocol):\n name: str\n def method(self) -> int: ...\n\nisinstance(x, Proto1) # E \u2014 not @runtime_checkable\nissubclass(x, Proto2) # E \u2014 data protocol in issubclass\nissubclass(x, (Proto2, Proto1)) # E \u2014 tuple contains violating protocol" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0114" + }, + { + "code": "BSK-E0115", + "severity": "error", + "summary": "Use of deprecated class, function, or method", + "summaryHtml": "Use of deprecated class, function, or method", + "body": [ + { + "type": "text", + "html": "PEP 702 introduces @deprecated from typing / typing_extensions. Using a deprecated entity (calling, importing, accessing) should produce a diagnostic so that developers migrate away from the deprecated API." + }, + { + "type": "code", + "lang": "python", + "code": "from typing_extensions import deprecated\n\n@deprecated(\"Use new_func instead\")\ndef old_func() -> None: ...\n\nold_func() # BSK-E0115" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0115" + }, + { + "code": "BSK-E0116", + "severity": "error", + "summary": "`NamedTuple` class definition errors", + "summaryHtml": "NamedTuple class definition errors", + "body": [ + { + "type": "text", + "html": "Detects several categories of NamedTuple definition errors:" + }, + { + "type": "text", + "html": "1. **Underscore field names**: Field names starting with _ are illegal in NamedTuple definitions (the runtime raises ValueError)." + }, + { + "type": "text", + "html": "2. **Default ordering**: Fields with default values must come after all fields without defaults (same rule as the runtime enforces)." + }, + { + "type": "text", + "html": "3. **Subclass field conflict**: A NamedTuple subclass cannot redefine fields that exist in the base NamedTuple." + }, + { + "type": "text", + "html": "4. **Multiple inheritance**: NamedTuple does not support inheriting from multiple bases (other than Generic...)." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0116" + }, + { + "code": "BSK-E0117", + "severity": "error", + "summary": "Unbound type variable in scope", + "summaryHtml": "Unbound type variable in scope", + "body": [ + { + "type": "text", + "html": "A type variable used in a type annotation must be "in scope" \u2014 i.e. it must be bound by a surrounding generic class (GenericT), PEP 695 type parameter, or function signature parameter." + }, + { + "type": "text", + "html": "Unbound usages include: - TypeVar in a local variable annotation when the function does not bind it - TypeVar in a class body attribute when the class does not include it in Generic... - Inner class reusing an outer class's TypeVar in GenericT or body annotations - TypeVar at module level in annotations - TypeAlias at class level referencing the class's own TypeVars" + }, + { + "type": "code", + "lang": "python", + "code": "T = TypeVar(\"T\")\nS = TypeVar(\"S\")\n\ndef fun(x: T) -> list[T]:\n z: list[S] = [] # E \u2014 S is not bound in this function\n\nclass Bar(Generic[T]):\n an_attr: list[S] = [] # E \u2014 S is not bound in Bar" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0117" + }, + { + "code": "BSK-E0118", + "severity": "error", + "summary": "Calling `super().method()` on an abstract method with no default implementation", + "summaryHtml": "Calling super().method() on an abstract method with no default implementation", + "body": [ + { + "type": "text", + "html": "When a Protocol (or ABC) declares a method as @abstractmethod with only an ellipsis (...) or pass body, calling super().method() from a subclass is invalid because there is no concrete implementation to delegate to." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Protocol\nfrom abc import abstractmethod\n\nclass PColor(Protocol):\n @abstractmethod\n def draw(self) -> str:\n ...\n\nclass BadColor(PColor):\n def draw(self) -> str:\n return super().draw() # E \u2014 no default implementation" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0118" + }, + { + "code": "BSK-E0119", + "severity": "error", + "summary": "Protocol `isinstance`/`issubclass` violations", + "summaryHtml": "Protocol isinstance/issubclass violations", + "body": [ + { + "type": "text", + "html": "Per PEP 544: - A protocol can be used as the second argument to isinstance() or issubclass() **only** if it is decorated with @runtime_checkable. - issubclass() can only be used with **non-data** protocols (protocols that define only methods, not data attributes). - Type checkers should reject an isinstance() or issubclass() call if there is an unsafe overlap between the type of the first argument and the protocol." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Protocol, runtime_checkable\n\nclass Proto1(Protocol):\n name: str\n\n@runtime_checkable\nclass Proto2(Protocol):\n name: str\n def method(self) -> int: ...\n\nisinstance(x, Proto1) # E \u2014 not @runtime_checkable\nissubclass(x, Proto2) # E \u2014 data protocol in issubclass\nisinstance(Concrete(), Proto3) # E \u2014 unsafe overlap" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0119" + }, + { + "code": "BSK-E0120", + "severity": "error", + "summary": "Generator return type and yield type violations", + "summaryHtml": "Generator return type and yield type violations", + "body": [ + { + "type": "text", + "html": "A generator function (one containing yield or yield from) must declare a return type compatible with generator protocols:" + }, + { + "type": "text", + "html": "- Sync generators: Generator, Iterator, or Iterable - Async generators: AsyncGenerator, AsyncIterator, or AsyncIterable" + }, + { + "type": "text", + "html": "Additionally, yield expressions must produce values assignable to the declared yield type, and yield from sub-generators must have compatible yield and send types." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Generator, Iterator\n\n# BAD -- generator with non-generator return type\ndef bad() -> int:\n yield 1\n\n# GOOD\ndef good() -> Iterator[int]:\n yield 1" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0120" + }, + { + "code": "BSK-E0121", + "severity": "error", + "summary": "Protocol conformance violation in annotated assignment", + "summaryHtml": "Protocol conformance violation in annotated assignment", + "body": [ + { + "type": "text", + "html": "Detects errors in annotated assignments at module level:" + }, + { + "type": "text", + "html": "1. **Missing protocol members**: the annotation names a Protocol class and the RHS constructs a class that does not implement all required methods." + }, + { + "type": "text", + "html": "2. **Non-protocol structural assignment**: the annotation names a class that inherits from a Protocol but does not itself include Protocol in its bases (i.e. it is a concrete/abstract class, not a protocol). In this case structural subtyping does not apply and only nominal subclasses are allowed." + }, + { + "type": "text", + "html": "3. **Member-kind mismatch** (see conformance): a member is present but in an incompatible form \u2014 a read-write protocol property satisfied by a read-only/immutable member, or a writable protocol instance variable satisfied by a ClassVar, read-only property, or wrong-typed attribute." + }, + { + "type": "code", + "lang": "python", + "code": "class P(Protocol):\n def method(self) -> None: ...\n\nclass NotP(P): # Note: no Protocol \u2014 this is a concrete class\n def method(self) -> None: pass\n\nclass C:\n pass\n\nx: P = C() # E \u2014 C does not implement `method` (case 1)\ny: NotP = C() # E \u2014 NotP is not a Protocol, no structural subtyping (case 2)" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0121" + }, + { + "code": "BSK-E0122", + "severity": "error", + "summary": "Callable call-site arity and argument validation", + "summaryHtml": "Callable call-site arity and argument validation", + "body": [ + { + "type": "text", + "html": "When a parameter is annotated as Callable[int, str, T], calls to that parameter must match the expected argument count. Additionally, Callable parameters are implicitly positional-only, so keyword arguments are not allowed." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0122" + }, + { + "code": "BSK-E0123", + "severity": "error", + "summary": "`super()` call on abstract protocol method with no default implementation", + "summaryHtml": "super() call on abstract protocol method with no default implementation", + "body": [ + { + "type": "text", + "html": "When a class explicitly implements a Protocol and one of its methods calls super().method_name(), the parent protocol method must provide a default implementation. If the parent method is abstract (its body is only ... or pass), calling super() on it is an error because there is no concrete implementation to dispatch to." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Protocol\nfrom abc import abstractmethod\n\nclass PColor(Protocol):\n @abstractmethod\n def draw(self) -> str:\n ...\n\nclass BadColor(PColor):\n def draw(self) -> str:\n return super().draw() # E \u2014 no default implementation" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0123" + }, + { + "code": "BSK-E0124", + "severity": "error", + "summary": "Protocol attribute tuple element type mismatch", + "summaryHtml": "Protocol attribute tuple element type mismatch", + "body": [ + { + "type": "text", + "html": "When a class explicitly implements a Protocol and assigns to a self.attr in __init__ where attr is declared as tupleT1, T2, ... in the protocol, each element of the assigned tuple must have a compatible type. If a parameter used in the tuple has a different type than the corresponding element type in the protocol's annotation, Basilisk reports the mismatch." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Protocol\n\nclass RGB(Protocol):\n rgb: tuple[int, int, int]\n\nclass Point(RGB):\n def __init__(self, red: int, green: int, blue: str) -> None:\n self.rgb = red, green, blue # E \u2014 'blue' must be 'int'" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0124" + }, + { + "code": "BSK-E0125", + "severity": "error", + "summary": "Access to instance attribute on a class object", + "summaryHtml": "Access to instance attribute on a class object", + "body": [ + { + "type": "text", + "html": "Instance attributes (annotations without ClassVar in the class body that lack a default value) exist only on instances, not on the class object itself. Accessing or assigning such attributes on the class (including parameterised generics like Nodeint) is an error." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Generic, TypeVar\n\nT = TypeVar(\"T\")\n\nclass Node(Generic[T]):\n label: T\n\nNode[int].label = 1 # E: instance attribute on class\nNode[int].label # E\nNode.label = 1 # E\nNode.label # E\ntype(n1).label # E" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0125" + }, + { + "code": "BSK-E0126", + "severity": "error", + "summary": "`LiteralString` and `Literal` assignment incompatibilities", + "summaryHtml": "LiteralString and Literal assignment incompatibilities", + "body": [ + { + "type": "text", + "html": "Detects annotated local variables inside function bodies where the declared type is incompatible with the assigned value, specifically for LiteralString and Literal... types." + }, + { + "type": "text", + "html": "Covered cases:" + }, + { + "type": "text", + "html": "1. Assigning a Literal"X"-typed parameter to a Literal"Y" variable where the literal values differ. 2. Assigning an f-string containing non-LiteralString interpolations to a LiteralString-annotated variable. 3. Assigning a generic parameterised with str where LiteralString is required (invariant generics like list, Container). 4. Assigning a listLiteralString to liststr \u2014 lists are invariant." + }, + { + "type": "code", + "lang": "python", + "code": "def func(b: Literal[\"two\"], non_literal: str):\n x1: Literal[\"\"] = b # E \u2014 different literal values\n x2: LiteralString = f\"{non_literal}\" # E \u2014 non-literal in f-string\n x3: Container[LiteralString] = Container(s) # E \u2014 str \u2260 LiteralString\n x4: list[str] = val # E \u2014 invariant mismatch" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0126" + }, + { + "code": "BSK-E0127", + "severity": "error", + "summary": "Tuple index out of range", + "summaryHtml": "Tuple index out of range", + "body": [ + { + "type": "text", + "html": "Detects subscript access on a fixed-length tupleT1, T2, ... parameter where the index is a known integer literal (either an inline int literal or a parameter typed as LiteralN) that falls outside the valid range -len, len-1." + }, + { + "type": "code", + "lang": "python", + "code": "def f(v: tuple[int, str, list[bool]], b: Literal[5]):\n v[b] # E \u2014 index 5 out of range for 3-element tuple\n v[4] # E \u2014 index 4 out of range\n v[-4] # E \u2014 index -4 out of range (valid: -3..-1)" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0127" + }, + { + "code": "BSK-E0128", + "severity": "error", + "summary": "```TypeVar``` default referential violations", + "summaryHtml": "``TypeVar`` default referential violations", + "body": [ + { + "type": "text", + "html": "PEP 696 defines rules for when a TypeVar default references another TypeVar:" + }, + { + "type": "text", + "html": "1. **Ordering**: The referenced TypeVar must appear before the referencing TypeVar in Generic.... 2. **Scope**: A TypeVar default must not reference TypeVarar from an outer class scope. 3. **Bound/constraint compatibility**: When TypeVar T2 defaults to TypeVar T1, T1's bound must be a subtype of T2's bound, and T2's constraints (if any) must be a superset of T1's constraints." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import TypeVar, Generic\n\nS1 = TypeVar(\"S1\")\nS2 = TypeVar(\"S2\", default=S1)\n\nStart2T = TypeVar(\"Start2T\", default=\"StopT\")\nStop2T = TypeVar(\"Stop2T\", default=int)\nclass slice2(Generic[Start2T, Stop2T]): ... # E: bad ordering\n\nclass Foo3(Generic[S1]):\n class Bar2(Generic[S2]): ... # E: outer scope\n\nY1 = TypeVar(\"Y1\", bound=int)\nInvalid2 = TypeVar(\"Invalid2\", float, str, default=Y1) # E" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0128" + }, + { + "code": "BSK-E0129", + "severity": "error", + "summary": "Literal value assignment incompatibility", + "summaryHtml": "Literal value assignment incompatibility", + "body": [ + { + "type": "text", + "html": "Detects two classes of Literal-related assignment errors inside function bodies:" + }, + { + "type": "text", + "html": "1. **Literal0 vs LiteralFalse non-equivalence (PEP 586)**: Literal0 and LiteralFalse are distinct types despite 0 == False in Python. Assigning a Literal0-typed parameter to a LiteralFalse local (or vice versa) is a type error." + }, + { + "type": "text", + "html": "2. **Augmented assignment widens a Literal type**: a += 3 where a is typed Literal3, 4, 5 produces an int result, which is not assignable back to Literal3, 4, 5." + }, + { + "type": "code", + "lang": "python", + "code": "def func(a: Literal[0], b: Literal[False]):\n x1: Literal[False] = a # E \u2014 int 0 \u2260 bool False in Literal\n x2: Literal[0] = b # E \u2014 bool False \u2260 int 0 in Literal\n\ndef func2(a: Literal[3, 4, 5]):\n a += 3 # E \u2014 result type is `int`, not `Literal[3, 4, 5]`" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0129" + }, + { + "code": "BSK-E0130", + "severity": "error", + "summary": "`TypeVar` scoping violation", + "summaryHtml": "TypeVar scoping violation", + "body": [ + { + "type": "text", + "html": "Detects uses of TypeVar instances outside their valid scope:" + }, + { + "type": "text", + "html": "1. A nested class inside a generic class using the outer class's TypeVar in its base classes or body (the outer class's type params don't cover the inner class scope). 2. A class nested inside a generic function re-using the function's TypeVar in Generic.... 3. A TypeVar used in a module-level expression (subscript call like listT()). 4. A method call on a generic class instance where the argument type does not match the substituted TypeVar type (e.g., a: MyClassint, calling a.meth('str') when meth expects T which is bound to int)." + }, + { + "type": "text", + "html": "Per PEP 484: "A generic class nested in another generic class cannot use the same type variables."" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0130" + }, + { + "code": "BSK-E0131", + "severity": "error", + "summary": "Generator yield/send/return type mismatch", + "summaryHtml": "Generator yield/send/return type mismatch", + "body": [ + { + "type": "text", + "html": "When a function is annotated with GeneratorY, S, R, IteratorY, or IterableY, the yield expressions must produce values compatible with Y, and yield from expressions must delegate to generators whose yield and send types are compatible." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Generator, Iterator\n\nclass A: ...\nclass B: ...\n\ndef bad() -> Generator[A, None, None]:\n yield 3 # E: incompatible yield type\n\ndef bad2() -> Iterator[A]:\n yield B() # E: incompatible yield type" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0131" + }, + { + "code": "BSK-E0132", + "severity": "error", + "summary": "Inconsistent `TypeVar` ordering across base classes", + "summaryHtml": "Inconsistent TypeVar ordering across base classes", + "body": [ + { + "type": "text", + "html": "When a class inherits from multiple generic bases that share a common generic ancestor, the TypeVar argument orderings must be consistent." + }, + { + "type": "code", + "lang": "python", + "code": "class Grandparent(Generic[T1, T2]): ...\nclass Parent(Grandparent[T1, T2]): ...\nclass BadChild(Parent[T1, T2], Grandparent[T2, T1]): ... # E" + }, + { + "type": "text", + "html": "BadChild inherits Grandparent twice \u2014 once via ParentT1, T2 (which maps to GrandparentT1, T2) and once directly as GrandparentT2, T1. The orderings conflict." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0132" + }, + { + "code": "BSK-E0133", + "severity": "error", + "summary": "Protocol `TypeVar` variance mismatch", + "summaryHtml": "Protocol TypeVar variance mismatch", + "body": [ + { + "type": "text", + "html": "When a generic protocol class declares a TypeVar as invariant but the inferred variance (from method parameter and return positions) is strictly covariant or contravariant, a diagnostic is emitted recommending the more specific variance." + }, + { + "type": "text", + "html": "PEP 544 specifies that type checkers should warn when the inferred variance of a type variable used in a protocol differs from its declared variance." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Protocol, TypeVar\n\nT = TypeVar(\"T\") # invariant\n\nclass MyProto(Protocol[T]): # E \u2014 T should be covariant\n def method(self) -> T: ..." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0133" + }, + { + "code": "BSK-E0134", + "severity": "error", + "summary": "Invariant generic type mismatch at call site", + "summaryHtml": "Invariant generic type mismatch at call site", + "body": [ + { + "type": "text", + "html": "When a function parameter expects a parameterised generic like dictstr, list[object] and a subclass whose base parameterisation differs in an invariant position is passed, the call is invalid." + }, + { + "type": "code", + "lang": "python", + "code": "class SymbolTable(dict[str, list[Node]]): ...\n\ndef takes(x: dict[str, list[object]]): ...\n\ndef test(s: SymbolTable):\n takes(s) # E -- list is invariant, list[Node] != list[object]" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0134" + }, + { + "code": "BSK-E0136", + "severity": "error", + "summary": "Callable subtyping violations (covariance / contravariance)", + "summaryHtml": "Callable subtyping violations (covariance / contravariance)", + "body": [ + { + "type": "text", + "html": "Callable types are covariant with respect to return types and contravariant with respect to parameter types. When a Callable[T, R]-annotated variable is assigned a value whose type is Callable[S, Q], the assignment is only valid when:" + }, + { + "type": "text", + "html": "- Q is a subtype of R (return type \u2014 covariant) - T is a subtype of S (parameter type \u2014 contravariant, i.e. the source must accept everything the target accepts, which means a broader type)" + }, + { + "type": "code", + "lang": "python", + "code": "def func(\n cb1: Callable[[float], int],\n cb3: Callable[[int], int],\n) -> None:\n f6: Callable[[float], float] = cb3 # E \u2014 int param is not supertype of float\n f8: Callable[[int], int] = cb2 # E \u2014 float return is not subtype of int" + }, + { + "type": "text", + "html": "This rule specifically handles assignments inside function bodies where the RHS is a parameter whose type is already known to be a Callable." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0136" + }, + { + "code": "BSK-E0137", + "severity": "error", + "summary": "Generic protocol violations", + "summaryHtml": "Generic protocol violations", + "body": [ + { + "type": "text", + "html": "Detects violations related to generic protocol usage:" + }, + { + "type": "text", + "html": "1. **ProtocolT combined with GenericT**: The ProtocolT, S, ... shorthand is already equivalent to Protocol, GenericT, S, .... It is an error to combine the shorthand with an explicit Generic... base." + }, + { + "type": "text", + "html": "2. **Incompatible generic protocol assignment**: When a module-level variable is annotated with a concrete generic protocol specialisation like Protoint, str and the RHS is a concrete class, the concrete class's method signatures must be compatible with the substituted type arguments." + }, + { + "type": "text", + "html": "3. **Self-typed protocol method incompatibility**: When a protocol declares methods using a self: T annotation (making the return type depend on the concrete receiver), concrete classes that implement those methods with incompatible signatures are flagged." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Generic, Protocol, TypeVar\n\nT_co = TypeVar(\"T_co\", covariant=True)\n\nclass Proto2(Protocol[T_co], Generic[T_co]): # E \u2014 shorthand + Generic\n ..." + }, + { + "type": "text", + "html": "PEP 544: <https://typing.readthedocs.io/en/latest/spec/protocol.html#generic-protocols>" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0137" + }, + { + "code": "BSK-E0138", + "severity": "error", + "summary": "`dataclass_transform` metaclass violations", + "summaryHtml": "dataclass_transform metaclass violations", + "body": [ + { + "type": "text", + "html": "Detects type errors in classes whose metaclass is decorated with @dataclass_transform(...). Four violation kinds are covered:" + }, + { + "type": "text", + "html": "1. **Frozen inheritance**: a non-frozen subclass inheriting from a frozen one. 2. **Frozen attribute assignment**: mutating an attribute of a frozen instance. 3. **Positional argument to kw-only constructor**: all fields are keyword-only when kw_only_default=True on the transform. 4. **Ordering comparison without order**: using </<=/>/>= on instances of a class that did not opt in to order=True." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import dataclass_transform\n\n@dataclass_transform(kw_only_default=True)\nclass ModelMeta(type): ...\n\nclass ModelBase(metaclass=ModelMeta): ...\n\nclass Customer(ModelBase, frozen=True):\n id: int\n\nc = Customer(id=1)\nc.id = 2 # E \u2014 frozen\nv = c < c # E \u2014 no ordering methods" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0138" + }, + { + "code": "BSK-E0139", + "severity": "error", + "summary": "Invalid `TypeVarTuple` specialization of generic alias", + "summaryHtml": "Invalid TypeVarTuple specialization of generic alias", + "body": [ + { + "type": "text", + "html": "Two related violations are detected:" + }, + { + "type": "text", + "html": "1. **Unpack in non-TypeVarTuple generic**: When a generic alias is defined using only regular TypeVars (no TypeVarTuple), you cannot specialise it with an unpacked TypeVarTuple (Ts) or an unpacked homogeneous tuple (tupleT, ...)." + }, + { + "type": "code", + "lang": "python", + "code": "T = TypeVar(\"T\")\nIntTupleGeneric = tuple[int, T]\n\nIntTupleGeneric[str] # OK\nIntTupleGeneric[*Ts] # E \u2014 Ts is a TypeVarTuple, not a TypeVar\nIntTupleGeneric[*tuple[float, ...]] # E \u2014 unpacked tuple not allowed here" + }, + { + "type": "text", + "html": "2. **Too few type arguments for TypeVarTuple+TypeVar alias**: When a generic alias contains both a TypeVarTuple and one or more regular TypeVars, every specialisation must supply at least as many arguments as there are regular TypeVars (the TypeVarTuple absorbs the rest)." + }, + { + "type": "code", + "lang": "python", + "code": "T1, T2 = TypeVar(\"T1\"), TypeVar(\"T2\")\nTs = TypeVarTuple(\"Ts\")\nTA7 = tuple[*Ts, T1, T2]\n\nv1: TA7[int] # E \u2014 requires at least two type arguments (T1, T2)\nv2: TA7[int, str] # OK \u2014 T1=int, T2=str, Ts=()" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0139" + }, + { + "code": "BSK-E0140", + "severity": "error", + "summary": "Callable and Protocol assignment compatibility", + "summaryHtml": "Callable and Protocol assignment compatibility", + "body": [ + { + "type": "text", + "html": "Checks that when a function is assigned to a variable annotated with a Callable type or a callback Protocol, the signatures are compatible." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0140" + }, + { + "code": "BSK-E0141", + "severity": "error", + "summary": "Unpack[`TypedDict`] kwargs violations", + "summaryHtml": "UnpackTypedDict kwargs violations", + "body": [ + { + "type": "text", + "html": "Detects invalid uses of **kwargs: UnpackTypedDict in function signatures: parameter overlap with TypedDict keys, UnpackTypeVar (invalid), and call-site validation for functions with Unpack kwargs." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0141" + }, + { + "code": "BSK-E0142", + "severity": "error", + "summary": "`dataclass_transform` violations when the transform is applied via a base class", + "summaryHtml": "dataclass_transform violations when the transform is applied via a base class", + "body": [ + { + "type": "text", + "html": "When a class is decorated with @dataclass_transform(...), subclasses that inherit from it behave like dataclasses with the transform's default settings overridable by keyword arguments on the class definition." + }, + { + "type": "text", + "html": "This rule detects: 1. A non-frozen subclass inheriting from a frozen transform-class (line 51). 2. Attribute assignment on a frozen transform-class instance (lines 63, 122). 3. Positional arguments to a kw_only transform-class constructor (lines 66, 82). 4. Comparison operators on transform-class instances that lack order=True (line 72)." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import dataclass_transform\n\n@dataclass_transform(kw_only_default=True)\nclass ModelBase: ...\n\nclass Customer(ModelBase, frozen=True):\n id: int\n\nc = Customer(3) # E \u2014 kw_only requires keyword args\nc.id = 4 # E \u2014 frozen instance is immutable" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0142" + }, + { + "code": "BSK-E0143", + "severity": "error", + "summary": "`NamedTuple` usage violations", + "summaryHtml": "NamedTuple usage violations", + "body": [ + { + "type": "text", + "html": "Detects invalid usage of NamedTuple instances:" + }, + { + "type": "text", + "html": "1. **Out-of-bounds index access**: p3 on a 3-field NamedTuple (valid: 0..2 or -3..-1). 2. **Attribute assignment**: p.x = 3 \u2014 NamedTuple fields are read-only. 3. **Subscript assignment**: p0 = 3 \u2014 NamedTuple elements are read-only. 4. **Attribute deletion**: del p.x \u2014 NamedTuple fields cannot be deleted. 5. **Subscript deletion**: del p0 \u2014 NamedTuple elements cannot be deleted. 6. **Wrong-count tuple unpack**: x, y = p when p has 3 fields." + }, + { + "type": "code", + "lang": "python", + "code": "class Point(NamedTuple):\n x: int\n y: int\n units: str = \"meters\"\n\np = Point(1, 2)\nprint(p[3]) # E: out-of-bounds index\nprint(p[-4]) # E: out-of-bounds negative index\np.x = 3 # E: NamedTuple fields are read-only\np[0] = 3 # E: NamedTuple elements are read-only\ndel p.x # E: NamedTuple fields cannot be deleted\ndel p[0] # E: NamedTuple elements cannot be deleted\nx, y = p # E: too few values to unpack (expected 3)" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0143" + }, + { + "code": "BSK-E0144", + "severity": "error", + "summary": "Invalid constructor call via `type[T]` parameter", + "summaryHtml": "Invalid constructor call via typeT parameter", + "body": [ + { + "type": "text", + "html": "When a parameter is typed as typeT (where T is a concrete class or a type variable), calling it as a constructor is equivalent to calling T(...). This rule checks that the arguments passed to such calls are consistent with the constructor of T." + }, + { + "type": "text", + "html": "Specification: <https://typing.readthedocs.io/en/latest/spec/constructors.html#constructor-calls-for-type-t>" + }, + { + "type": "text", + "html": "## Cases detected" + }, + { + "type": "text", + "html": "1. cls: typeClass where Class.__init__ / Class.__new__ / metaclass __call__ requires arguments but cls() is called with none. 2. cls: typeClass where Class has no custom constructor but cls(arg) is called with extra arguments. 3. cls: typeT (unbound TypeVar) called with any arguments \u2014 the constraint is unknown, so no arguments are permitted. 4. cls: typeT where T is bounded: same rules as the bound class." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0144" + }, + { + "code": "BSK-E0145", + "severity": "error", + "summary": "Invalid `type[X]` usage violations", + "summaryHtml": "Invalid typeX usage violations", + "body": [ + { + "type": "text", + "html": "Detects several categories of invalid use of typeX (or TypeX):" + }, + { + "type": "text", + "html": "1. **Callable passed as typeT argument** \u2014 Callable and other special forms are not valid class objects and cannot be passed where typeT is expected." + }, + { + "type": "text", + "html": "2. **Incompatible class passed to typeA | B** \u2014 when a function expects typeA | B, passing a class that is neither A nor B is an error." + }, + { + "type": "text", + "html": "3. **Unknown attribute access on typeobject** \u2014 unlike typeAny, typeobject only exposes object's own attributes; accessing any other member is an error." + }, + { + "type": "text", + "html": "4. **Unknown attribute access on a TypeAlias bound to type / Type** \u2014 a bare alias such as TA1: TypeAlias = Type resolves to typeAny, but the alias name itself (used at module scope like TA1.unknown) does not expose arbitrary attributes." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0145" + }, + { + "code": "BSK-E0146", + "severity": "error", + "summary": "Protocol class object violations", + "summaryHtml": "Protocol class object violations", + "body": [ + { + "type": "text", + "html": "Detects two related violations involving Protocol classes and class objects:" + }, + { + "type": "text", + "html": "1. A Protocol class itself is passed/assigned where typeProto is expected. Only concrete (non-Protocol) subtypes may be used." + }, + { + "type": "text", + "html": "2. A class object is assigned to a variable typed as a Protocol instance, but the class does not structurally satisfy the protocol when treated as an object (i.e. class-level access to protocol members gives incompatible types)." + }, + { + "type": "code", + "lang": "python", + "code": "class Proto(Protocol):\n def meth(self) -> int: ...\n\nclass Concrete:\n def meth(self) -> int: return 42\n\ndef fun(cls: type[Proto]) -> int:\n return cls().meth()\n\nfun(Proto) # E0146 \u2014 Protocol class itself passed to type[Proto]\nfun(Concrete) # OK\n\nvar: type[Proto]\nvar = Proto # E0146 \u2014 Protocol class assigned to type[Proto]\nvar = Concrete # OK\n\npa1: ProtoA1 = ConcreteA # E0146 \u2014 class object can't satisfy instance protocol\npa2: ProtoA2 = ConcreteA # OK \u2014 protocol uses _self/self pattern" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0146" + }, + { + "code": "BSK-E0147", + "severity": "error", + "summary": "Tuple starred-unpack type compatibility violation", + "summaryHtml": "Tuple starred-unpack type compatibility violation", + "body": [ + { + "type": "text", + "html": "Detects assignments where a tuple literal or a tuple-typed variable is assigned to a target whose annotation contains a starred unpack expression (tupleT, ... or tupleT) and the assignment is incompatible with that annotation." + }, + { + "type": "text", + "html": "Covers module-level bare reassignments of annotated tuple variables and function-body variable assignments." + }, + { + "type": "text", + "html": "## Examples" + }, + { + "type": "code", + "lang": "python", + "code": "t1: tuple[int, *tuple[str]] = (1, \"\") # OK\nt1 = (1, \"\", \"\") # E \u2014 too many elements for *tuple[str]\n\nt2: tuple[int, *tuple[str, ...]] = (1, \"\") # OK\nt2 = (1, 1, \"\") # E \u2014 second element must be str\n\ndef f(t1: tuple[int], t2: tuple[int, *tuple[int, ...]], t3: tuple[int, ...]):\n v2: tuple[int, *tuple[int, ...]]\n v2 = t3 # E \u2014 homogeneous tuple[int,...] not assignable to mixed starred form\n v3: tuple[int]\n v3 = t2 # E \u2014 t2 may have more elements than v3 allows\n v3 = t3 # E \u2014 t3 is unbounded, v3 is fixed length 1" + }, + { + "type": "text", + "html": "# Specification" + }, + { + "type": "text", + "html": "<https://typing.readthedocs.io/en/latest/spec/tuples.html#type-compatibility-rules>" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0147" + }, + { + "code": "BSK-E0148", + "severity": "error", + "summary": "Generic type argument violations", + "summaryHtml": "Generic type argument violations", + "body": [ + { + "type": "text", + "html": "Detects several generic-type errors:" + }, + { + "type": "text", + "html": "1. **Constrained TypeVar constraint mismatch**: When a function parameter is typed with a constrained TypeVar (e.g. AnyStr = TypeVar("AnyStr", str, bytes)), all arguments bound to the same type variable must belong to the same constraint. Passing (str_val, bytes_val) for (x: AnyStr, y: AnyStr) is an error." + }, + { + "type": "text", + "html": "2. **Mapping subscript key type mismatch**: When a Mapping-derived type has a known key type (e.g. MyMapstr, int), indexing with a literal of the wrong type (e.g. my_map0) is an error." + }, + { + "type": "text", + "html": "3. **Generic metaclass usage**: Using a parameterized generic class as a metaclass (metaclass=SomeGenericT) is not supported by the Python type system." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0148" + }, + { + "code": "BSK-E0149", + "severity": "error", + "summary": "PEP 695 generic type parameter scoping violations", + "summaryHtml": "PEP 695 generic type parameter scoping violations", + "body": [ + { + "type": "text", + "html": "Detects violations of PEP 695 type-parameter scoping rules, driven entirely by ruff_python_ast nodes (via basilisk_resolver::Pep695Scoping) \u2014 never by raw source.lines() scanning, so docstring/comment/string content is never mistaken for real class / def / type declarations." + }, + { + "type": "text", + "html": "1. A type parameter's bound references another type parameter in the same list (forward or backward reference). 2. A type parameter is used at module scope (2a) or in a decorator applied to the generic construct that declares it (2b). 3. A method re-declares an enclosing class's type parameter (shadowing). 4. A type statement references an old-style TypeVar. 5. A type statement appears inside a function body. 6. A type alias is circular. 7. A type alias is misused (called, subclassed, isinstance, attribute). 8. A type argument violates a bounded alias type parameter." + }, + { + "type": "code", + "lang": "python", + "code": "class ClassA[S, T: Sequence[S]]: ... # E \u2014 T's bound references S\nprint(T) # E \u2014 T not defined at module scope\n\n@decorator(Foo[T]) # E \u2014 T not in scope in the decorator\nclass ClassD[T]: ...\n\nclass ClassE[T]:\n def method1[T](self): ... # E \u2014 method re-defines class type param" + }, + { + "type": "text", + "html": "Reference: <https://peps.python.org/pep-0695/#type-parameter-scopes>" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0149" + }, + { + "code": "BSK-E0150", + "severity": "error", + "summary": "Variable defined only in dead version/platform branch", + "summaryHtml": "Variable defined only in dead version/platform branch", + "body": [ + { + "type": "text", + "html": "When sys.version_info, sys.platform, or os.name is compared against a constant, one branch may be statically known to be dead for the configured target Python version (CHKARCH-VERSION-TARGET, issue #93) and platform. Variables defined exclusively in a dead branch are undefined outside that branch." + }, + { + "type": "code", + "lang": "python", + "code": "import sys\n\nif sys.version_info < (3, 8):\n val = \"\" # dead on Python 3.12\nelse:\n other = \"\"\n\nprint(val) # E: `val` is only defined in a dead branch\nprint(other) # OK" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0150" + }, + { + "code": "BSK-E0151", + "severity": "error", + "summary": "Invalid `TypeAliasType(...)` call", + "summaryHtml": "Invalid TypeAliasType(...) call", + "body": [ + { + "type": "text", + "html": "Detects violations in TypeAliasType(...) calls:" + }, + { + "type": "text", + "html": "1. **Invalid type expression**: The value argument is not a valid type form (e.g. a list literal, dict literal, lambda, conditional expression)." + }, + { + "type": "text", + "html": "2. **Circular reference**: The alias value references itself directly or through a forward-reference string." + }, + { + "type": "text", + "html": "3. **Undeclared type variable**: A TypeVar / ParamSpec / TypeVarTuple used in the value is not listed in type_params." + }, + { + "type": "text", + "html": "4. **Non-literal type_params**: The type_params keyword argument is not a literal tuple expression." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import TypeAliasType, TypeVar\n\nT = TypeVar(\"T\")\nS = TypeVar(\"S\")\n\nBad1 = TypeAliasType(\"Bad1\", [int, str]) # E: list is not a type expression\nBad2 = TypeAliasType(\"Bad2\", \"Bad2\") # E: circular reference\nBad3 = TypeAliasType(\"Bad3\", list[S], type_params=(T,)) # E: S not in type_params\nBad4 = TypeAliasType(\"Bad4\", int, type_params=my_tuple) # E: not a literal tuple" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0151" + }, + { + "code": "BSK-E0152", + "severity": "error", + "summary": "Missing type stubs for installed package", + "summaryHtml": "Missing type stubs for installed package", + "body": [ + { + "type": "text", + "html": "Fires when a package is imported and resolves to a .py source file (not .pyi) without a py.typed marker. This means the package is installed but lacks type information, reducing type safety. Strict-by-default: an untyped third-party import is a hard error. Projects can opt out per import (# type: warningBSK-E0152) or globally ("BSK-E0152" = "warning") to import non-type-safe libraries at their own risk." + }, + { + "type": "code", + "lang": "python", + "code": "import flask # E0152: Package 'flask' is installed but has no type stubs" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0152" + }, + { + "code": "BSK-E0153", + "severity": "error", + "summary": "Invalid call to a constructor-derived callable", + "summaryHtml": "Invalid call to a constructor-derived callable", + "body": [ + { + "type": "text", + "html": "(<https://typing.readthedocs.io/en/latest/spec/constructors.html#converting-a-constructor-to-callable>)." + }, + { + "type": "text", + "html": "When a class object flows through an identity-over-callable function such as" + }, + { + "type": "code", + "lang": "python", + "code": "def accepts_callable(cb: Callable[P, R]) -> Callable[P, R]:\n return cb\n\nr1 = accepts_callable(Class1) # r1 has Class1's constructor signature" + }, + { + "type": "text", + "html": "the bound variable (r1) gains the constructor-to-callable signature of the class. Calls to that variable must match the synthesized signature:" + }, + { + "type": "code", + "lang": "python", + "code": "r1() # E0153: missing required argument `x`\nr1(y=1) # E0153: unexpected keyword argument `y`" + }, + { + "type": "text", + "html": "The synthesized signature is derived (in priority order) from the metaclass __call__, then __new__ (when it returns a type other than the class / Self), then __init__, mirroring runtime construction." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0153" + }, + { + "code": "BSK-E0154", + "severity": "error", + "summary": "Access to a module attribute the local stub does not declare", + "summaryHtml": "Access to a module attribute the local stub does not declare", + "body": [ + { + "type": "text", + "html": "When import X resolves to a **user/local stub** (a .pyi under a configured stub-paths dir, including the auto-discovered .basilisk/stubs), that stub is authoritative: Basilisk only knows the names it declares. So X.attr where attr is not declared is a hard error \u2014 the strict-by-default counterpart that makes a hand-written or quick-fix-generated stub mean something." + }, + { + "type": "text", + "html": "The escape hatch is the module-level def __getattr__(name: str) -> Any: ... that the "Create local type stub" quick fix ships by default: keep it and every attribute is allowed (the module stays Any); remove it and declare specific symbols, and undeclared access is flagged." + }, + { + "type": "code", + "lang": "python", + "code": "import cowsay # resolves to .basilisk/stubs/cowsay.pyi\ncowsay.get_output_string(...) # E0154 if the stub declares neither this nor __getattr__" + }, + { + "type": "text", + "html": "Scope (Phase 1): only plain, single-segment import X backed by a user stub. imported_modules is populated only for those, so this rule is a complete no-op for code without local stubs (the conformance suite, first-party code), keeping the false-positive surface at zero." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0154" + }, + { + "code": "BSK-E0155", + "severity": "error", + "summary": "PEP 695 syntax used below the configured target version", + "summaryHtml": "PEP 695 syntax used below the configured target version", + "body": [ + { + "type": "text", + "html": "type X = ... aliases and class FooT / def fT() type-parameter lists are Python 3.12+ syntax (PEP 695). When the configured python_version targets anything older, the file cannot even be parsed by the target interpreter, so this fires as an error (issue #93)." + }, + { + "type": "code", + "lang": "python", + "code": "# python_version = \"3.11\"\ntype Alias = int # E0155 \u2014 `type` statement requires 3.12+\nclass Box[T]: ... # E0155 \u2014 PEP 695 type params require 3.12+\ndef first[T](x: T) -> T: # E0155 \u2014 PEP 695 type params require 3.12+" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0155" + }, + { + "code": "BSK-E0156", + "severity": "error", + "summary": "`TypedDict` `extra_items` / `closed` (PEP 728) violations", + "summaryHtml": "TypedDict extra_items / closed (PEP 728) violations", + "body": [ + { + "type": "text", + "html": "Validates class-definition legality, dict-literal construction, assignability between TypedDicts, and constructor calls against the PEP 728 rules. Operates on the module AST and is independent of resolver state." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0156" + }, + { + "code": "BSK-E0157", + "severity": "error", + "summary": "Dataclass field without a default after a field with a default", + "summaryHtml": "Dataclass field without a default after a field with a default", + "body": [ + { + "type": "text", + "html": "A dataclass synthesizes an __init__ whose parameters follow field declaration order. A field without a default that follows a field with a default would produce a non-default argument after a default one \u2014 a TypeError at class-definition time. field(default=...) and InitVar fields with a value both count as "has a default"; ClassVar, kw_only, and field(init=False) fields are excluded because they do not become positional __init__ parameters." + }, + { + "type": "code", + "lang": "python", + "code": "@dataclass\nclass C:\n a: int = 0\n b: int # E0157: no-default field after a defaulted one" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0157" + }, + { + "code": "BSK-E0158", + "severity": "error", + "summary": "Inconsistent decorators across an overloaded method", + "summaryHtml": "Inconsistent decorators across an overloaded method", + "body": [ + { + "type": "text", + "html": "The typing spec constrains how decorators may be spread across an @overload group and its implementation:" + }, + { + "type": "text", + "html": " If any signature is @staticmethod / @classmethod, all signatures and the implementation must carry the same decorator. @final and @override apply to the implementation only (or, in a stub, the first overload). Placing either on an @overload signature when an implementation is present is an error." + }, + { + "type": "text", + "html": "These checks run only on groups that have a concrete implementation, so stub declarations (which legitimately place @final/@override on the first overload) are never flagged." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0158" + }, + { + "code": "BSK-E0159", + "severity": "error", + "summary": "`@override` on a method with no matching ancestor method", + "summaryHtml": "@override on a method with no matching ancestor method", + "body": [ + { + "type": "text", + "html": "PEP 698 \u2014 a method decorated @override (or typing.override) must actually override a method declared in a base class. When no ancestor declares a method of that name, the decorator is a lie and the type checker should report it." + }, + { + "type": "text", + "html": "To stay free of false positives the check is deliberately conservative: it only fires when the entire ancestor chain is resolvable within the current module (no Any base and no imported base whose methods we cannot see), so a method that legitimately overrides something in an unseen base is never flagged." + }, + { + "type": "code", + "lang": "python", + "code": "class Base:\n def existing(self) -> int: ...\n\nclass Child(Base):\n @override\n def missing(self) -> int: # E0159: nothing named `missing` in any base\n return 1" + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0159" + }, + { + "code": "BSK-E0160", + "severity": "error", + "summary": "Overload implementation is inconsistent with its signatures", + "summaryHtml": "Overload implementation is inconsistent with its signatures", + "body": [ + { + "type": "text", + "html": "When an overload implementation is present the spec requires: the return type of every overload is assignable to the implementation's return type, and the implementation's parameter types are assignable from every overload's parameter types (the implementation must accept them all)." + }, + { + "type": "text", + "html": "To remain false-positive free this only compares **known primitive types** (int/str/bytes/float/bool/complex/object/None and unions of them). Any TypeVar, generic (listint), Callable, or otherwise non-primitive annotation is skipped, since text-level assignability cannot be decided for it." + } + ], + "group": "Type System", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-E0160" + }, + { + "code": "BSK-W0011", + "severity": "warning", + "summary": "Undeclared dependency import", + "summaryHtml": "Undeclared dependency import", + "body": [ + { + "type": "text", + "html": "Fires when an import resolves to a package that is only a transitive dependency \u2014 present in uv.lock but not listed in the project's project.dependencies in pyproject.toml." + }, + { + "type": "text", + "html": "Transitive dependencies can disappear when a direct dependency drops them, breaking imports that relied on their implicit availability." + }, + { + "type": "code", + "lang": "python", + "code": "import urllib3 # W0011: 'urllib3' is a transitive dependency (via requests)" + } + ], + "group": "Warnings", + "docsUrl": "https://www.basilisk-python.dev/warnings/BSK-W0011" + }, + { + "code": "BSK-W0012", + "severity": "warning", + "summary": "Unused dependency", + "summaryHtml": "Unused dependency", + "body": [ + { + "type": "text", + "html": "Fires when a package is declared in project.dependencies but no module in the workspace imports it. This indicates a dependency that can be removed, reducing the project's dependency footprint." + }, + { + "type": "text", + "html": "This is a **whole-module-only** diagnostic \u2014 it requires scanning all files in the workspace to determine which packages are actually imported. The rule currently provides the skeleton; it activates when the workspace layer provides aggregate import data." + } + ], + "group": "Warnings", + "docsUrl": "https://www.basilisk-python.dev/warnings/BSK-W0012" + }, + { + "code": "BSK-W0013", + "severity": "warning", + "summary": "Stale uv lock file", + "summaryHtml": "Stale uv lock file", + "body": [ + { + "type": "text", + "html": "Fires when the uv.lock file is older than pyproject.toml, indicating that dependencies may have changed without re-locking. This can cause import resolution to use stale package versions." + }, + { + "type": "text", + "html": "This rule currently provides the skeleton; it activates when the workspace provides lock-file staleness information via the resolver context." + } + ], + "group": "Warnings", + "docsUrl": "https://www.basilisk-python.dev/warnings/BSK-W0013" + }, + { + "code": "BSK-W0014", + "severity": "warning", + "summary": "Explicit `Any` annotation", + "summaryHtml": "Explicit Any annotation", + "body": [ + { + "type": "text", + "html": "Emitted as a Warning when a function parameter or return annotation is written as Any (from typing). Any silences all type checking for the annotated value and should be used only when intentional." + }, + { + "type": "text", + "html": "This is an opinionated strictness nudge, not a type-system requirement: the typing spec treats Any as a fully valid type. It is therefore a distinct (user-suppressible) code from the genuine return-type-mismatch error (BSK-E0011); the two used to share a code, so a user could not silence the style nudge while keeping the real type check. W0014 itself is never disabled for PEP conformance \u2014 like every rule it runs fully enabled during scoring; there is no "spec-conformance mode" that turns it off. See CHKARCH-CONFORMANCE-MODE." + }, + { + "type": "code", + "lang": "python", + "code": "from typing import Any\n\ndef greet(name: Any) -> str: ... # W0014 \u2014 parameter `name` is annotated Any\ndef parse(text: str) -> Any: ... # W0014 \u2014 return annotation is Any\n\ndef greet(name: str) -> str: ... # NO warning \u2014 concrete types" + } + ], + "group": "Warnings", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-W0014" + }, + { + "code": "BSK-W0040", + "severity": "warning", + "summary": "Lambda function missing type annotations", + "summaryHtml": "Lambda function missing type annotations", + "body": [ + { + "type": "text", + "html": "Emitted when a lambda function is assigned to a variable without type annotations. This is a warning rather than an error since lambda functions are often used for simple operations where type annotations might be considered verbose." + }, + { + "type": "code", + "lang": "python", + "code": "# BAD (warning)\nf = lambda x: x + 1 # W: lambda assigned to unannotated variable 'f'\n\n# GOOD\nf: Callable[[int], int] = lambda x: x + 1 # OK: variable has type annotation" + } + ], + "group": "Warnings", + "docsUrl": "https://www.basilisk-python.dev/warnings/BSK-W0040" + }, + { + "code": "BSK-W0050", + "severity": "warning", + "summary": "Redundant type annotation warning", + "summaryHtml": "Redundant type annotation warning", + "body": [ + { + "type": "text", + "html": "Emits a warning when a type annotation is redundant because the inferred type exactly matches the declared type. This is Basilisk's headline differentiator from other type checkers." + }, + { + "type": "code", + "lang": "python", + "code": "x: int = 42 # W0050 \u2014 annotation is redundant\ny: str = \"hello\" # W0050 \u2014 annotation is redundant\nz: float = 42 # NO warning \u2014 annotation adds information (widening)" + } + ], + "group": "Warnings", + "docsUrl": "https://www.basilisk-python.dev/errors/BSK-W0050" + } +] diff --git a/website/src/assets/css/styles.css b/website/src/assets/css/styles.css index a0f99750..0975eece 100644 --- a/website/src/assets/css/styles.css +++ b/website/src/assets/css/styles.css @@ -1142,3 +1142,31 @@ button { cursor: pointer; font-family: inherit; border: none; background: none; .demo-section { padding: var(--space-12) 0; } .cta-section { padding: var(--space-12) 0; } } + +/* ── Error reference pages ([WEBSITE-ERROR-PAGES]) ─────────────────── */ +.breadcrumb { font-size: 0.8125rem; color: var(--color-text-muted); margin-bottom: var(--space-5); } +.breadcrumb a { color: var(--color-text-secondary); } +.breadcrumb a:hover { color: var(--color-primary-light); } +.breadcrumb span[aria-hidden] { margin: 0 0.4em; color: var(--color-border-bright); } + +.error-title { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; } +.error-title code { font-size: 1.5rem; color: var(--color-text-primary); background: var(--color-bg-tertiary); border: 1px solid var(--color-border-bright); padding: 0.1em 0.4em; } + +.badge { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; padding: 0.2em 0.6em; border-radius: var(--radius-sm); } +.badge--error { color: var(--color-error); background: var(--color-error-dim); border: 1px solid rgba(248, 113, 113, 0.35); } +.badge--warning { color: var(--color-warning); background: rgba(251, 191, 36, 0.12); border: 1px solid rgba(251, 191, 36, 0.35); } + +.error-summary { font-size: 1.125rem; color: var(--color-text-secondary); margin: var(--space-4) 0 var(--space-6); } +.error-screenshot { max-width: 100%; height: auto; border-radius: var(--radius-lg); border: 1px solid var(--color-border-bright); display: block; } +.error-perma { font-size: 0.8125rem; color: var(--color-text-muted); border-top: 1px solid var(--color-border); padding-top: var(--space-4); margin-top: var(--space-8); word-break: break-all; } + +.error-list { list-style: none; padding: 0; margin: 0 0 var(--space-8); display: grid; gap: var(--space-2); } +.error-list li { display: flex; gap: var(--space-3); align-items: baseline; padding: var(--space-2) 0; border-bottom: 1px solid var(--color-border); } +.error-list a { flex: 0 0 auto; } +.error-list__summary { color: var(--color-text-secondary); font-size: 0.9375rem; } +.error-count { color: var(--color-text-muted); font-weight: 400; font-size: 0.875rem; } + +.rules-table { width: 100%; border-collapse: collapse; margin-bottom: var(--space-6); font-size: 0.9375rem; } +.rules-table th, .rules-table td { text-align: left; padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--color-border); vertical-align: top; } +.rules-table th { color: var(--color-text-muted); font-weight: 600; font-size: 0.8125rem; text-transform: uppercase; letter-spacing: 0.05em; } +.rules-table td:first-child { white-space: nowrap; } diff --git a/website/src/assets/images/cli-clean.png b/website/src/assets/images/cli-clean.png index b7d2fdb0..59e9775e 100644 Binary files a/website/src/assets/images/cli-clean.png and b/website/src/assets/images/cli-clean.png differ diff --git a/website/src/assets/images/cli-demo.png b/website/src/assets/images/cli-demo.png index dad0df8d..20126c54 100644 Binary files a/website/src/assets/images/cli-demo.png and b/website/src/assets/images/cli-demo.png differ diff --git a/website/src/assets/images/e0001.png b/website/src/assets/images/e0001.png index 18bdbff2..8a8b6672 100644 Binary files a/website/src/assets/images/e0001.png and b/website/src/assets/images/e0001.png differ diff --git a/website/src/assets/images/e0002.png b/website/src/assets/images/e0002.png index 723a0113..3681ce17 100644 Binary files a/website/src/assets/images/e0002.png and b/website/src/assets/images/e0002.png differ diff --git a/website/src/assets/images/e0003.png b/website/src/assets/images/e0003.png index ae32bb22..f3516057 100644 Binary files a/website/src/assets/images/e0003.png and b/website/src/assets/images/e0003.png differ diff --git a/website/src/assets/images/e0004.png b/website/src/assets/images/e0004.png index b77310bf..2cce3cbb 100644 Binary files a/website/src/assets/images/e0004.png and b/website/src/assets/images/e0004.png differ diff --git a/website/src/assets/images/e0005.png b/website/src/assets/images/e0005.png index 5efd3f00..6c856f09 100644 Binary files a/website/src/assets/images/e0005.png and b/website/src/assets/images/e0005.png differ diff --git a/website/src/assets/images/e0010.png b/website/src/assets/images/e0010.png index 29e78fda..ec48da27 100644 Binary files a/website/src/assets/images/e0010.png and b/website/src/assets/images/e0010.png differ diff --git a/website/src/assets/images/e0011.png b/website/src/assets/images/e0011.png index fc5137bb..d87d97a7 100644 Binary files a/website/src/assets/images/e0011.png and b/website/src/assets/images/e0011.png differ diff --git a/website/src/assets/images/e0012.png b/website/src/assets/images/e0012.png index afb2b781..690ce5aa 100644 Binary files a/website/src/assets/images/e0012.png and b/website/src/assets/images/e0012.png differ diff --git a/website/src/assets/images/e0013.png b/website/src/assets/images/e0013.png index fa6c5fd9..99e4a9a3 100644 Binary files a/website/src/assets/images/e0013.png and b/website/src/assets/images/e0013.png differ diff --git a/website/src/assets/images/e0014.png b/website/src/assets/images/e0014.png index 85d83ede..e4044c93 100644 Binary files a/website/src/assets/images/e0014.png and b/website/src/assets/images/e0014.png differ diff --git a/website/src/assets/images/e0015.png b/website/src/assets/images/e0015.png index 6db5f44c..f859167a 100644 Binary files a/website/src/assets/images/e0015.png and b/website/src/assets/images/e0015.png differ diff --git a/website/src/assets/images/e0016.png b/website/src/assets/images/e0016.png index a6161d07..91a5a734 100644 Binary files a/website/src/assets/images/e0016.png and b/website/src/assets/images/e0016.png differ diff --git a/website/src/assets/images/e0017.png b/website/src/assets/images/e0017.png new file mode 100644 index 00000000..4c2c2a1d Binary files /dev/null and b/website/src/assets/images/e0017.png differ diff --git a/website/src/assets/images/e0018.png b/website/src/assets/images/e0018.png index b5a9e2a9..4070f03d 100644 Binary files a/website/src/assets/images/e0018.png and b/website/src/assets/images/e0018.png differ diff --git a/website/src/assets/images/e0019.png b/website/src/assets/images/e0019.png index 527c98dc..d265d562 100644 Binary files a/website/src/assets/images/e0019.png and b/website/src/assets/images/e0019.png differ diff --git a/website/src/assets/images/e0020.png b/website/src/assets/images/e0020.png new file mode 100644 index 00000000..a02cf44f Binary files /dev/null and b/website/src/assets/images/e0020.png differ diff --git a/website/src/assets/images/e0023.png b/website/src/assets/images/e0023.png new file mode 100644 index 00000000..ec77b822 Binary files /dev/null and b/website/src/assets/images/e0023.png differ diff --git a/website/src/assets/images/e0025.png b/website/src/assets/images/e0025.png index fcb92f4c..cce69e8a 100644 Binary files a/website/src/assets/images/e0025.png and b/website/src/assets/images/e0025.png differ diff --git a/website/src/assets/images/e0026.png b/website/src/assets/images/e0026.png new file mode 100644 index 00000000..025b3106 Binary files /dev/null and b/website/src/assets/images/e0026.png differ diff --git a/website/src/assets/images/e0027.png b/website/src/assets/images/e0027.png new file mode 100644 index 00000000..90b1c436 Binary files /dev/null and b/website/src/assets/images/e0027.png differ diff --git a/website/src/assets/images/e0029.png b/website/src/assets/images/e0029.png new file mode 100644 index 00000000..b13e8f76 Binary files /dev/null and b/website/src/assets/images/e0029.png differ diff --git a/website/src/assets/images/e0031.png b/website/src/assets/images/e0031.png new file mode 100644 index 00000000..c6cec5ca Binary files /dev/null and b/website/src/assets/images/e0031.png differ diff --git a/website/src/assets/images/e0033.png b/website/src/assets/images/e0033.png new file mode 100644 index 00000000..e1e7a358 Binary files /dev/null and b/website/src/assets/images/e0033.png differ diff --git a/website/src/assets/images/e0040.png b/website/src/assets/images/e0040.png new file mode 100644 index 00000000..751fb0cb Binary files /dev/null and b/website/src/assets/images/e0040.png differ diff --git a/website/src/assets/images/e0041.png b/website/src/assets/images/e0041.png new file mode 100644 index 00000000..e75ee03e Binary files /dev/null and b/website/src/assets/images/e0041.png differ diff --git a/website/src/assets/images/e0099.png b/website/src/assets/images/e0099.png new file mode 100644 index 00000000..d37b1a71 Binary files /dev/null and b/website/src/assets/images/e0099.png differ diff --git a/website/src/assets/images/e0115.png b/website/src/assets/images/e0115.png new file mode 100644 index 00000000..1cc45034 Binary files /dev/null and b/website/src/assets/images/e0115.png differ diff --git a/website/src/assets/images/vscode-diagnostics.png b/website/src/assets/images/vscode-diagnostics.png new file mode 100644 index 00000000..2e008aae Binary files /dev/null and b/website/src/assets/images/vscode-diagnostics.png differ diff --git a/website/src/assets/images/vscode-hover.png b/website/src/assets/images/vscode-hover.png new file mode 100644 index 00000000..2e331e6f Binary files /dev/null and b/website/src/assets/images/vscode-hover.png differ diff --git a/website/src/assets/images/vscode-module-explorer.png b/website/src/assets/images/vscode-module-explorer.png new file mode 100644 index 00000000..6a6a8bc9 Binary files /dev/null and b/website/src/assets/images/vscode-module-explorer.png differ diff --git a/website/src/assets/images/vscode-quickfix.png b/website/src/assets/images/vscode-quickfix.png new file mode 100644 index 00000000..2b05b920 Binary files /dev/null and b/website/src/assets/images/vscode-quickfix.png differ diff --git a/website/src/assets/images/zed-screenshot.png b/website/src/assets/images/zed-screenshot.png new file mode 100644 index 00000000..e87015fb Binary files /dev/null and b/website/src/assets/images/zed-screenshot.png differ diff --git a/website/src/docs/index.md b/website/src/docs/index.md index 1d7d38e8..ebad6220 100644 --- a/website/src/docs/index.md +++ b/website/src/docs/index.md @@ -38,6 +38,10 @@ Basilisk takes a different position. It brings the whole stack — type checking - **uv integration** — workspace detection, lock file parsing, and package management commands - Written in **Rust** — ships as a single binary with no runtime dependencies +![Basilisk activity panel in VS Code — Module Explorer with typed-coverage percentage, Python Processes for CPU and memory profiling, and type-checking status](/assets/images/vscode-module-explorer.png) + +*The Basilisk activity panel: module type-coverage, one-click CPU/memory profiling, and live server status.* + ## What Basilisk is not - Not a compiler — your Python code runs on CPython as normal diff --git a/website/src/docs/install-vscode.md b/website/src/docs/install-vscode.md index edfb791e..4faee591 100644 --- a/website/src/docs/install-vscode.md +++ b/website/src/docs/install-vscode.md @@ -25,6 +25,10 @@ The extension is published to the **[VS Code Marketplace](https://marketplace.vi Open a Python file and Basilisk activates automatically — diagnostics, completions, hover, go-to-definition, rename, refactoring, formatting, debugging (F5), and profiling. +![Basilisk in VS Code — strict-by-default type errors shown inline with red squiggles and listed in the Problems panel](/assets/images/vscode-diagnostics.png) + +*Strict-by-default diagnostics the moment you open a file — no configuration.* + ## The binary is bundled — no separate install **The extension ships the matching Basilisk binary for your platform inside the VSIX.** A default install needs no extra setup: no `cargo install`, no PATH configuration, no manual download. diff --git a/website/src/docs/quick-start.md b/website/src/docs/quick-start.md index cb247644..89841a3b 100644 --- a/website/src/docs/quick-start.md +++ b/website/src/docs/quick-start.md @@ -148,6 +148,10 @@ error[BSK-E0001]: Missing parameter type annotation for `data` - **`= note:`** — why the rule exists - **`= see:`** — link to full documentation +The same information is available in your editor — hover any symbol for its inferred type: + +![Basilisk hover in VS Code — hovering a function shows its full inferred signature](/assets/images/vscode-hover.png) + ## Step 6 — Intentional suppressions When you genuinely need to use `Any` or suppress a diagnostic, you can — but you must provide a reason: diff --git a/website/src/docs/refactoring.md b/website/src/docs/refactoring.md index b41ca941..125a4589 100644 --- a/website/src/docs/refactoring.md +++ b/website/src/docs/refactoring.md @@ -15,6 +15,10 @@ eleventyNavigation: Basilisk provides **a full suite of refactoring code actions** via the LSP protocol. They appear in the lightbulb menu in VS Code, Zed, and Neovim automatically (Cursor/Windsurf via Open VSX coming very soon) — no additional extensions or configuration required. +![Basilisk Quick Fix menu in VS Code — fix-all, add annotation, demote or disable a rule, and move-function refactorings](/assets/images/vscode-quickfix.png) + +*The Quick Fix menu (`Cmd/Ctrl+.`) on a Basilisk diagnostic: autofixes, per-rule controls, and refactorings.* + Every refactoring produces a `WorkspaceEdit` that the editor applies atomically. Multi-file refactorings (move symbol, module rename) use `DocumentChanges` with `CreateFile` operations. ## Extract diff --git a/website/src/docs/rules/index.md b/website/src/docs/rules/index.md index 78e9d19d..774ec9cd 100644 --- a/website/src/docs/rules/index.md +++ b/website/src/docs/rules/index.md @@ -17,7 +17,7 @@ Every Basilisk diagnostic has a unique code in the format `BSK-EXXXX` (error) or Rules are enabled by default. You can dial individual rules down per-file or per-path from your editor or `pyproject.toml` — strict is the default, not a cage. -Basilisk ships **155 diagnostic codes** (150 errors, 5 warnings) spanning the full Python typing surface — generics, protocols, dataclasses, TypedDicts, overloads, literals, enums, and more — and is scored by the [official Python typing conformance suite](https://github.com/python/typing/blob/main/conformance/results/results.html) (currently **{{ conformance.scorePct }}%**, {{ conformance.pass }} / {{ conformance.total }} (errors+warnings, strictest); target 100% — [how we measure](/docs/conformance/)). The two foundational groups have worked examples: +Basilisk ships **{{ ruleStats.total }} diagnostic codes** ({{ ruleStats.errors }} errors, {{ ruleStats.warnings }} warnings) spanning the full Python typing surface — generics, protocols, dataclasses, TypedDicts, overloads, literals, enums, and more — and is scored by the [official Python typing conformance suite](https://github.com/python/typing/blob/main/conformance/results/results.html) (currently **{{ conformance.scorePct }}%**, {{ conformance.pass }} / {{ conformance.total }} (errors+warnings, strictest); target 100% — [how we measure](/docs/conformance/)). The two foundational groups have worked examples: | Group | Codes | Description | |---|---|---| @@ -28,162 +28,17 @@ Basilisk ships **155 diagnostic codes** (150 errors, 5 warnings) spanning the fu ## Complete diagnostic reference -Every code currently emitted by the checker. This table is generated from the -checker source (`scripts/gen_rules_reference.py`) — it is the authoritative list. - -| Code | Description | -|---|---| -| `BSK-E0001` | Missing parameter type annotation | -| `BSK-E0002` | Missing return type annotation | -| `BSK-E0003` | Missing variable type annotation | -| `BSK-E0004` | Missing `*args` / `**kwargs` type annotation | -| `BSK-E0005` | Missing class attribute type annotation | -| `BSK-E0010` | Unresolved import | -| `BSK-E0011` | Explicit `Any` annotation / return type mismatch | -| `BSK-E0012` | Argument type mismatch at a call site | -| `BSK-E0013` | Return type mismatch — inferred return type incompatible with annotation | -| `BSK-E0014` | Assignment type incompatibility (literal mismatches) | -| `BSK-E0015` | Invalid type argument count or form | -| `BSK-E0016` | Incompatible method override | -| `BSK-E0017` | Incompatible class attribute override | -| `BSK-E0018` | Undefined variable used in a return statement | -| `BSK-E0019` | Unbound variable on some code paths | -| `BSK-E0020` | Missing `@overload` implementation | -| `BSK-E0021` | Overlapping `@overload` signatures | -| `BSK-E0022` | Unhashable type used as a dict key | -| `BSK-E0023` | Non-exhaustive `match` statement | -| `BSK-E0024` | Invalid type form — numeric literal used as type annotation | -| `BSK-E0025` | Missing `@override` decorator | -| `BSK-E0026` | `TypeVar` declared with exactly one constraint | -| `BSK-E0027` | Duplicate `TypeVar` in a `Generic[...]` base | -| `BSK-E0029` | Method defined inside a `TypedDict` class | -| `BSK-E0030` | Non-default `TypeVar` follows a default `TypeVar` in `Generic[...]` | -| `BSK-E0031` | Invalid `cast()` call | -| `BSK-E0032` | Invalid keyword argument in `TypedDict` class definition | -| `BSK-E0033` | Invalid `reveal_type()` call | -| `BSK-E0034` | `@final` decorator violations | -| `BSK-E0035` | `Required` / `NotRequired` used in an invalid context | -| `BSK-E0036` | `ClassVar` used in an invalid context | -| `BSK-E0037` | Invalid `TypedDict(...)` functional-syntax call | -| `BSK-E0038` | Invalid `TypedDict` inheritance | -| `BSK-E0039` | Invalid `assert_type()` call | -| `BSK-E0040` | Invalid Enum subclassing | -| `BSK-E0041` | Too few arguments in a function call | -| `BSK-E0042` | PEP 695 type parameter syntax mixed with traditional `TypeVars` | -| `BSK-E0043` | Non-TypeVar argument in `Generic[...]` or `Protocol[...]` | -| `BSK-E0044` | `Final` used in an invalid position | -| `BSK-E0045` | Invalid first argument to `Annotated[...]` | -| `BSK-E0046` | Enum member annotated with an explicit type | -| `BSK-E0047` | Invalid type expression in annotation | -| `BSK-E0048` | Invalid right-hand side for a `TypeAlias` annotation | -| `BSK-E0049` | Multiple unbounded tuple components in a single tuple type | -| `BSK-E0050` | Invalid `NewType(...)` call | -| `BSK-E0051` | Invalid `Literal` parameterization | -| `BSK-E0052` | Assignment to attribute of a frozen dataclass instance, or invalid frozen/non-frozen dataclass inheritance | -| `BSK-E0053` | `assert_type()` type mismatch | -| `BSK-E0054` | `Final` type qualifier annotation violations | -| `BSK-E0055` | Invalid `TypeVar` / `TypeVarTuple` / `ParamSpec` keyword argument combination | -| `BSK-E0056` | Mutation of `ReadOnly` `TypedDict` fields | -| `BSK-E0057` | Invalid RHS in a PEP 695 `type X = rhs` statement | -| `BSK-E0058` | `Annotated[...]` requires at least two arguments | -| `BSK-E0059` | Access to `__match_args__` on a dataclass with `match_args=False` | -| `BSK-E0060` | Invalid ordering comparison of dataclass instances | -| `BSK-E0061` | `assert_type` with `Literal[Enum.MEMBER]` on enum-typed param | -| `BSK-E0062` | `-> NoReturn` / `-> Never` function can fall through | -| `BSK-E0063` | Non-hashable dataclass assigned to a `Hashable`-annotated variable | -| `BSK-E0064` | Invalid argument in a `NamedTuple` constructor call | -| `BSK-E0065` | Access to an `int`-only attribute on a `float`-typed parameter | -| `BSK-E0066` | Enum member value incompatible with `_value_` type annotation | -| `BSK-E0067` | Non-member referenced in `Literal[EnumClass.X]` annotation | -| `BSK-E0068` | `Literal["EnumClass.MEMBER"]` (string) used where `Literal[EnumClass.MEMBER]` (enum member reference) is required | -| `BSK-E0069` | Dataclass constructor argument violations | -| `BSK-E0070` | `Never` type compatibility violations | -| `BSK-E0071` | Historical positional-only parameter violations | -| `BSK-E0072` | No matching overload for subscript indexing | -| `BSK-E0073` | `NamedTuple`-to-tuple type incompatibility | -| `BSK-E0074` | Constructor call type mismatch with specialized generic class | -| `BSK-E0075` | Incompatible type for `Self`-typed attribute | -| `BSK-E0076` | Overload union expansion failure | -| `BSK-E0077` | Protocol `Self`-return conformance violation | -| `BSK-E0078` | `Self` type violations in generics | -| `BSK-E0079` | Module assigned to incompatible protocol type | -| `BSK-E0080` | `TypeVar` upper bound violation at call site | -| `BSK-E0081` | `TypeVarTuple` unpack minimum type argument violation | -| `BSK-E0082` | `TypeVarTuple` callable/tuple argument mismatch | -| `BSK-E0083` | `TypeVarTuple` must be unpacked with `*` operator | -| `BSK-E0084` | `TypeVarTuple` variance/bounds/constraints violation | -| `BSK-E0085` | `TypeVarTuple` argument count mismatch | -| `BSK-E0086` | Multiple `TypeVarTuple` unpacks in generic or tuple type | -| `BSK-E0087` | Reserved for future PEP 695 type parameter checks | -| `BSK-E0088` | `TypedDict` runtime violation | -| `BSK-E0089` | Invalid PEP 695 type parameter bound or constraint | -| `BSK-E0090` | Invalid tuple type syntax | -| `BSK-E0091` | Incompatible `TypeVar` bound or constraint with its default | -| `BSK-E0092` | Wrong number of type arguments to a generic class or type alias | -| `BSK-E0093` | Invalid key or value type in `TypedDict` assignment | -| `BSK-E0094` | `Self` type used in an invalid location | -| `BSK-E0095` | `InitVar` field validation in dataclasses | -| `BSK-E0096` | Type mismatch between a dataclass `field(default_factory=…)` and the field's declared type annotation | -| `BSK-E0097` | Protocol `__new__`/`__init__` sets self-attributes not declared in Protocol | -| `BSK-E0098` | Non-Protocol base class in a Protocol definition | -| `BSK-E0099` | Direct instantiation of a Protocol class | -| `BSK-E0100` | Augmented assignment widens `Literal` type | -| `BSK-E0101` | `TypeGuard` or `TypeIs` on method with no narrowing parameter | -| `BSK-E0102` | Invalid `TypeVar` default referencing another `TypeVar` | -| `BSK-E0103` | Tuple index out of bounds | -| `BSK-E0104` | Cyclical type alias reference | -| `BSK-E0105` | Invalid attribute access on bounded type variable | -| `BSK-E0106` | Protocol class used where `type[Proto]` is expected | -| `BSK-E0107` | Variance incompatibility in base class parameterisation | -| `BSK-E0108` | Dataclass slots violations | -| `BSK-E0109` | `TypeVar` bound violation at call site | -| `BSK-E0110` | Protocol variance violation | -| `BSK-E0111` | Constructor call errors via `__init__` method | -| `BSK-E0112` | TypeGuard/TypeIs return type incompatibility in callable arguments | -| `BSK-E0113` | `TypeIs` narrows to a type inconsistent with the input type | -| `BSK-E0114` | Protocol `isinstance`/`issubclass` violations | -| `BSK-E0115` | Use of deprecated class, function, or method | -| `BSK-E0116` | `NamedTuple` class definition errors | -| `BSK-E0117` | Unbound type variable in scope | -| `BSK-E0118` | Calling `super().method()` on an abstract method with no default implementation | -| `BSK-E0119` | Protocol `isinstance`/`issubclass` violations | -| `BSK-E0120` | Generator return type and yield type violations | -| `BSK-E0121` | Protocol conformance violation in annotated assignment | -| `BSK-E0122` | Callable call-site arity and argument validation | -| `BSK-E0123` | `super()` call on abstract protocol method with no default implementation | -| `BSK-E0124` | Protocol attribute tuple element type mismatch | -| `BSK-E0125` | Access to instance attribute on a class object | -| `BSK-E0126` | `LiteralString` and `Literal` assignment incompatibilities | -| `BSK-E0127` | Tuple index out of range | -| `BSK-E0128` | ```TypeVar``` default referential violations | -| `BSK-E0129` | Literal value assignment incompatibility | -| `BSK-E0130` | `TypeVar` scoping violation | -| `BSK-E0131` | Generator yield/send/return type mismatch | -| `BSK-E0132` | Inconsistent `TypeVar` ordering across base classes | -| `BSK-E0133` | Protocol `TypeVar` variance mismatch | -| `BSK-E0134` | Invariant generic type mismatch at call site | -| `BSK-E0136` | Callable subtyping violations (covariance / contravariance) | -| `BSK-E0137` | Generic protocol violations | -| `BSK-E0138` | `dataclass_transform` metaclass violations | -| `BSK-E0139` | Invalid `TypeVarTuple` specialization of generic alias | -| `BSK-E0140` | Callable and Protocol assignment compatibility | -| `BSK-E0141` | Unpack[`TypedDict`] kwargs violations | -| `BSK-E0142` | `dataclass_transform` violations when the transform is applied via a base class | -| `BSK-E0143` | `NamedTuple` usage violations | -| `BSK-E0144` | Invalid constructor call via `type[T]` parameter | -| `BSK-E0145` | Invalid `type[X]` usage violations | -| `BSK-E0146` | Protocol class object violations | -| `BSK-E0147` | Tuple starred-unpack type compatibility violation | -| `BSK-E0148` | Generic type argument violations | -| `BSK-E0149` | PEP 695 generic type parameter scoping violations | -| `BSK-E0150` | Variable defined only in dead version/platform branch | -| `BSK-E0151` | Invalid `TypeAliasType(...)` call | -| `BSK-E0152` | Missing type stubs for installed package | -| `BSK-E0153` | Invalid call to a constructor-derived callable | -| `BSK-E0154` | Access to a module attribute the local stub does not declare | -| `BSK-E0155` | PEP 695 syntax used below the configured target Python version | -| `BSK-W0011` | Undeclared dependency import | -| `BSK-W0012` | Unused dependency | -| `BSK-W0013` | Stale uv lock file | -| `BSK-W0040` | Lambda function missing type annotations | -| `BSK-W0050` | Redundant type annotation warning | +Every code the checker emits — generated from the checker source +(`scripts/gen_rules_reference.py`), so it never drifts. **Each code links to its +own page** with a full explanation and fix, the same page the CLI sends you to +(`see: https://www.basilisk-python.dev/errors/BSK-XXXX`). Browse them all in the +[error reference](/errors/). + + + + +{%- for rule in rules %} + +{%- endfor %} + +
CodeDescription
{{ rule.code }}{{ rule.summaryHtml | safe }}
diff --git a/website/src/errors/error.njk b/website/src/errors/error.njk new file mode 100644 index 00000000..7ae7418d --- /dev/null +++ b/website/src/errors/error.njk @@ -0,0 +1,63 @@ +--- +# Implements [WEBSITE-ERROR-PAGES]: one landing page per diagnostic code, built +# from the checker source (_data/rules.json). Every page resolves the URL the +# CLI prints — `see: https://www.basilisk-python.dev/errors/BSK-XXXX`. +# See docs/specs/WEBSITE-ERROR-PAGES-SPEC.md. +layout: layouts/docs.njk +pagination: + data: rules + size: 1 + alias: rule + # Add every code's page (not just the first) to collections.all so they all + # land in the generated sitemap.xml for discoverability. + addAllPagesToCollections: true +permalink: "/errors/{{ rule.code }}/" +eleventyComputed: + title: "{{ rule.code }}: {{ rule.summary }} — Basilisk" + description: "What Basilisk's {{ rule.code }} ({{ rule.summary }}) diagnostic means and how to fix it — the strict-by-default Python type checker." +--- +{% set stem = examples[rule.code] %} +{% set groupUrl = "/docs/rules/" %} +{% if rule.group == "Missing Annotations" %}{% set groupUrl = "/docs/rules/missing-annotations/" %} +{% elif rule.group == "Type Safety" %}{% set groupUrl = "/docs/rules/type-safety/" %}{% endif %} + + + +

+ {{ rule.code }} + {{ rule.severity }} +

+ +

{{ rule.summaryHtml | safe }}

+ +{% for block in rule.body %} + {% if block.type == "code" %} +
{{ block.code }}
+ {% else %} +

{{ block.html | safe }}

+ {% endif %} +{% endfor %} + +{% if stem %} +

Real basilisk check output

+

What you see when {{ rule.code }} fires on a minimal example:

+basilisk check output reporting {{ rule.code }} — {{ rule.summary }} +{% endif %} + +

How to handle it

+

+ Every rule is on by default — strict is the default, not a cage. You can dial + {{ rule.code }} down per-file or per-path from your editor or + pyproject.toml, or fix the code + so it type-checks. See the {{ rule.group }} rules and + the complete diagnostic reference. +

+ +

+ Canonical URL: {{ rule.docsUrl }} +

diff --git a/website/src/errors/index.njk b/website/src/errors/index.njk new file mode 100644 index 00000000..b8dcf264 --- /dev/null +++ b/website/src/errors/index.njk @@ -0,0 +1,29 @@ +--- +# Implements [WEBSITE-ERROR-PAGES]: browsable directory of every diagnostic. +layout: layouts/docs.njk +title: "Basilisk Error Reference — Every BSK Diagnostic Code Explained" +description: "Browse every Basilisk diagnostic code (BSK-E errors and BSK-W warnings) with an explanation and fix for each — the page the CLI links to for any reported error." +permalink: "/errors/index.html" +--- +

Error reference

+ +

+ Every Basilisk diagnostic has a unique code. When the checker reports one it + links here — see: https://www.basilisk-python.dev/errors/BSK-XXXX — + so you can read what it means and how to fix it. {{ rules.length }} codes in total. +

+ +

For worked examples with real CLI output, start with the + rules overview.

+ +{% for group in ruleGroups %} +

{{ group.group }} ({{ group.items.length }})

+
    + {% for rule in group.items %} +
  • + {{ rule.code }} + {{ rule.summaryHtml | safe }} +
  • + {% endfor %} +
+{% endfor %} diff --git a/website/tests/e2e/errors.spec.ts b/website/tests/e2e/errors.spec.ts new file mode 100644 index 00000000..6846bb20 --- /dev/null +++ b/website/tests/e2e/errors.spec.ts @@ -0,0 +1,70 @@ +import { test, expect, type Page } from "@playwright/test"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { SHOTS } from "../../screenshots/shots.mjs"; + +// Implements [WEBSITE-ERROR-PAGES-VERIFY]: every diagnostic the CLI deep-links to +// must resolve to a rendered /errors// page. The checker prints +// `see: https://www.basilisk-python.dev/errors/BSK-XXXX` for each diagnostic, so a +// missing page is a broken "learn more" link shown to real users. +// See docs/specs/WEBSITE-ERROR-PAGES-SPEC.md. + +type Rule = { code: string; severity: string; summary: string }; + +// The generated data the pages are built from — the single source of truth. +const RULES: Rule[] = JSON.parse( + readFileSync(new URL("../../src/_data/rules.json", import.meta.url), "utf8"), +); + +// Worked-example shots, keyed by the code each one triggers (shot.expect). +const EXAMPLE_CODES = SHOTS.filter( + (s) => /^e\d+$/.test(s.name) && /^BSK-[EW]\d{4}$/.test(s.expect), +).map((s) => ({ code: s.expect, stem: s.name })); + +const expectRendered = async (page: Page, src: string): Promise => { + const img = page.locator(`img[src="${src}"]`).first(); + await img.scrollIntoViewIfNeeded(); + const state = await img.evaluate((el: HTMLImageElement) => + el.decode().then(() => el.naturalWidth, () => 0), + ); + expect(state, `screenshot should decode: ${src}`).toBeGreaterThan(0); +}; + +test.describe("error reference pages", () => { + test("every diagnostic code has a built /errors/ page", () => { + const missing = RULES.map((r) => r.code).filter( + (code) => !existsSync(join(process.cwd(), "_site", "errors", code, "index.html")), + ); + expect(missing, `codes with no /errors/ page: ${missing.join(", ")}`).toHaveLength(0); + expect(RULES.length).toBeGreaterThanOrEqual(155); // every CLI-linked code, at least + }); + + test("a sampled page renders its code, title and severity", async ({ page }) => { + for (const code of ["BSK-E0001", "BSK-W0014", "BSK-E0099", RULES[RULES.length - 1].code]) { + const rule = RULES.find((r) => r.code === code)!; + await page.goto(`/errors/${code}/`); + await expect(page.locator("h1.error-title code")).toHaveText(code); + await expect(page).toHaveTitle(new RegExp(code)); + await expect(page.locator(`.badge--${rule.severity}`)).toBeVisible(); + } + }); + + test("the /errors/ index links every diagnostic code", async ({ page }) => { + await page.goto("/errors/"); + const linked = await page + .locator('.error-list a[href^="/errors/BSK-"]') + .evaluateAll((els) => els.length); + expect(linked).toBe(RULES.length); + }); + + // Each worked example must actually surface on its error page (desktop only — + // rendering is viewport-independent and this navigates ~30 pages). + test("every worked example renders on its error page", async ({ page, isMobile }) => { + test.skip(!!isMobile, "viewport-independent; run once on desktop"); + for (const { code, stem } of EXAMPLE_CODES) { + await page.goto(`/errors/${code}/`); + await expectRendered(page, `/assets/images/${stem}.png`); + } + }); +}); diff --git a/website/tests/e2e/screenshots.spec.ts b/website/tests/e2e/screenshots.spec.ts new file mode 100644 index 00000000..dc330888 --- /dev/null +++ b/website/tests/e2e/screenshots.spec.ts @@ -0,0 +1,92 @@ +import { test, expect, type Page } from "@playwright/test"; + +import { SHOTS } from "../../screenshots/shots.mjs"; + +// Implements [WEBSITE-SCREENSHOTS-VERIFY]: every CLI screenshot the docs embed +// must actually exist and render (non-zero pixels), so a missing or zero-byte +// regeneration is caught in CI rather than shipping a broken image. +// See docs/specs/WEBSITE-SCREENSHOTS-SPEC.md. + +// Derived from the generator manifest so the test can never drift from the set of +// images we actually produce. Rule shots are embedded on their per-code +// /errors// page (see errors.spec.ts); the homepage demo (cli-demo / +// cli-clean) is asserted here. +const HOME_STEMS = SHOTS.map((s) => s.name).filter((n) => !n.startsWith("e0")); + +const stemOf = (src: string): string => src.split("/").pop()!.replace(/\.png$/, ""); + +// Assert a single screenshot has decoded to real pixels. +const expectRendered = async (page: Page, src: string): Promise => { + const img = page.locator(`img[src="${src}"]`).first(); + await img.scrollIntoViewIfNeeded(); + const state = await img.evaluate((el: HTMLImageElement) => + el.decode().then( + () => ({ ok: true, w: el.naturalWidth, h: el.naturalHeight }), + () => ({ ok: false, w: 0, h: 0 }), + ), + ); + expect(state.ok, `screenshot should decode: ${src}`).toBe(true); + expect(state.w, `screenshot should have pixel width: ${src}`).toBeGreaterThan(0); + expect(state.h, `screenshot should have pixel height: ${src}`).toBeGreaterThan(0); +}; + +// Collect the stems of every screenshot-asset referenced on a page, failing +// fast on any request that did not return 200. +const screenshotStemsOn = async (page: Page, path: string): Promise => { + const failed: string[] = []; + page.on("response", (r) => { + if (r.url().includes("/assets/images/") && r.url().endsWith(".png") && r.status() !== 200) { + failed.push(`${r.url()} → ${r.status()}`); + } + }); + await page.goto(path); + expect(failed, `image requests must all succeed: ${failed.join(", ")}`).toHaveLength(0); + + const srcs = await page.locator('img[src*="/assets/images/"]').evaluateAll((imgs) => + imgs.map((el) => (el as HTMLImageElement).getAttribute("src") ?? ""), + ); + return srcs.map(stemOf); +}; + +test.describe("CLI screenshots render", () => { + test("rule docs embed worked-example screenshots and none are broken", async ({ page }) => { + for (const path of ["/docs/rules/missing-annotations/", "/docs/rules/type-safety/"]) { + const stems = await screenshotStemsOn(page, path); + const ruleShots = stems.filter((stem) => stem.startsWith("e0")); + expect(ruleShots.length, `${path} should embed rule screenshots`).toBeGreaterThan(0); + for (const stem of ruleShots) { + await expectRendered(page, `/assets/images/${stem}.png`); + } + } + }); + + test("homepage before/after demo renders both CLI screenshots", async ({ page }) => { + const stems = await screenshotStemsOn(page, "/"); + for (const stem of HOME_STEMS) { + expect(stems.includes(stem), `homepage must embed ${stem}.png`).toBe(true); + } + + // The "before" panel (cli-demo) is visible; reveal "after" (cli-clean), which + // sits in a lazy, initially-hidden tab panel, before asserting it renders. + await expectRendered(page, "/assets/images/cli-demo.png"); + await page.locator('.demo-tab[data-tab="after"]').click(); + await expectRendered(page, "/assets/images/cli-clean.png"); + }); + + // Real VS Code editor screenshots ([VSIX-EDITOR-SCREENSHOTS]) embedded on the + // feature docs — assert each decodes so a missing/zero-byte capture fails CI. + const EDITOR_SHOTS: Array<{ path: string; image: string }> = [ + { path: "/docs/install-vscode/", image: "vscode-diagnostics.png" }, + { path: "/docs/refactoring/", image: "vscode-quickfix.png" }, + { path: "/docs/", image: "vscode-module-explorer.png" }, + { path: "/docs/quick-start/", image: "vscode-hover.png" }, + ]; + + for (const { path, image } of EDITOR_SHOTS) { + test(`${path} embeds the ${image} editor screenshot`, async ({ page }) => { + const stems = await screenshotStemsOn(page, path); + expect(stems.includes(image.replace(/\.png$/, "")), `${path} must embed ${image}`).toBe(true); + await expectRendered(page, `/assets/images/${image}`); + }); + } +});