From 5049b2286d99eb1c843f891bc7dffd50f0e8e06e Mon Sep 17 00:00:00 2001 From: Christian Findlay <16697547+MelbourneDeveloper@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:48:32 +1000 Subject: [PATCH 1/5] First steps --- .claude/skills/screenshot/SKILL.MD | 62 - .github/workflows/ci.yml | 123 +- .github/workflows/deploy-pages.yml | 77 +- CLAUDE.md | 194 +- Makefile | 16 +- README-pypi.md | 210 +- README.md | 204 +- README.zh.md | 193 - basilisk-zed/README.md | 72 +- basilisk-zed/README.zh.md | 70 - basilisk.nvim/README.md | 150 +- basilisk.nvim/README.zh.md | 148 - conformance/test_release_attribution.py | 6 - crates/basilisk-cli/src/adopt.rs | 627 -- crates/basilisk-cli/src/cache_check.rs | 421 - crates/basilisk-cli/src/fix.rs | 547 -- crates/basilisk-cli/src/format.rs | 140 - crates/basilisk-cli/src/import_search.rs | 92 - crates/basilisk-cli/src/mcp.rs | 415 - crates/basilisk-cli/src/mcp/tests.rs | 445 - crates/basilisk-cli/src/output/json.rs | 120 - crates/basilisk-cli/src/output/mod.rs | 1012 --- crates/basilisk-cli/src/output/text.rs | 277 - crates/basilisk-cli/src/pipeline/mod.rs | 587 -- crates/basilisk-cli/src/pipeline/tests.rs | 1016 --- crates/basilisk-cli/src/pipeline/typeshed.rs | 542 -- crates/basilisk-cli/src/stubs.rs | 426 - crates/basilisk-cli/src/typeshed_cli.rs | 550 -- docs/INDEX.md | 7 +- docs/readme/README.src.md | 211 +- docs/readme/README.zh.src.md | 200 - docs/specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md | 112 + docs/specs/WEBSITE-E2E-SPEC.md | 64 +- docs/specs/WEBSITE-ERROR-PAGES-SPEC.md | 83 - docs/specs/WEBSITE-SCREENSHOTS-SPEC.md | 81 - examples/README.md | 124 - examples/README.zh.md | 118 - examples/api_server.py | 88 - examples/api_server_clean.py | 90 - examples/bad.py | 71 - examples/cli_tool.py | 131 - examples/cpu_demo.py | 76 - examples/cpu_demo_loop.py | 86 - examples/data_pipeline.py | 103 - examples/debug_demo.py | 54 - examples/exception_demo.py | 59 - examples/finance.py | 126 - examples/good.py | 59 - examples/heap_demo.py | 140 - examples/memory_demo.py | 83 - examples/mixed.py | 37 - examples/ml_trainer.py | 118 - examples/profile_demo.py | 74 - examples/pyproject.toml | 20 - examples/redundant_annotations.py | 713 -- examples/tests/__init__.py | 0 examples/tests/test_data_pipeline.py | 73 - examples/tests/test_finance.py | 97 - examples/tests/test_good.py | 46 - examples/weird_violations.py | 129 - pyproject.toml | 11 +- screenshots/blog-post-styled.png | Bin 111696 -> 0 bytes screenshots/blog-styled.png | Bin 54734 -> 0 bytes scripts/gen_conformance_reference.py | 221 - scripts/gen_readmes.py | 73 +- scripts/gen_rules_reference.py | 636 -- scripts/gen_withdrawal_copy.py | 208 + scripts/test_published_readmes.py | 95 + vscode-extension/README.md | 210 +- vscode-extension/README.zh.md | 193 - vscode-extension/package.json | 22 +- website/_hero_verify.mjs | 49 - website/eleventy.config.js | 208 +- website/package-lock.json | 63 +- website/package.json | 11 +- website/screenshots/ansi.mjs | 72 - website/screenshots/generate.mjs | 95 - website/screenshots/shots.mjs | 275 - website/screenshots/terminal.mjs | 109 - website/src/404.njk | 18 + website/src/_data/authors.json | 42 - website/src/_data/benchmarks.js | 189 - website/src/_data/conformance.js | 253 - website/src/_data/conformanceOfficial.js | 78 - website/src/_data/conformance_report.json | 1680 ---- website/src/_data/examples.js | 17 - website/src/_data/i18n.json | 118 - website/src/_data/navigation.json | 85 +- website/src/_data/releases.js | 150 - website/src/_data/retiredUrls.json | 299 + website/src/_data/ruleStats.js | 33 - website/src/_data/ruleTagGroups.js | 95 - website/src/_data/rules.json | 7382 ----------------- website/src/_data/site.js | 20 + website/src/_data/site.json | 22 - website/src/_data/withdrawal.json | 26 + website/src/_includes/benchmark-section.njk | 61 - website/src/_includes/components/blog.njk | 41 - website/src/_includes/components/rules.njk | 51 - website/src/_includes/conformance-chart.njk | 50 - website/src/_includes/layouts/base.njk | 218 +- website/src/_includes/layouts/blog.njk | 104 - website/src/_includes/layouts/docs.njk | 129 - .../_includes/pages/blog/categories-pages.njk | 27 - .../src/_includes/pages/blog/categories.njk | 36 - website/src/_includes/pages/blog/index.njk | 35 - .../src/_includes/pages/blog/tags-pages.njk | 26 - website/src/_includes/pages/blog/tags.njk | 32 - website/src/_includes/pages/feed.njk | 26 +- website/src/_includes/pages/llms.txt.njk | 13 + website/src/_includes/pages/sitemap.njk | 27 +- website/src/assets/css/playground.css | 50 - website/src/assets/css/styles.css | 1363 +-- .../assets/images/authors/basilisk-team.png | Bin 7182 -> 0 bytes .../images/authors/christian-findlay.png | Bin 9137 -> 0 bytes .../assets/images/banners/banner-1440x180.png | Bin 57928 -> 0 bytes .../assets/images/banners/banner-1440x180.svg | 21 - .../images/banners/square-1200x1200.png | Bin 302848 -> 0 bytes .../images/banners/square-1200x1200.svg | 20 - .../blog/ai-agents-python-type-checking.png | Bin 381783 -> 0 bytes .../basilisk-037-configuration-editor.png | Bin 321421 -> 0 bytes .../basilisk-037-configuration-preview.png | Bin 112304 -> 0 bytes .../blog/basilisk-037-configuration.png | Bin 218655 -> 0 bytes .../images/blog/basilisk-100-conformance.png | Bin 868890 -> 0 bytes .../src/assets/images/blog/blog-masthead.webp | Bin 69474 -> 0 bytes .../free-threaded-python-type-checking.png | Bin 941161 -> 0 bytes .../images/blog/introducing-basilisk.png | Bin 868956 -> 0 bytes .../images/blog/openai-acquires-astral.png | Bin 1023103 -> 0 bytes .../images/blog/pep-827-type-manipulation.png | Bin 1041774 -> 0 bytes ...ython-315-annotations-fastapi-pydantic.png | Bin 829552 -> 0 bytes website/src/assets/images/debug.png | Bin 1350058 -> 0 bytes website/src/assets/images/e0001.png | Bin 78471 -> 0 bytes website/src/assets/images/e0002.png | Bin 79365 -> 0 bytes website/src/assets/images/e0003.png | Bin 80604 -> 0 bytes website/src/assets/images/e0004.png | Bin 142503 -> 0 bytes website/src/assets/images/e0005.png | Bin 79889 -> 0 bytes website/src/assets/images/e0010.png | Bin 85576 -> 0 bytes website/src/assets/images/e0011.png | Bin 84345 -> 0 bytes website/src/assets/images/e0012.png | Bin 80388 -> 0 bytes website/src/assets/images/e0013.png | Bin 124833 -> 0 bytes website/src/assets/images/e0014.png | Bin 81757 -> 0 bytes website/src/assets/images/e0015.png | Bin 142744 -> 0 bytes website/src/assets/images/e0016.png | Bin 98080 -> 0 bytes website/src/assets/images/e0017.png | Bin 89683 -> 0 bytes website/src/assets/images/e0018.png | Bin 84413 -> 0 bytes website/src/assets/images/e0019.png | Bin 91577 -> 0 bytes website/src/assets/images/e0020.png | Bin 83247 -> 0 bytes website/src/assets/images/e0023.png | Bin 82786 -> 0 bytes website/src/assets/images/e0025.png | Bin 90670 -> 0 bytes website/src/assets/images/e0026.png | Bin 81785 -> 0 bytes website/src/assets/images/e0027.png | Bin 82039 -> 0 bytes website/src/assets/images/e0029.png | Bin 83035 -> 0 bytes website/src/assets/images/e0031.png | Bin 80813 -> 0 bytes website/src/assets/images/e0033.png | Bin 80426 -> 0 bytes website/src/assets/images/e0040.png | Bin 85111 -> 0 bytes website/src/assets/images/e0041.png | Bin 59363 -> 0 bytes website/src/assets/images/e0099.png | Bin 82969 -> 0 bytes website/src/assets/images/e0115.png | Bin 68424 -> 0 bytes website/src/assets/images/logo.svg | 4 - website/src/assets/images/og-image.png | Bin 86944 -> 0 bytes website/src/assets/images/og-image.svg | 41 - website/src/assets/images/rename-poster.jpg | Bin 152859 -> 0 bytes website/src/assets/images/rename.mp4 | Bin 3543748 -> 0 bytes website/src/assets/images/screenshot.png | Bin 1681796 -> 0 bytes .../images/vscode-configuration-editor.png | Bin 375455 -> 0 bytes .../src/assets/images/vscode-diagnostics.png | Bin 289440 -> 0 bytes website/src/assets/images/vscode-hover.png | Bin 128187 -> 0 bytes .../assets/images/vscode-module-explorer.png | Bin 253251 -> 0 bytes website/src/assets/images/vscode-quickfix.png | Bin 284036 -> 0 bytes website/src/assets/images/zed-screenshot.png | Bin 301840 -> 0 bytes website/src/assets/js/demo.js | 54 - website/src/assets/js/playground.js | 94 - website/src/authors/author.njk | 81 - website/src/authors/index.njk | 48 - ...ts-write-python-type-checking-guardrail.md | 122 - ...k-037-python-type-checker-configuration.md | 343 - ...k-100-percent-python-typing-conformance.md | 130 - website/src/blog/blog.json | 3 - ...d-python-why-type-checking-matters-more.md | 122 - website/src/blog/introducing-basilisk.md | 109 - ...uires-astral-what-it-means-for-basilisk.md | 121 - ...5-typeform-fastapi-pydantic-annotations.md | 158 - website/src/blog/type-manipulation-pep-827.md | 195 - website/src/docs/benchmarks.njk | 79 - website/src/docs/comparison.md | 208 - website/src/docs/configuration.md | 657 -- website/src/docs/conformance.md | 78 - website/src/docs/debugging.md | 338 - website/src/docs/formatting.md | 79 - website/src/docs/index.md | 103 - website/src/docs/install-cli.md | 113 - website/src/docs/install-neovim.md | 124 - website/src/docs/install-vscode.md | 63 - website/src/docs/install-zed.md | 116 - website/src/docs/installation.md | 58 - website/src/docs/migration.md | 204 - website/src/docs/profiler.md | 307 - website/src/docs/quick-start.md | 275 - website/src/docs/refactoring.md | 199 - website/src/docs/releases.njk | 71 - website/src/docs/rules/index.md | 38 - website/src/docs/rules/missing-annotations.md | 117 - website/src/docs/rules/tag.njk | 14 - website/src/docs/rules/type-safety.md | 285 - website/src/errors/error.njk | 81 - website/src/errors/index.njk | 37 - website/src/index.njk | 143 +- website/src/notice.njk | 25 + website/src/playground.njk | 31 - ...k-100-percent-python-typing-conformance.md | 113 - website/src/zh/blog/blog.json | 3 - ...d-python-why-type-checking-matters-more.md | 123 - website/src/zh/blog/introducing-basilisk.md | 109 - ...uires-astral-what-it-means-for-basilisk.md | 122 - website/src/zh/docs/comparison.md | 202 - website/src/zh/docs/configuration.md | 571 -- website/src/zh/docs/conformance.md | 74 - website/src/zh/docs/debugging.md | 333 - website/src/zh/docs/index.md | 94 - website/src/zh/docs/install-cli.md | 109 - website/src/zh/docs/install-neovim.md | 120 - website/src/zh/docs/install-vscode.md | 59 - website/src/zh/docs/install-zed.md | 111 - website/src/zh/docs/installation.md | 55 - website/src/zh/docs/migration.md | 162 - website/src/zh/docs/profiler.md | 306 - website/src/zh/docs/quick-start.md | 259 - website/src/zh/docs/refactoring.md | 194 - website/src/zh/docs/rules/index.md | 36 - .../src/zh/docs/rules/missing-annotations.md | 93 - website/src/zh/docs/rules/tag.njk | 15 - website/src/zh/docs/rules/type-safety.md | 235 - website/src/zh/index.njk | 134 - website/tests/e2e/benchmarks.spec.ts | 52 - website/tests/e2e/errors.spec.ts | 85 - website/tests/e2e/homepage.spec.ts | 230 - website/tests/e2e/navigation.spec.ts | 96 - website/tests/e2e/playground.spec.ts | 46 - website/tests/e2e/screenshots.spec.ts | 82 - website/tests/e2e/withdrawal.spec.ts | 177 + 240 files changed, 1429 insertions(+), 36042 deletions(-) delete mode 100644 .claude/skills/screenshot/SKILL.MD delete mode 100644 README.zh.md delete mode 100644 basilisk-zed/README.zh.md delete mode 100644 basilisk.nvim/README.zh.md delete mode 100644 crates/basilisk-cli/src/adopt.rs delete mode 100644 crates/basilisk-cli/src/cache_check.rs delete mode 100644 crates/basilisk-cli/src/fix.rs delete mode 100644 crates/basilisk-cli/src/format.rs delete mode 100644 crates/basilisk-cli/src/import_search.rs delete mode 100644 crates/basilisk-cli/src/mcp.rs delete mode 100644 crates/basilisk-cli/src/mcp/tests.rs delete mode 100644 crates/basilisk-cli/src/output/json.rs delete mode 100644 crates/basilisk-cli/src/output/mod.rs delete mode 100644 crates/basilisk-cli/src/output/text.rs delete mode 100644 crates/basilisk-cli/src/pipeline/mod.rs delete mode 100644 crates/basilisk-cli/src/pipeline/tests.rs delete mode 100644 crates/basilisk-cli/src/pipeline/typeshed.rs delete mode 100644 crates/basilisk-cli/src/stubs.rs delete mode 100644 crates/basilisk-cli/src/typeshed_cli.rs delete mode 100644 docs/readme/README.zh.src.md create mode 100644 docs/specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md delete mode 100644 docs/specs/WEBSITE-ERROR-PAGES-SPEC.md delete mode 100644 docs/specs/WEBSITE-SCREENSHOTS-SPEC.md delete mode 100644 examples/README.md delete mode 100644 examples/README.zh.md delete mode 100644 examples/api_server.py delete mode 100644 examples/api_server_clean.py delete mode 100644 examples/bad.py delete mode 100644 examples/cli_tool.py delete mode 100644 examples/cpu_demo.py delete mode 100644 examples/cpu_demo_loop.py delete mode 100644 examples/data_pipeline.py delete mode 100644 examples/debug_demo.py delete mode 100644 examples/exception_demo.py delete mode 100644 examples/finance.py delete mode 100644 examples/good.py delete mode 100644 examples/heap_demo.py delete mode 100644 examples/memory_demo.py delete mode 100644 examples/mixed.py delete mode 100644 examples/ml_trainer.py delete mode 100644 examples/profile_demo.py delete mode 100644 examples/pyproject.toml delete mode 100644 examples/redundant_annotations.py delete mode 100644 examples/tests/__init__.py delete mode 100644 examples/tests/test_data_pipeline.py delete mode 100644 examples/tests/test_finance.py delete mode 100644 examples/tests/test_good.py delete mode 100644 examples/weird_violations.py delete mode 100644 screenshots/blog-post-styled.png delete mode 100644 screenshots/blog-styled.png delete mode 100644 scripts/gen_conformance_reference.py delete mode 100644 scripts/gen_rules_reference.py create mode 100644 scripts/gen_withdrawal_copy.py create mode 100644 scripts/test_published_readmes.py delete mode 100644 vscode-extension/README.zh.md delete mode 100644 website/_hero_verify.mjs delete mode 100644 website/screenshots/ansi.mjs delete mode 100644 website/screenshots/generate.mjs delete mode 100644 website/screenshots/shots.mjs delete mode 100644 website/screenshots/terminal.mjs create mode 100644 website/src/404.njk delete mode 100644 website/src/_data/authors.json delete mode 100644 website/src/_data/benchmarks.js delete mode 100644 website/src/_data/conformance.js delete mode 100644 website/src/_data/conformanceOfficial.js delete mode 100644 website/src/_data/conformance_report.json delete mode 100644 website/src/_data/examples.js delete mode 100644 website/src/_data/i18n.json delete mode 100644 website/src/_data/releases.js create mode 100644 website/src/_data/retiredUrls.json delete mode 100644 website/src/_data/ruleStats.js delete mode 100644 website/src/_data/ruleTagGroups.js delete mode 100644 website/src/_data/rules.json create mode 100644 website/src/_data/site.js delete mode 100644 website/src/_data/site.json create mode 100644 website/src/_data/withdrawal.json delete mode 100644 website/src/_includes/benchmark-section.njk delete mode 100644 website/src/_includes/components/blog.njk delete mode 100644 website/src/_includes/components/rules.njk delete mode 100644 website/src/_includes/conformance-chart.njk delete mode 100644 website/src/_includes/layouts/blog.njk delete mode 100644 website/src/_includes/layouts/docs.njk delete mode 100644 website/src/_includes/pages/blog/categories-pages.njk delete mode 100644 website/src/_includes/pages/blog/categories.njk delete mode 100644 website/src/_includes/pages/blog/index.njk delete mode 100644 website/src/_includes/pages/blog/tags-pages.njk delete mode 100644 website/src/_includes/pages/blog/tags.njk create mode 100644 website/src/_includes/pages/llms.txt.njk delete mode 100644 website/src/assets/css/playground.css delete mode 100644 website/src/assets/images/authors/basilisk-team.png delete mode 100644 website/src/assets/images/authors/christian-findlay.png delete mode 100644 website/src/assets/images/banners/banner-1440x180.png delete mode 100644 website/src/assets/images/banners/banner-1440x180.svg delete mode 100644 website/src/assets/images/banners/square-1200x1200.png delete mode 100644 website/src/assets/images/banners/square-1200x1200.svg delete mode 100644 website/src/assets/images/blog/ai-agents-python-type-checking.png delete mode 100644 website/src/assets/images/blog/basilisk-037-configuration-editor.png delete mode 100644 website/src/assets/images/blog/basilisk-037-configuration-preview.png delete mode 100644 website/src/assets/images/blog/basilisk-037-configuration.png delete mode 100644 website/src/assets/images/blog/basilisk-100-conformance.png delete mode 100644 website/src/assets/images/blog/blog-masthead.webp delete mode 100644 website/src/assets/images/blog/free-threaded-python-type-checking.png delete mode 100644 website/src/assets/images/blog/introducing-basilisk.png delete mode 100644 website/src/assets/images/blog/openai-acquires-astral.png delete mode 100644 website/src/assets/images/blog/pep-827-type-manipulation.png delete mode 100644 website/src/assets/images/blog/python-315-annotations-fastapi-pydantic.png delete mode 100644 website/src/assets/images/debug.png delete mode 100644 website/src/assets/images/e0001.png delete mode 100644 website/src/assets/images/e0002.png delete mode 100644 website/src/assets/images/e0003.png delete mode 100644 website/src/assets/images/e0004.png delete mode 100644 website/src/assets/images/e0005.png delete mode 100644 website/src/assets/images/e0010.png delete mode 100644 website/src/assets/images/e0011.png delete mode 100644 website/src/assets/images/e0012.png delete mode 100644 website/src/assets/images/e0013.png delete mode 100644 website/src/assets/images/e0014.png delete mode 100644 website/src/assets/images/e0015.png delete mode 100644 website/src/assets/images/e0016.png delete mode 100644 website/src/assets/images/e0017.png delete mode 100644 website/src/assets/images/e0018.png delete mode 100644 website/src/assets/images/e0019.png delete mode 100644 website/src/assets/images/e0020.png delete mode 100644 website/src/assets/images/e0023.png delete mode 100644 website/src/assets/images/e0025.png delete mode 100644 website/src/assets/images/e0026.png delete mode 100644 website/src/assets/images/e0027.png delete mode 100644 website/src/assets/images/e0029.png delete mode 100644 website/src/assets/images/e0031.png delete mode 100644 website/src/assets/images/e0033.png delete mode 100644 website/src/assets/images/e0040.png delete mode 100644 website/src/assets/images/e0041.png delete mode 100644 website/src/assets/images/e0099.png delete mode 100644 website/src/assets/images/e0115.png delete mode 100644 website/src/assets/images/logo.svg delete mode 100644 website/src/assets/images/og-image.png delete mode 100644 website/src/assets/images/og-image.svg delete mode 100644 website/src/assets/images/rename-poster.jpg delete mode 100644 website/src/assets/images/rename.mp4 delete mode 100644 website/src/assets/images/screenshot.png delete mode 100644 website/src/assets/images/vscode-configuration-editor.png delete mode 100644 website/src/assets/images/vscode-diagnostics.png delete mode 100644 website/src/assets/images/vscode-hover.png delete mode 100644 website/src/assets/images/vscode-module-explorer.png delete mode 100644 website/src/assets/images/vscode-quickfix.png delete mode 100644 website/src/assets/images/zed-screenshot.png delete mode 100644 website/src/assets/js/demo.js delete mode 100644 website/src/assets/js/playground.js delete mode 100644 website/src/authors/author.njk delete mode 100644 website/src/authors/index.njk delete mode 100644 website/src/blog/ai-agents-write-python-type-checking-guardrail.md delete mode 100644 website/src/blog/basilisk-037-python-type-checker-configuration.md delete mode 100644 website/src/blog/basilisk-100-percent-python-typing-conformance.md delete mode 100644 website/src/blog/blog.json delete mode 100644 website/src/blog/free-threaded-python-why-type-checking-matters-more.md delete mode 100644 website/src/blog/introducing-basilisk.md delete mode 100644 website/src/blog/openai-acquires-astral-what-it-means-for-basilisk.md delete mode 100644 website/src/blog/python-315-typeform-fastapi-pydantic-annotations.md delete mode 100644 website/src/blog/type-manipulation-pep-827.md delete mode 100644 website/src/docs/benchmarks.njk delete mode 100644 website/src/docs/comparison.md delete mode 100644 website/src/docs/configuration.md delete mode 100644 website/src/docs/conformance.md delete mode 100644 website/src/docs/debugging.md delete mode 100644 website/src/docs/formatting.md delete mode 100644 website/src/docs/index.md delete mode 100644 website/src/docs/install-cli.md delete mode 100644 website/src/docs/install-neovim.md delete mode 100644 website/src/docs/install-vscode.md delete mode 100644 website/src/docs/install-zed.md delete mode 100644 website/src/docs/installation.md delete mode 100644 website/src/docs/migration.md delete mode 100644 website/src/docs/profiler.md delete mode 100644 website/src/docs/quick-start.md delete mode 100644 website/src/docs/refactoring.md delete mode 100644 website/src/docs/releases.njk delete mode 100644 website/src/docs/rules/index.md delete mode 100644 website/src/docs/rules/missing-annotations.md delete mode 100644 website/src/docs/rules/tag.njk delete mode 100644 website/src/docs/rules/type-safety.md delete mode 100644 website/src/errors/error.njk delete mode 100644 website/src/errors/index.njk create mode 100644 website/src/notice.njk delete mode 100644 website/src/playground.njk delete mode 100644 website/src/zh/blog/basilisk-100-percent-python-typing-conformance.md delete mode 100644 website/src/zh/blog/blog.json delete mode 100644 website/src/zh/blog/free-threaded-python-why-type-checking-matters-more.md delete mode 100644 website/src/zh/blog/introducing-basilisk.md delete mode 100644 website/src/zh/blog/openai-acquires-astral-what-it-means-for-basilisk.md delete mode 100644 website/src/zh/docs/comparison.md delete mode 100644 website/src/zh/docs/configuration.md delete mode 100644 website/src/zh/docs/conformance.md delete mode 100644 website/src/zh/docs/debugging.md delete mode 100644 website/src/zh/docs/index.md delete mode 100644 website/src/zh/docs/install-cli.md delete mode 100644 website/src/zh/docs/install-neovim.md delete mode 100644 website/src/zh/docs/install-vscode.md delete mode 100644 website/src/zh/docs/install-zed.md delete mode 100644 website/src/zh/docs/installation.md delete mode 100644 website/src/zh/docs/migration.md delete mode 100644 website/src/zh/docs/profiler.md delete mode 100644 website/src/zh/docs/quick-start.md delete mode 100644 website/src/zh/docs/refactoring.md delete mode 100644 website/src/zh/docs/rules/index.md delete mode 100644 website/src/zh/docs/rules/missing-annotations.md delete mode 100644 website/src/zh/docs/rules/tag.njk delete mode 100644 website/src/zh/docs/rules/type-safety.md delete mode 100644 website/src/zh/index.njk delete mode 100644 website/tests/e2e/benchmarks.spec.ts delete mode 100644 website/tests/e2e/errors.spec.ts delete mode 100644 website/tests/e2e/homepage.spec.ts delete mode 100644 website/tests/e2e/navigation.spec.ts delete mode 100644 website/tests/e2e/playground.spec.ts delete mode 100644 website/tests/e2e/screenshots.spec.ts create mode 100644 website/tests/e2e/withdrawal.spec.ts diff --git a/.claude/skills/screenshot/SKILL.MD b/.claude/skills/screenshot/SKILL.MD deleted file mode 100644 index b542d1df8..000000000 --- a/.claude/skills/screenshot/SKILL.MD +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: screenshot -description: Regenerate real product screenshots — either CLI screenshots (actual `basilisk check` output rendered in a Terminal window) or VS Code editor screenshots (the extension showing diagnostics, hover, quick-fix, activity panel). Use when the user asks to regenerate, update, or add marketing/docs screenshots. Requires a target argument, `cli` or `vsix`. -argument-hint: "cli|vsix [cli-shot-names...]" -arguments: target -allowed-tools: Bash(npm run *), Bash(node screenshots/*), Bash(cargo build *) ---- - -# Regenerate product screenshots - -Screenshots must be **real output of the actual binary/extension**, never hand-typed code fences or synthetic renders (those drift and mislead). Both suites write to the canonical location `website/src/assets/images/` and are committed; CI only verifies they render (`website/tests/e2e/screenshots.spec.ts`), never captures, per `[GITHUB-NO-ARTIFACTS]`. - -## Step 1 — Resolve the target - -The target is `$target` (`$ARGUMENTS`). - -- `cli` → CLI screenshots. Go to **Step 2a**. -- `vsix` (or `vscode`) → VS Code editor screenshots. Go to **Step 2b**. -- **Empty or anything else** → STOP and ask the user which target they want: `cli` or `vsix`. Do not guess — the two paths capture different images with different tooling. - -## Step 2a — CLI screenshots - -Real `basilisk check --color always` output rendered in a faithful macOS Terminal window via Playwright. See `[WEBSITE-SCREENSHOTS]` (`docs/specs/WEBSITE-SCREENSHOTS-SPEC.md`). - -Images: rule shots `e0001.png`…`e0025.png` (named after the code), plus the homepage pair `cli-demo.png` (errors) + `cli-clean.png` (pass). Snippet→expected-code pairings live in `website/screenshots/shots.mjs`; `generate.mjs` **asserts the documented diagnostic actually fires**, so a checker change can't silently ship a misleading image. - -From `website/`: - -```bash -npm run screenshots # regenerate every image -node screenshots/generate.mjs e0001 e0012 # only the named shots -BASILISK_BIN=../target/release/basilisk npm run screenshots # pin the binary -``` - -If the user passed extra args after `cli` (e.g. `e0001 e0012`), pass them through to `node screenshots/generate.mjs ` to regenerate only those. Otherwise regenerate all. - -To **add or change** a shot, edit `website/screenshots/shots.mjs` (snippet + expected code) and rerun — never craft images by hand. After regenerating, run `npm run build` and confirm the images copied to `website/_site/assets/images/`. - -## Step 2b — VS Code editor screenshots - -Headed VS Code capture of the running extension (diagnostics, hover, quick-fix, activity panel) via a dependency-free CDP sidecar (`vscode-extension/screenshot-watcher.mjs`). See `[VSIX-EDITOR-SCREENSHOTS]` (`docs/specs/VSIX-EDITOR-SCREENSHOTS-SPEC.md`). - -Images: `vscode-*.png` in `website/src/assets/images/`. - -The suite is a no-op without the `BASILISK_SCREENSHOTS=1` flag (which `npm run screenshots:editor` sets), so normal `npm test` never opens these windows. - -First build the binaries the extension stages, then run the suite. From `vscode-extension/`: - -```bash -cargo build -p basilisk-cli -p basilisk-profiler-helper -npm run screenshots:editor -``` - -`screenshots:editor` stages the freshly built binary into the dev extension, copies `shipwright.json`, launches the **headed** "Editor screenshots" suite, and the sidecar captures each window to `website/src/assets/images/vscode-*.png`. - -To **add** an editor screenshot, add a `test(...)` that makes the feature visible and calls `takeWindowScreenshot(...)`, then rerun. - -> ⚠️ Never kill a VS Code process (per CLAUDE.md) — it disrupts active debugging/test sessions. Let the headed suite open and close its own windows. - -## Step 3 — Report - -Report exactly which images were regenerated and their paths. If a run asserted a diagnostic and it failed to fire, surface that failure — do not commit a misleading image. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da233c9c0..616b3c4db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,10 +26,9 @@ env: # anyway. Verified empirically: with RUSTFLAGS unset the config flag reaches # rustc; with RUSTFLAGS set it reaches it zero times. # - # Three jobs override this per-job because they link something that is not - # x86_64 linux and would reject `-fuse-ld=lld`: `website` (wasm32-unknown- - # unknown, and it re-adds the playground's 16 MiB stack for the same reason), - # `zed` (wasm32-wasip2), and `test-vscode-windows` (MSVC link.exe). Job-level + # Two jobs override this per-job because they link something that is not + # x86_64 linux and would reject `-fuse-ld=lld`: `zed` (wasm32-wasip2) and + # `test-vscode-windows` (MSVC link.exe). Job-level # `env` merges per-key, so every other job inherits both flags automatically. RUSTFLAGS: "-D warnings -C link-arg=-fuse-ld=lld" @@ -132,28 +131,13 @@ 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 - # 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 ;; - # The playground ships the checker itself, compiled to wasm at - # site-build time ([WASM-BUILD]). A change to ANY crate the - # playground links changes what the published site answers, so the - # site build and its playground e2e must re-run to catch a - # browser-only break. This list is `basilisk-wasm`'s dependency - # closure; extend it when that closure grows. The release deploy - # publishes whatever is on `main`, unfiltered — so a crate that - # skips this check reaches the live playground unverified. - crates/basilisk-wasm/*|crates/basilisk-checker/*|crates/basilisk-common/*) website=true ;; - crates/basilisk-config/*|crates/basilisk-db/*|crates/basilisk-parser/*) website=true ;; - crates/basilisk-resolver/*|crates/basilisk-stubs/*|crates/basilisk-uv/*) website=true ;; - Cargo.lock) website=true ;; - # The conformance score + graded python/typing commit are stamped - # into these by scripts/gen_conformance_reference.py — editing - # them re-runs the website job's stamp drift guard. The published - # READMEs are generated from docs/readme/ ([README]), so touching - # a source OR a generated copy must re-run that guard too. - docs/readme/*|docs/specs/CHECKER-ARCHITECTURE-SPEC.md) website=true ;; + # The site build, plus the messaging spec its every published word + # is extracted from ([WITHDRAWAL-COPY]) — so editing the spec + # re-runs the drift guard and the site build. + website/*|docs/specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md) website=true ;; + # The published READMEs are generated from docs/readme/ ([README]), + # so touching a source OR a generated copy re-runs that guard too. + docs/readme/*) website=true ;; README.md|README.zh.md|README-pypi.md) website=true ;; vscode-extension/README.md|vscode-extension/README.zh.md) website=true ;; esac @@ -201,26 +185,11 @@ jobs: needs: changes if: needs.changes.outputs.website == 'true' runs-on: ubuntu-24.04 - # Overrides the workflow-level RUSTFLAGS: this job links wasm32-unknown- - # unknown, which rejects `-fuse-ld=lld`. It also RESTORES the playground's - # 16 MiB stack ([WASM-BUILD]) — that flag lives in .cargo/config.toml, which - # the workflow-level RUSTFLAGS discards entirely (see the env block above), - # so this pre-merge check was compiling the playground with the 1 MiB wasm - # default while deploy-pages.yml (which sets no RUSTFLAGS) shipped it with - # 16 MiB. The check and the deploy now build the same thing. - env: - RUSTFLAGS: "-D warnings -C link-arg=-zstack-size=16777216" - # 20, not 10: this job compiles the checker to WebAssembly for the - # playground ([WASM-BUILD]) before Eleventy runs, and a cold wasm build of - # the parser + typeshed does not fit the old budget. - timeout-minutes: 20 + # The site is templates and one generated data file — no Rust, no wasm, no + # git history ([WITHDRAWAL-SURFACES]). + timeout-minutes: 10 steps: - # fetch-depth: 0 — mirrors deploy-pages.yml so the conformance over-time - # chart (built from the git history of conformance_status.csv) is exercised - # by the pre-merge build check, not just the post-merge deploy. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: @@ -228,70 +197,36 @@ jobs: cache: npm cache-dependency-path: website/package-lock.json - # The playground engine is the real checker compiled to wasm32, built by - # `npm run build:wasm` ([WASM-BUILD]). Declare the target explicitly - # rather than letting wasm-pack add it implicitly, so a missing target is - # a setup failure here instead of a confusing mid-build one. - - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - with: - targets: wasm32-unknown-unknown - - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - with: - cache-on-failure: true - prefix-key: v1-rust-glibc - - name: Install dependencies 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 + # Every word the site publishes is extracted from the messaging spec + # ([WITHDRAWAL-COPY]); fail if the committed data drifted from it, so the + # site can never say something the spec does not. + - name: Check site copy is in sync with the messaging spec + run: python3 scripts/gen_withdrawal_copy.py --check + + # Every published README renders from docs/readme/ ([README-DRIFT]) — + # GitHub, the VSIX (Marketplace AND Open VSX), and PyPI cannot drift apart + # or be edited in place. + - name: Check generated READMEs 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; } - - # The README source/spec quote the live score + graded python/typing - # commit, stamped by scripts/gen_conformance_reference.py on every scorer - # run; fail if the committed text drifted from conformance_report.json so - # a quoted commit can never go stale ([CHKARCH-CONFORMANCE]). The same - # command then verifies that every published README still renders from - # docs/readme/ unchanged ([README-DRIFT]) — GitHub, the VSIX (Marketplace - # AND Open VSX), and PyPI cannot drift apart or be edited in place. - - name: Check stamped conformance references and generated READMEs - run: python3 scripts/gen_conformance_reference.py --check - - # Separate from `npm run build` ON PURPOSE. The Eleventy build has no Rust - # dependency — every page but the playground renders from committed data — - # so a checker that does not compile can no longer take the whole site - # down, locally or here. The playground e2e below drives the real engine, - # so this job still builds it explicitly. - - name: Build playground engine (wasm) - working-directory: website - run: npm run build:wasm + python3 scripts/gen_readmes.py --check + python3 -m pytest scripts/test_published_readmes.py -q - name: Build site working-directory: website - # GITHUB_TOKEN raises the GitHub API rate limit for _data/releases.js - # (the releases page is generated from the live Releases API at build - # time); the build still degrades gracefully if the call fails. - env: - GITHUB_TOKEN: ${{ github.token }} run: npm run build - # 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. + # Withdrawal-contract tests ([WEBSITE-E2E-WITHDRAWAL]): the statement is + # the approved copy, every retired URL still resolves, and no page says + # anything [WITHDRAWAL-PROHIBITED] forbids. Both presets run on Chromium. - name: Install Playwright browser working-directory: website run: npx playwright install --with-deps chromium - - name: Run navigation + screenshot smoke tests (desktop + mobile) + - name: Run withdrawal-contract 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/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index d12173f1a..915a4a692 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -1,27 +1,25 @@ name: Deploy GitHub Pages -# A RELEASE is the only thing that publishes this site. There is DELIBERATELY no -# `push:` trigger — a merge to `main` must NOT deploy, and adding one back is a -# regression, not a convenience. +# The site is one statement plus a notice at every retired URL +# ([WITHDRAWAL-SURFACES]). It publishes on merge to `main`. # -# Why: the site is not static data. The playground embeds the checker itself, -# compiled to WebAssembly at build time ([WASM-BUILD]), and the docs quote the -# conformance score and the released binary's behaviour. Publishing on merge -# would put an UNRELEASED checker in front of users — the playground would -# answer differently from the `basilisk` anyone can install, and the docs would -# describe a version that does not exist yet. Deploying only on release keeps -# the site in lockstep with the binary it documents. -# -# A change merged to `main` is verified pre-merge by ci.yml's `Website Build` -# job (same build, same playground e2e) and ships with the next release. +# This used to deploy on release ONLY, because the site was coupled to the +# binary: the playground embedded the checker compiled to wasm, and the docs +# quoted the conformance score, so publishing on merge would have put an +# unreleased checker in front of users. None of that is left — there is no +# playground, no score, no documentation of a shipped version, and there will be +# no further release to ride along with. Holding the withdrawal notice back +# until a release that is not coming would leave the old marketing live. on: - # Called by release.yml after a stable GitHub Release is created. A release - # created with the default GITHUB_TOKEN does not emit a `release: published` - # event that can start another workflow, so the release pipeline invokes this - # one directly instead of relying on that event. + push: + branches: [main] + paths: + - "website/**" + - "docs/specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md" + - ".github/workflows/deploy-pages.yml" + # Still callable by release.yml, which invokes this workflow directly. workflow_call: - # Manual escape hatch for an out-of-band site fix between releases (a typo, a - # dead link). Deliberately a human decision — never automatic. + # Manual escape hatch for an out-of-band fix. Deliberately a human decision. workflow_dispatch: permissions: @@ -37,20 +35,14 @@ jobs: deploy: name: Build and deploy runs-on: ubuntu-latest - # 20, not 10: this job compiles the checker to WebAssembly for the - # playground ([WASM-BUILD]) before Eleventy runs. Kept in step with the - # same budget in ci.yml's website job. - timeout-minutes: 20 + # The site is templates and one generated data file — no Rust, no wasm, no + # git history. Kept in step with ci.yml's website job. + timeout-minutes: 10 environment: name: github-pages url: ${{ steps.deploy.outputs.page_url }} steps: - # fetch-depth: 0 — the conformance over-time chart (_data/conformance.js) - # reads the full git history of conformance/conformance_status.csv. A - # shallow clone would collapse the chart to a single point. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: @@ -58,37 +50,18 @@ jobs: cache: npm cache-dependency-path: website/package-lock.json - # The playground engine is the real checker compiled to wasm32, built by - # `npm run build:wasm` ([WASM-BUILD]). Without this the deployed site - # would serve a playground page whose engine never loads. - - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - with: - targets: wasm32-unknown-unknown - - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - with: - cache-on-failure: true - prefix-key: v1-rust-glibc - - name: Install dependencies working-directory: website run: npm ci - # Separate from `npm run build` ON PURPOSE. The Eleventy build has no Rust - # dependency, so a checker that does not compile can no longer take every - # page down with it. The deployed site DOES ship a working playground, so - # this job runs the engine build explicitly and fails here if it breaks. - - name: Build playground engine (wasm) - working-directory: website - run: npm run build:wasm + # Every word the site publishes is extracted from the messaging spec + # ([WITHDRAWAL-COPY]). Deploy must not be able to ship copy the spec does + # not contain, so the drift gate runs here too — not only pre-merge. + - name: Check site copy is in sync with the messaging spec + run: python3 scripts/gen_withdrawal_copy.py --check - name: Build site working-directory: website - # GITHUB_TOKEN raises the GitHub API rate limit for _data/releases.js - # (the releases page is generated from the live Releases API at build - # time); the build still degrades gracefully if the call fails. - env: - GITHUB_TOKEN: ${{ github.token }} run: npm run build - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 diff --git a/CLAUDE.md b/CLAUDE.md index b0a56f019..c667d4535 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,179 +1,43 @@ - # CLAUDE.md -Code here must comfortably pass review at a top-tier engineering org. Fix shortcomings as you find them. +## Where Basilisk stands -# Accuracy Is the Prime Directive +Basilisk's type checker was producing incorrect results. We asked for it to be removed from the `python/typing` results, and it was ([python/typing#2330](https://github.com/python/typing/pull/2330)). The code responsible is not isolated, and we cannot say how many rules are affected. -Basilisk must be correct on Python it has never seen. Every rule decides from the resolved AST — bindings, types, symbol identity — never from how the source happens to be spelled. `from typing import Final as F` behaves identically to `typing.Final`; reformatting a file changes no diagnostic. +A code-quality tool that does not produce correct results is worse than useless. -Basilisk was **removed from the python/typing conformance results** on 2026-08-05, at its own author's request — [python/typing#2330](https://github.com/python/typing/pull/2330), reverting [#2316](https://github.com/python/typing/pull/2316). The reason: *"Many of Basilisk's rules match against raw source text and hard-coded typing symbol names instead of resolved symbols on the AST."* Semantics-preserving edits to the suite — renaming imports, adjusting whitespace — broke **113 of 141 test files**. The score was real; the checker under it was not. +**So: unlist first, no rebuilding until after the public delisting is complete.** Basilisk is being unlisted everywhere and then rebuilt from the ground up as a new product — not fixed, not audited, not salvaged. It will ship only what can be trusted, which most likely will not include type checking. If type checking ever returns, it goes through external auditing before release. -Conformance is a downstream sample of that accuracy, and one this codebase is overfitted to. A change that lifts the conformance number without improving analysis of arbitrary Python is a regression. +Full messaging, including every approved word of public copy: [`docs/specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md`](docs/specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md). It is the single source of truth. Copy from it; never write your own version. -## Text-matched logic — test, delete, report +## The current job -A rule whose behaviour depends on the *spelling* of its input rather than the *meaning* is broken, whatever it scores. Finding and removing it outranks every other task here. Find it by: +1. **Unlist** every channel — Marketplace, Open VSX, Zed, PyPI, Homebrew, Scoop. +2. **Strip** every public surface to the messaging: READMEs, website, listings. +3. **Make the CLI inert** ([WITHDRAWAL-INERT](docs/specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md#WITHDRAWAL-INERT)). -- Raw source-text matching — `.contains` / `starts_with` / `ends_with` on user code (131 of 145 rule files today). -- Hard-coded symbol spellings instead of resolved identity: `t == "typing.Final"`, `text.starts_with("Callable[")`, `import.module == "typing"`. -- Any regex over Python source. -- Logic keyed to a test fixture: rule files named after conformance tests (`generics_base_class_2.rs`, `constructors_call_init`), branches for shapes only the suite contains, comments citing a test file as justification. -- Detection that fires on formatting — line breaks, spacing, quote style, comment text, statement order. +Nothing else is in scope until that is done. -On encountering it, do exactly three things — **do not fix it, do not rewrite it, do not leave a TODO**: +While the eventual plan is to rebuild a Python dev experience tool from the ground up again, you are not currently allowed to contribute to that end. -1. **Write a test that fails** because of the incorrect code — pin the real defect: an aliased import, a reformatted source, a shape the conformance suite never contains. -2. **Delete the offending code.** -3. **Tell the user what you deleted and why**, and that the test is now failing. +## Do not -Replacing it is not your call. The point is to surface every one of these so the user can acknowledge it and decide what gets built back. A checker with fewer rules and visible failing tests is the correct outcome; a diagnostic that only fires on one spelling looks like coverage and isn't. +- **Do not fix, improve, audit, or extend the type checker.** Not a rule, not a diagnostic, not a false positive. That code is finished. Deleting is fine; repairing is not. +- **Do not touch conformance.** Don't run it, quote it, restore it, or resubmit. Never publish a conformance or benchmark figure, in any tense. +- **Do not extract "the good parts" yet.** The code is too contaminated to separate; an extraction now carries the problem into the new product. +- **Do not market anything** — no feature lists, rule counts, or per-rule docs, including for parts that never touched the checker. +- **Do not reassure about scope.** Never "only a few rules", "the language server is unaffected, keep using it". We cannot scope it; saying so is the point. +- **Do not quote the apology.** Link it, neutrally, and nothing more. +- **Do not blame anyone outside the project. Do not give a timeline.** -**A failing test that pins real incorrect behaviour is worth more than a passing fixture carried by logic that does not analyse code.** The first is an accurate map of what Basilisk cannot yet do; the second is a false claim that it can. Given the choice, take the failing test — every time. +## Still standing -## What a correct rule looks like - -The yardstick for judging code — not licence to go and fix it: - -- Decides on the **resolved semantic model** from `basilisk-resolver`, never tokens or text. Parses with `ruff_python_parser`. -- Named for the **typing-spec concept** it implements, not a test file. -- Survives **semantics-preserving mutation**: aliased imports, reformatting, reordering → identical diagnostics. Rules without that coverage are unverified. -- Tested against Python the conformance suite has never contained. - -## Direction of travel - -Background, not a directive: strip text-matched logic, establish which rules genuinely analyse code, and rebuild around those — deliberately, with the user, not as a side effect of some other task. Anything that can't be made to work on the AST gets removed rather than propped up; a smaller trustworthy checker beats a large unreliable one. Analysis Basilisk can't do reliably may be delegated to an external engine. Deletion is a legitimate outcome. - -## Conformance's role - -`python3 conformance/run_conformance.py` stays honest: fresh `git clone` from `python/typing@main`, clean `cargo build --release` from THIS checkout, the suite's own unmodified `src/main.py --only-run basilisk` via `BASILISK_BIN`. A vendored scorer, injected adapter, cached fixtures, or committed results standing in for a live run is a **BUILD FAILURE**. The number is a regression detector, never an objective: - -- **Never publish, quote, or market a conformance figure** — nothing may imply Basilisk is in the official results. -- **Never re-submit to python/typing** until the mutation harness passes clean and an external audit has run. -- Never move the number by touching the scoreboard: rule-suppressing config, deleting source to dodge a failure, hand-editing `conformance/conformance_status.csv`, loosening `coverage-thresholds.json` ([CHKARCH-CONFORMANCE]). -- **A drop caused by removing text-matched logic is progress.** Record it and say so plainly — never restore the code or fake a pass to hold a ratchet. The boundary is intent: deleting a rule to reach a number hides the loss; deleting text-matched logic leaves a failing test behind and reports the drop. -- `coverage-thresholds.json` still gates the pass percentage at 100 with zero false positives, so the first honest deletion fails `make test`. That floor is the incentive that caused the fitting; removing it is the user's call. Until they decide: **delete anyway, report the drop and the failing gate, and stop there.** - -# Design Principles - -One IDE extension = a complete, fast Python workflow. The LSP drives all functionality — extensions only react to LSP signals and NEVER register a command the LSP doesn't advertise. - -**No modes** — behaviour is per-rule configuration ([CHKARCH-CONFIGURATION-ONLY]). Default: every PEP typing-spec rule and nothing else; house-style rules (`BSK-0001/0002/0004` require-annotation, `BSK-0025` require-`@override`, `BSK-0050` redundant-annotation, `BSK-0014` explicit-`Any`) are opt-in. Every diagnostic teaches — why, not just what. - -# Documentation Honesty - -Trust is the product. Applies everywhere — specs, plans, README, website, marketing, code comments. - -- **Every claim about the outside world** (stats, adoption, competitor numbers, market facts, quotes) carries an inline link to the source making it. Link it or delete it. Drifting values link live, never frozen. -- **Self-measured metrics** state how they're measured, are reproducible, and are never compared across methodologies. Conformance isn't publishable at all (above). -- **Book screenshots are release evidence** — captured from the book's pinned released build; never mocked, redrawn, generated, or hand-composed, not even labelled "diagram". Crop and resize freely; never repaint product pixels. No real capture → omit it. See [`book/VISUAL-DESIGN-SYSTEM.md`](book/VISUAL-DESIGN-SYSTEM.md#screenshot-contract). - -# Documentation Structure - -The spec-ID web is non-negotiable: - -- Every spec section has a unique, non-numeric, hierarchical ID (`[GROUP-TOPIC-DETAIL]`). -- Code cites its spec ID in comments (`// Implements [LSP-HOVER]`) so `grep [LSP-` walks spec → code → tests; tests cross-reference both. Anything unlinked gets the missing ID. -- `docs/INDEX.md` indexes `docs/specs/[COMPONENT]-[FEATURE]-SPEC.md` and `docs/plans/[COMPONENT]-[FEATURE]-PLAN.md`. `docs/specs/LSP-ARCHITECTURE-SPEC.md` is the **single source of truth** for shared LSP/DAP/config/commands. - -# Rules - -Build scripts live in the Makefile. [Pyrefly](https://pyrefly.org/en/docs/) and [Pyright](https://microsoft.github.io/pyright/#/) are references to compare against — NEVER copy their code. - -- **Never parse with strings or regex** — `ruff_python_parser` and the resolver only. -- **After correctness, reduce duplication.** `deslop:find-similar` before writing new code, `deslop:top-offenders` after. Merge duplicates. -- Hoist shared code into shared crates/modules. Use [lspkit](https://crates.io/crates/lspkit) where possible. -- One global-state file per app. All mutable state uses Signals — no stale state on screen. -- Keep dependency versions in sync across `.github/workflows/ci.yml` and `.devcontainer/Dockerfile`. -- Define spec models in [typeDiagram markup](https://typediagram.dev/docs/language-reference.html); generate ADTs with its [code generator](https://typediagram.dev/docs/cli.html). -- Don't use Git unless asked. -- Legacy code is code to be removed; there is none here. -- Files under 500 LOC. Move files rather than copying. -- Use your judgment — do NOT stop to ask questions. (Reporting a deletion isn't a question; report and continue.) -- NEVER kill a VS Code process — it disrupts active debugging and test sessions. -- Bug Fix Process: [fix bug skill](.claude/skills/fix-bug/SKILL.md) - -## Git & Branch Discipline - -Off-limits unless explicitly asked. When git IS used: - -- **NEVER push to `main`** — every change ships via PR → CI green → merge. -- **NEVER list the agent as co-author** — no `Co-Authored-By`, no agent attribution. -- **Exactly ONE branch.** Reuse the feature branch; merge multiples into one first. -- **Worktrees are forbidden.** -- **NEVER close anything you did not open** — write `Refs #123`, never `Closes/Fixes #123`. - -## Testing - -Tests must **enforce behaviour**, not work around the gaps in it. Judge a test by what it would catch, never by whether it's green. - -- Tests exercise **meaning, not spelling**: every rule test gets an aliased-import and a reformatted variant, with identical diagnostics. The harness that would enforce this across the suite ([CHKARCH-TESTING-SEMANTIC-MUTATION]) **does not exist yet** — until it does, every rule is unverified and must be described that way. -- Test against Python the conformance suite has never contained. A test copied from `conformance/tests/` cannot detect a rule fitted to `conformance/tests/`. -- NEVER delete a failing test, remove a failure-causing assertion, reduce assertiveness, or ignore tests. Broken functionality gets MORE failing tests, never fewer. -- Target 100% coverage on every measure. Each PR MUST increase overall coverage. Line coverage proves execution, never assertion — a rule at 100% coverage and zero real assertions is the normal failure, not an edge case. -- Mutation score only increases; widen scope with `#[mutation_safe]` tests. The gate ([CHKARCH-TESTING-MUTATION-RATCHET], `mutation_testing/mutation_scores.json`) fails CI if the mutant pool shrinks, caught drops, missed/timeout rise, or kill rate drops. **Read the denominator before the rate:** scope is opt-in, so the committed 100% covers 161 mutants out of an ~82k-LOC crate; timeouts are credited as kills; survivors are aggregated into a count. Never narrow scope to protect a rate, and never kill a mutant by asserting on incidental output instead of the behaviour it changed. -- `make test` is FAIL-FAST — never `--no-fail-fast`. It enforces coverage from `coverage-thresholds.json` at the repo root, not env vars or CI YAML. Ratchet only. -- VSIX tests must not call `whenCommandReady` or `getCommands(true)` to check existence — assert through the UI, or worst case internal VSIX state. - -## Benchmarks - -The benchmark is **indicative, not a gate** ([CHKARCH-TESTING-BENCH]) — it runs on a workstation against whatever else that machine is doing, shifting absolute times by tens of percent between identical runs. **Nothing in CI passes or fails on a benchmark number, and no gate is to be reintroduced.** - -- Run `make bench` when touching checker hot paths. Each run does `cargo clean` + a fresh `--release` build and pulls the latest release of each competitor (pyright, mypy, ty, pyrefly, zuban). -- **Write always.** Numbers go to `benchmarks/status/.csv` after every fixture and at the end (`benchmarks/summarize.py`). Measuring without recording is a lie. -- **Read correctly.** Compare tools *within* one run, never across machines or times. See `website/src/docs/benchmarks.njk`. - -## Logging - -- **Structured only** — `tracing` + `tracing-subscriber`, never `println!`/`eprintln!`. Can't see what's happening? Add more logging. -- Log entry/exit of significant operations with structured fields: `tracing::info!(user_id = 42, action = "checkout")`. -- VS Code extension: detailed logs to a file in the extension's state folder AND the Output Channel. -- **NEVER log PII** or secrets — log `"key: present"` or a truncated hash. - -## Rust Quality - -- Clippy and fmt routinely. All lints at highest strictness (Cargo.toml `[lints]`). Add lints if in doubt; never remove them. -- `unsafe` is forbidden (`unsafe_code = "deny"`). `unwrap()` is always a violation — use `?` with proper error types. No `panic!`, `todo!`, `unimplemented!`. -- `Result` / `Option` everywhere; early returns with `?`. Expressions over statements. Pattern matching over casting. Pure functions. -- Functions <20 lines, low cognitive complexity. Descriptive names (no single letters outside closures). Group into modules; document public APIs. - -# Too Many Cooks — Multi-Agent Coordination - -Register before starting work. The coordinator dictates orders through plans and messages; others follow and check messages regularly. Lock files before editing, never edit locked files, respond promptly. - -# Website - -**Minimize CSS classes**; name them after what the element IS, not what section it's in. Avoid LLM-default colors (e.g. purple) — use RNG and color wheels. - -## Per-diagnostic error pages (`/errors/BSK-XXXX/`) - -Every diagnostic ends with `see: https://www.basilisk-python.dev/errors/BSK-XXXX` (each rule's `ErrorCode.docs_url`). Pages generate from checker source — `[WEBSITE-ERROR-PAGES]`. Single source is `website/src/_data/rules.json`: - -```bash -python3 scripts/gen_rules_reference.py --data -``` - -It extracts the `//! BSK-XXXX:` summary + doc-comment body from each `crates/basilisk-checker/src/rules/*.rs`. **Rerun after adding or renaming a rule** — CI regenerates and `diff`s it (`[WEBSITE-ERROR-PAGES-DRIFT]`). The same data drives `/docs/rules/`. Pages render via `website/src/errors/error.njk`; screenshots appear for any code in `screenshots/shots.mjs`. - -# Architecture - -Strict-by-default Python type checker and comprehensive LSP in **Rust**. Users can flick errors down to warnings and adopt type safety incrementally, or use the LSP alone for autofixes, formatting, debugging, and profiling. - -- **Parser**: `ruff_python_parser`. **Incremental**: Salsa — sub-10ms incremental checks. -- **Formatting**: `ruff_python_formatter` in-process ([LSPFMT-ENGINE]); import hygiene native on the Ruff AST ([LSPFMT-IMPORTS]). The `ruff` CLI is NOT a runtime dependency — never spawn it. -- **Concurrency**: Tokio in the LSP server; analysis single-threaded on one dedicated large-stack thread ([LSPARCH-ARCH-STACK]). -- **No Pyright/mypy/Node.js** — zero TypeScript or Python runtime. - -## Migration to `lspkit` - -LSP scaffolding is being distilled into the `lspkit-*` workspace in [`Nimblesite/lsp_toolkit`](https://github.com/Nimblesite/lsp_toolkit). Prefer `lspkit-*` for new infrastructure; flag in the PR if a patch duplicates it. - -| Current path | Toolkit crate | -|---|---| -| `crates/basilisk-lsp/src/server/mod.rs:96` tower-lsp `Server` | `lspkit-server` | -| `crates/basilisk-lsp/src/workspace.rs:39–116` `WorkspaceIndex` | `lspkit-vfs` + consumer-side index | -| `crates/basilisk-lsp/src/server/handlers/{navigation,features}.rs` | `lspkit-server::Dispatcher::register` | -| `crates/basilisk-lsp/src/server/init.rs:224–242` diagnostics | `lspkit-server::diagnostics::DiagnosticsBus` | -| `crates/basilisk-lsp/src/server/mod.rs:61,64` debounce + watcher | `lspkit-live::watcher` + `lspkit-live::scheduler` | -| `crates/basilisk-lsp/src/config.rs:35–100` `WorkspaceConfig` | `lspkit-config::load_from_ancestor` | -| `crates/basilisk-lsp/tests/lsp/ws_test_common.rs` E2E fixture | not yet in toolkit | +- **Honesty is the product.** Every external claim carries an inline link to its source. Self-measured numbers state their method or don't exist. Screenshots are real captures or absent. +- Internal specs, plans, and [`docs/CONFORMANCE-INTEGRITY-AUDIT.md`](docs/CONFORMANCE-INTEGRITY-AUDIT.md) are the record of what went wrong. Keep them; they are not marketing surfaces. +- Spec IDs stay: `[GROUP-TOPIC-DETAIL]`, cited from code, indexed in [`docs/INDEX.md`](docs/INDEX.md). +- Rust: no `unsafe`, no `unwrap()`, no `panic!`/`todo!`. `Result`/`Option`, early `?`. Clippy and fmt clean. Files under 500 LOC, functions under 20 lines. +- Structured logging via `tracing` only — never `println!`/`eprintln!`, never PII. (The inert CLI notice is the one deliberate direct write to stderr.) +- Never delete a failing test, weaken an assertion, or skip a test to go green. +- Don't use git unless asked. Never push to `main`; never list an agent as co-author; no worktrees; one branch. +- Use your judgment — don't stop to ask questions. +- Never kill a VS Code process. diff --git a/Makefile b/Makefile index 23315eb8d..7ac2c2ab0 100644 --- a/Makefile +++ b/Makefile @@ -445,17 +445,17 @@ _lint_deslop: deslop . && \ echo -e '\033[0;32m✓ Deslop duplication gate passed\033[0m' -# Generated-documentation drift gates. The published READMEs (GitHub, the VSIX -# on both Marketplace and Open VSX, PyPI) are rendered from docs/readme/ -# ([README]), and the diagnostic reference data is generated from the checker -# rule sources ([WEBSITE-ERROR-PAGES-DRIFT]) — editing either output by hand, -# or editing a source without regenerating, fails here as it does in CI. +# Generated-documentation drift gates. The published READMEs are rendered from +# docs/readme/ ([README]), and the site's copy is extracted from the messaging +# spec ([WITHDRAWAL-COPY]) — editing either output by hand, or editing a source +# without regenerating, fails here as it does in CI. The withdrawal gate is the +# load-bearing one: it is what stops the site saying something the messaging +# spec does not. _lint_docs: @echo -e '\033[1m\033[0;36m▶ Checking generated documentation\033[0m' && \ python3 scripts/gen_readmes.py --check && \ - python3 scripts/gen_rules_reference.py --data /tmp/basilisk-rules.json && \ - diff -u website/src/_data/rules.json /tmp/basilisk-rules.json > /dev/null || \ - { echo 'rules.json is stale — run: python3 scripts/gen_rules_reference.py --data'; exit 1; } && \ + python3 scripts/gen_withdrawal_copy.py --check && \ + python3 -m pytest scripts/test_published_readmes.py -q && \ echo -e '\033[0;32m✓ Generated documentation is in sync\033[0m' _fmt_rust: diff --git a/README-pypi.md b/README-pypi.md index 56fcfa987..564985415 100644 --- a/README-pypi.md +++ b/README-pypi.md @@ -1,208 +1,38 @@ -

- Basilisk -

- -

Basilisk

- -

English · 简体中文

- -

- An open-source Python type checker and language server, built in Rust.
- One extension for the whole workflow — diagnostics, autocomplete, refactoring, formatting, debugging, and profiling — driven by a single bundled binary. -

- -> **You are reading the `basilisk-python` wheel listing** — the Basilisk CLI packaged for `pip`/`uv`. The distribution is named `basilisk-python` because `basilisk` was taken on PyPI; the installed command is still `basilisk`. - -

- Website  •  - Install  •  - Quick Start  •  - Rules  •  - Refactoring  •  - GitHub -

- -

- Basilisk in action — type checking, diagnostics, and refactoring in the editor -

- -> ## ⚠️ Do not use Basilisk's type checker in your pipeline -> -> **The type checker still contains code that isn't doing real type checking, and -> it is not yet trustworthy.** Some rules decide from the way code is *spelled* -> rather than what it means, so they can be wrong in both directions — a false -> error on correct code, or silence where there is a genuine bug. Until the audit -> below is finished, don't gate CI on `basilisk check`, don't block a merge with -> it, and don't read a clean run as a clean codebase. -> -> The rest of Basilisk — language server, refactoring, formatting, debugging, -> profiling — does not depend on those rules and is unaffected. - -## Restoring trust: audit, delete, and lean on a checker that works - -We withdrew our former conformance claim and our benchmark figures, and asked to be -[removed from the official `python/typing` results](https://github.com/python/typing/blob/main/conformance/results/results.html). -The cause was checker logic fitted to the contents of conformance test files -instead of implementing the typing specification generally: rules that matched -the *spelling* of code rather than its meaning. Rename an import or reformat a -file and the answer changed. A score produced that way is not evidence. - -**This was a mistake and a failure to verify.** Our process treated the score as -the goal, matching text raises a score faster than real analysis does, and we -published without ever asking whether a rule still held when the same program was -spelled differently. Basilisk's author has published a -[personal account and apology](https://www.christianfindlay.com/blog/basilisk-conformance-apology). - -**So we are auditing every rule and deleting the ones that don't do real type -checking.** Not rewriting them, not patching them, not marking them TODO — -deleting them, with a failing test left behind so the gap is visible instead of -hidden. A rule stays only if it decides from the resolved syntax tree and gives -the same answer when the code is spelled differently. - -**Where a rule can't be made reliable in a straightforward way, we will depend on -a different, established type checker rather than ship our own unreliable version -of it.** An answer from an engine that has earned trust is worth more to you than -a Basilisk-branded one that hasn't. No replacement figure gets published until it -survives off-suite and mutation testing. - -That means Basilisk gets **smaller** before it gets better. Expect fewer rules, -fewer diagnostics, and a lower conformance number. We will report each drop -rather than avoid it. What is left will be code that is honest about what it -does — nothing else. - -### Basilisk is much more than a type checker - -Type checking is one part of it. The rest is a complete Python workflow in a -single Rust binary — language server, refactoring, formatting, integrated -debugging, profiling, and the editor extensions — and none of it rests on the -rules under audit. That is what we are sharpening while the audit runs: make the -parts that are genuinely useful solid, and remove anything that could hand you a -misleading result. The point of getting smaller is to end up with a tool you can -believe. - -[Read the full correction →](https://www.basilisk-python.dev/docs/conformance/)  •  -[Integrity audit →](https://github.com/Nimblesite/Basilisk/blob/main/docs/CONFORMANCE-INTEGRITY-AUDIT.md) - -## What you get - -One extension covers the whole Python workflow. A single bundled Rust binary -drives it — no Node.js, no npm, no `pip install`: - -- **Diagnostics as you type** — incremental analysis powered by [Salsa](https://github.com/salsa-rs/salsa) -- **Autocomplete, hover, go-to-definition, find references, rename** -- **Refactoring code actions** — extract, inline, move symbol, organize imports -- **Integrated debugging** — F5 to debug via bundled [debugpy](https://github.com/microsoft/debugpy); no separate extension -- **Integrated profiling** — CPU heat map, flame graph, and a memory dashboard with leak detection -- **Activity panel** — module tree with per-module type-health coverage, plus feature toggles -- **Inlay hints** and **Ruff** formatting/import-organization, built in -- **Standard-library types from [typeshed](https://github.com/python/typeshed)** — a complete `stdlib/` snapshot is compiled into the binary, so hover and diagnostics work offline with no configuration - -Strictness is configured **per rule**, never by a mode: the unconfigured default -enables the typing-spec rule set, and each rule can be graded down to -`warning`/`info` so a codebase can adopt type safety incrementally. Every -diagnostic carries a `help`, a `note`, and a link to a per-rule explainer, so a -red squiggle tells you *why*. - -## Install - -**Editor extension** — install *Basilisk* from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=Nimblesite.basilisk) or [Open VSX](https://open-vsx.org/extension/Nimblesite/basilisk) (Cursor, Windsurf, and other forks read Open VSX). The Basilisk binary is bundled for macOS (Apple Silicon), Linux (x86_64, aarch64), and Windows (x86_64, aarch64) — nothing else to install. Zed and Neovim 0.10+ extensions are available too. - -**CLI** — on [PyPI as `basilisk-python`](https://pypi.org/project/basilisk-python/); the installed command is `basilisk`: - -```sh -uv tool install basilisk-python # or: pipx install basilisk-python, pip install basilisk-python -``` - -Also via Homebrew (`brew install Nimblesite/tap/basilisk`), Scoop (`scoop bucket add nimblesite https://github.com/Nimblesite/scoop-bucket && scoop install basilisk`), and [GitHub Releases](https://github.com/Nimblesite/Basilisk/releases). Every channel ships the same single Rust CLI, built from this repository at the same version, with no runtime dependencies. Point `basilisk.executablePath` at your own build to have the extension use it. Full options: [install guide](https://www.basilisk-python.dev/docs/installation/). - -## Try it - -The [`examples/`](https://github.com/Nimblesite/Basilisk/blob/main/examples/) folder has ready-to-go Python files: - -```sh -basilisk check examples/bad.py # 8 typing-spec errors — always on, no config needed -basilisk analyze examples/bad.py # the opt-in strictness warnings on the same file -basilisk analyze examples/good.py # clean, even at full strictness -basilisk check examples/mixed.py # one real type error -basilisk check examples/ # the whole folder at once -``` - -Machine-readable output for CI and tooling: - -```sh -basilisk check path/to/your_code.py --output json --color never -``` - -The two commands read one rule universe split by provenance ([`CHKARCH-COMMANDS`](https://github.com/Nimblesite/Basilisk/blob/main/docs/specs/CHECKER-ARCHITECTURE-SPEC.md)): `check` reports -the `pep`-tagged typing-spec rules and nothing else — that set is always on, and -while a config table may grade one of them down to `warning`/`info`, none may -switch it off. `analyze` reports the non-`pep` house rules, which stay silent -until a table selects them. Only `analyze` emits `BSK-` diagnostics. - -## Standard-library types, always offline - -Basilisk resolves the standard library from [typeshed](https://github.com/python/typeshed), -and checking **never downloads anything**. Out of the box it uses the complete -typeshed `stdlib/` snapshot compiled into the binary, reporting the source as -unpinned — so stdlib types work on a plane, behind a firewall, or in an -air-gapped CI runner, with no configuration. - -Pin an exact commit with `typeshed-commit = "<40-char sha>"` under -`[tool.basilisk]`. A pin does exactly one thing: it verifies, offline, that the -typeshed tree in the local store hashes to that commit. If the commit is not on -this machine the run fails hard with `NO SOURCE` rather than substituting -another source — bring it down first with `basilisk typeshed download` (with no -`--commit` it downloads the latest and writes the pin for you), or use the -editor's **Download latest** button. Alternatively, point `typeshed-path` at -your own typeshed tree. Full options: -[configuration guide](https://www.basilisk-python.dev/docs/configuration/). +# Basilisk is unlisted -## Development +> **You are reading the `basilisk-python` wheel listing** — the Basilisk CLI packaged for `pip`/`uv`. -```sh -cargo build # build all crates -cargo test # run all tests -cargo clippy # lint (zero warnings policy) -cargo fmt # format -``` +**Basilisk's type checker was producing incorrect results.** Rules decided from the way code was *spelled* rather than what it meant, so they could be wrong in both directions — a false error on correct code, or silence on a real bug. -Rust 1.87+ required. +**We asked for Basilisk to be removed from the `python/typing` conformance results, and it has been removed.** The score it held was not evidence of anything. -## Contributing +**We cannot tell you how much of the checker this affects.** The code responsible is not isolated to a known set of rules. We will not estimate. That uncertainty is the reason for everything below. -Basilisk is built by a human + AI partnership, with the work split on purpose. See -[CONTRIBUTING.md](https://github.com/Nimblesite/Basilisk/blob/main/CONTRIBUTING.md) — **For Humans** (testing, code-quality review, -conformance/security audits, IDE feature parity, sharpening the AI instructions) and -**For AI** (the technical execution, under the standing rules in [CLAUDE.md](https://github.com/Nimblesite/Basilisk/blob/main/CLAUDE.md)). +**A code-quality tool that does not produce correct results is worse than useless.** Basilisk is being delisted everywhere it was published — VS Code Marketplace, Open VSX, PyPI, Homebrew, Scoop — and the type checker is being made inert. Remove it from your pipeline; it checks nothing, and it fails rather than reporting a clean run. -## Acknowledgments +**We are not fixing Basilisk's type checker code. We are rebuilding Basilisk.** What comes next is a new product, built from the ground up, shipping only what can be shown to be trustworthy. That most likely will not include type checking. Nothing is relisted until it has been rebuilt from components we can vouch for. If type checking ever returns, it will come from established third-party engines, or code audited by a third party. + +Basilisk's author has published a full public account: [an apology](https://www.christianfindlay.com/blog/basilisk-conformance-apology). + +## What to do now -Basilisk builds on the open-source community — with thanks to: +**Remove Basilisk from your pipeline, your pre-commit hooks, and your editor.** Uninstall the CLI and the extension. -- **[Astral](https://astral.sh/)** — [Ruff](https://github.com/astral-sh/ruff), whose parser, AST, and formatter crates Basilisk embeds (MIT). The foundation we rely on most. -- **[typeshed](https://github.com/python/typeshed)** — standard-library type stubs (Apache-2.0, with MIT-licensed parts). -- **[Salsa](https://github.com/salsa-rs/salsa)** — incremental query engine. -- **[Rayon](https://github.com/rayon-rs/rayon)** — data parallelism. -- **[tower-lsp](https://github.com/ebkalderon/tower-lsp)** — LSP scaffolding. -- **[debugpy](https://github.com/microsoft/debugpy)** — debug adapter (bundled in the VS Code extension). -- The [`python/typing`](https://github.com/python/typing) conformance suite. +The type checker is being made inert: it checks nothing, and it exits non-zero so a build that still calls it fails loudly rather than reporting a clean run. Do not treat that failure as a finding about your code. -Full component list, selected licenses, and required notices: [NOTICES](https://github.com/Nimblesite/Basilisk/blob/main/NOTICES) -and [RUST-DEPENDENCY-LICENSES](https://github.com/Nimblesite/Basilisk/blob/main/RUST-DEPENDENCY-LICENSES). Each published -artifact carries its own copies: the VSIX ships Rust notices in -`RUST-DEPENDENCY-LICENSES`, npm notices in `VSCODE-DEPENDENCY-LICENSES`, and -debugpy's license and `ThirdPartyNotices.txt` inside `bundled/debugpy`; the -wheel carries the complete locked notices in its `.dist-info/licenses/` -directory. +**Treat every result Basilisk gave you as unverified.** A clean run was never evidence that your code was clean, and an error it reported may never have been real. + +Every distribution channel is being delisted. Nothing will be relisted until it has been rebuilt from components we can vouch for. + +## Acknowledgments ---- +Basilisk is built on [Ruff](https://github.com/astral-sh/ruff) by [Astral](https://astral.sh/), whose parser, AST, and formatter crates it embeds (MIT), and on standard-library type stubs from [typeshed](https://github.com/python/typeshed) (Apache-2.0, with MIT-licensed parts). Neither project is responsible for how Basilisk used them. Full component list and required notices: [NOTICES](https://github.com/Nimblesite/Basilisk/blob/main/NOTICES) and [RUST-DEPENDENCY-LICENSES](https://github.com/Nimblesite/Basilisk/blob/main/RUST-DEPENDENCY-LICENSES). ## License -Basilisk source code is MIT licensed. Binary distributions also contain -third-party components under the licenses shipped beside each artifact. +Basilisk source code is MIT licensed. Binary distributions also contain third-party components under the licenses shipped beside each artifact. Built by [NIMBLESITE PTY LTD](https://www.nimblesite.co). diff --git a/README.md b/README.md index f4b6ed1eb..9f743e3a1 100644 --- a/README.md +++ b/README.md @@ -1,208 +1,36 @@ -

- Basilisk -

+# Basilisk is unlisted -

Basilisk

- -

English · 简体中文

+> **You are reading the Basilisk source repository** — the checker, language server, editor extensions, and website all live here. -

- An open-source Python type checker and language server, built in Rust.
- One extension for the whole workflow — diagnostics, autocomplete, refactoring, formatting, debugging, and profiling — driven by a single bundled binary. -

+**Basilisk's type checker was producing incorrect results.** Rules decided from the way code was *spelled* rather than what it meant, so they could be wrong in both directions — a false error on correct code, or silence on a real bug. -> **You are reading the Basilisk source repository** — the checker, language server, editor extensions, and website all live here. +**We asked for Basilisk to be removed from the `python/typing` conformance results, and it has been removed.** The score it held was not evidence of anything. -

- Website  •  - Install  •  - Quick Start  •  - Rules  •  - Refactoring  •  - GitHub -

- -

- Basilisk in action — type checking, diagnostics, and refactoring in the editor -

- -> ## ⚠️ Do not use Basilisk's type checker in your pipeline -> -> **The type checker still contains code that isn't doing real type checking, and -> it is not yet trustworthy.** Some rules decide from the way code is *spelled* -> rather than what it means, so they can be wrong in both directions — a false -> error on correct code, or silence where there is a genuine bug. Until the audit -> below is finished, don't gate CI on `basilisk check`, don't block a merge with -> it, and don't read a clean run as a clean codebase. -> -> The rest of Basilisk — language server, refactoring, formatting, debugging, -> profiling — does not depend on those rules and is unaffected. - -## Restoring trust: audit, delete, and lean on a checker that works - -We withdrew our former conformance claim and our benchmark figures, and asked to be -[removed from the official `python/typing` results](https://github.com/python/typing/blob/main/conformance/results/results.html). -The cause was checker logic fitted to the contents of conformance test files -instead of implementing the typing specification generally: rules that matched -the *spelling* of code rather than its meaning. Rename an import or reformat a -file and the answer changed. A score produced that way is not evidence. - -**This was a mistake and a failure to verify.** Our process treated the score as -the goal, matching text raises a score faster than real analysis does, and we -published without ever asking whether a rule still held when the same program was -spelled differently. Basilisk's author has published a -[personal account and apology](https://www.christianfindlay.com/blog/basilisk-conformance-apology). - -**So we are auditing every rule and deleting the ones that don't do real type -checking.** Not rewriting them, not patching them, not marking them TODO — -deleting them, with a failing test left behind so the gap is visible instead of -hidden. A rule stays only if it decides from the resolved syntax tree and gives -the same answer when the code is spelled differently. - -**Where a rule can't be made reliable in a straightforward way, we will depend on -a different, established type checker rather than ship our own unreliable version -of it.** An answer from an engine that has earned trust is worth more to you than -a Basilisk-branded one that hasn't. No replacement figure gets published until it -survives off-suite and mutation testing. - -That means Basilisk gets **smaller** before it gets better. Expect fewer rules, -fewer diagnostics, and a lower conformance number. We will report each drop -rather than avoid it. What is left will be code that is honest about what it -does — nothing else. - -### Basilisk is much more than a type checker - -Type checking is one part of it. The rest is a complete Python workflow in a -single Rust binary — language server, refactoring, formatting, integrated -debugging, profiling, and the editor extensions — and none of it rests on the -rules under audit. That is what we are sharpening while the audit runs: make the -parts that are genuinely useful solid, and remove anything that could hand you a -misleading result. The point of getting smaller is to end up with a tool you can -believe. - -[Read the full correction →](https://www.basilisk-python.dev/docs/conformance/)  •  -[Integrity audit →](docs/CONFORMANCE-INTEGRITY-AUDIT.md) - -## What you get - -One extension covers the whole Python workflow. A single bundled Rust binary -drives it — no Node.js, no npm, no `pip install`: - -- **Diagnostics as you type** — incremental analysis powered by [Salsa](https://github.com/salsa-rs/salsa) -- **Autocomplete, hover, go-to-definition, find references, rename** -- **Refactoring code actions** — extract, inline, move symbol, organize imports -- **Integrated debugging** — F5 to debug via bundled [debugpy](https://github.com/microsoft/debugpy); no separate extension -- **Integrated profiling** — CPU heat map, flame graph, and a memory dashboard with leak detection -- **Activity panel** — module tree with per-module type-health coverage, plus feature toggles -- **Inlay hints** and **Ruff** formatting/import-organization, built in -- **Standard-library types from [typeshed](https://github.com/python/typeshed)** — a complete `stdlib/` snapshot is compiled into the binary, so hover and diagnostics work offline with no configuration - -Strictness is configured **per rule**, never by a mode: the unconfigured default -enables the typing-spec rule set, and each rule can be graded down to -`warning`/`info` so a codebase can adopt type safety incrementally. Every -diagnostic carries a `help`, a `note`, and a link to a per-rule explainer, so a -red squiggle tells you *why*. - -## Install - -**Editor extension** — install *Basilisk* from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=Nimblesite.basilisk) or [Open VSX](https://open-vsx.org/extension/Nimblesite/basilisk) (Cursor, Windsurf, and other forks read Open VSX). The Basilisk binary is bundled for macOS (Apple Silicon), Linux (x86_64, aarch64), and Windows (x86_64, aarch64) — nothing else to install. Zed and Neovim 0.10+ extensions are available too. - -**CLI** — on [PyPI as `basilisk-python`](https://pypi.org/project/basilisk-python/); the installed command is `basilisk`: - -```sh -uv tool install basilisk-python # or: pipx install basilisk-python, pip install basilisk-python -``` - -Also via Homebrew (`brew install Nimblesite/tap/basilisk`), Scoop (`scoop bucket add nimblesite https://github.com/Nimblesite/scoop-bucket && scoop install basilisk`), and [GitHub Releases](https://github.com/Nimblesite/Basilisk/releases). Every channel ships the same single Rust CLI, built from this repository at the same version, with no runtime dependencies. Point `basilisk.executablePath` at your own build to have the extension use it. Full options: [install guide](https://www.basilisk-python.dev/docs/installation/). - -## Try it - -The [`examples/`](examples/) folder has ready-to-go Python files: - -```sh -basilisk check examples/bad.py # 8 typing-spec errors — always on, no config needed -basilisk analyze examples/bad.py # the opt-in strictness warnings on the same file -basilisk analyze examples/good.py # clean, even at full strictness -basilisk check examples/mixed.py # one real type error -basilisk check examples/ # the whole folder at once -``` - -Machine-readable output for CI and tooling: - -```sh -basilisk check path/to/your_code.py --output json --color never -``` - -The two commands read one rule universe split by provenance ([`CHKARCH-COMMANDS`](docs/specs/CHECKER-ARCHITECTURE-SPEC.md)): `check` reports -the `pep`-tagged typing-spec rules and nothing else — that set is always on, and -while a config table may grade one of them down to `warning`/`info`, none may -switch it off. `analyze` reports the non-`pep` house rules, which stay silent -until a table selects them. Only `analyze` emits `BSK-` diagnostics. - -## Standard-library types, always offline - -Basilisk resolves the standard library from [typeshed](https://github.com/python/typeshed), -and checking **never downloads anything**. Out of the box it uses the complete -typeshed `stdlib/` snapshot compiled into the binary, reporting the source as -unpinned — so stdlib types work on a plane, behind a firewall, or in an -air-gapped CI runner, with no configuration. - -Pin an exact commit with `typeshed-commit = "<40-char sha>"` under -`[tool.basilisk]`. A pin does exactly one thing: it verifies, offline, that the -typeshed tree in the local store hashes to that commit. If the commit is not on -this machine the run fails hard with `NO SOURCE` rather than substituting -another source — bring it down first with `basilisk typeshed download` (with no -`--commit` it downloads the latest and writes the pin for you), or use the -editor's **Download latest** button. Alternatively, point `typeshed-path` at -your own typeshed tree. Full options: -[configuration guide](https://www.basilisk-python.dev/docs/configuration/). +**We cannot tell you how much of the checker this affects.** The code responsible is not isolated to a known set of rules. We will not estimate. That uncertainty is the reason for everything below. -## Development +**A code-quality tool that does not produce correct results is worse than useless.** Basilisk is being delisted everywhere it was published — VS Code Marketplace, Open VSX, PyPI, Homebrew, Scoop — and the type checker is being made inert. Remove it from your pipeline; it checks nothing, and it fails rather than reporting a clean run. -```sh -cargo build # build all crates -cargo test # run all tests -cargo clippy # lint (zero warnings policy) -cargo fmt # format -``` +**We are not fixing Basilisk's type checker code. We are rebuilding Basilisk.** What comes next is a new product, built from the ground up, shipping only what can be shown to be trustworthy. That most likely will not include type checking. Nothing is relisted until it has been rebuilt from components we can vouch for. If type checking ever returns, it will come from established third-party engines, or code audited by a third party. -Rust 1.87+ required. +Basilisk's author has published a full public account: [an apology](https://www.christianfindlay.com/blog/basilisk-conformance-apology). -## Contributing +## What to do now -Basilisk is built by a human + AI partnership, with the work split on purpose. See -[CONTRIBUTING.md](CONTRIBUTING.md) — **For Humans** (testing, code-quality review, -conformance/security audits, IDE feature parity, sharpening the AI instructions) and -**For AI** (the technical execution, under the standing rules in [CLAUDE.md](CLAUDE.md)). +**Remove Basilisk from your pipeline, your pre-commit hooks, and your editor.** Uninstall the CLI and the extension. -## Acknowledgments +The type checker is being made inert: it checks nothing, and it exits non-zero so a build that still calls it fails loudly rather than reporting a clean run. Do not treat that failure as a finding about your code. -Basilisk builds on the open-source community — with thanks to: +**Treat every result Basilisk gave you as unverified.** A clean run was never evidence that your code was clean, and an error it reported may never have been real. -- **[Astral](https://astral.sh/)** — [Ruff](https://github.com/astral-sh/ruff), whose parser, AST, and formatter crates Basilisk embeds (MIT). The foundation we rely on most. -- **[typeshed](https://github.com/python/typeshed)** — standard-library type stubs (Apache-2.0, with MIT-licensed parts). -- **[Salsa](https://github.com/salsa-rs/salsa)** — incremental query engine. -- **[Rayon](https://github.com/rayon-rs/rayon)** — data parallelism. -- **[tower-lsp](https://github.com/ebkalderon/tower-lsp)** — LSP scaffolding. -- **[debugpy](https://github.com/microsoft/debugpy)** — debug adapter (bundled in the VS Code extension). -- The [`python/typing`](https://github.com/python/typing) conformance suite. +Every distribution channel is being delisted. Nothing will be relisted until it has been rebuilt from components we can vouch for. -Full component list, selected licenses, and required notices: [NOTICES](NOTICES) -and [RUST-DEPENDENCY-LICENSES](RUST-DEPENDENCY-LICENSES). Each published -artifact carries its own copies: the VSIX ships Rust notices in -`RUST-DEPENDENCY-LICENSES`, npm notices in `VSCODE-DEPENDENCY-LICENSES`, and -debugpy's license and `ThirdPartyNotices.txt` inside `bundled/debugpy`; the -wheel carries the complete locked notices in its `.dist-info/licenses/` -directory. +## Acknowledgments ---- +Basilisk is built on [Ruff](https://github.com/astral-sh/ruff) by [Astral](https://astral.sh/), whose parser, AST, and formatter crates it embeds (MIT), and on standard-library type stubs from [typeshed](https://github.com/python/typeshed) (Apache-2.0, with MIT-licensed parts). Neither project is responsible for how Basilisk used them. Full component list and required notices: [NOTICES](NOTICES) and [RUST-DEPENDENCY-LICENSES](RUST-DEPENDENCY-LICENSES). ## License -Basilisk source code is MIT licensed. Binary distributions also contain -third-party components under the licenses shipped beside each artifact. - -Built by [NIMBLESITE PTY LTD](https://www.nimblesite.co). +Basilisk source code is MIT licensed. Binary distributions also contain third-party components under the licenses shipped beside each artifact. diff --git a/README.zh.md b/README.zh.md deleted file mode 100644 index 85ebcfe03..000000000 --- a/README.zh.md +++ /dev/null @@ -1,193 +0,0 @@ - -

- Basilisk -

- -

Basilisk

- -

English · 简体中文

- -

- 用 Rust 打造的开源 Python 类型检查器与语言服务器。
- 一个扩展覆盖整套工作流 —— 诊断、自动补全、重构、格式化、调试与性能分析 —— 全部由单一捆绑的二进制文件驱动。 -

- -> **你正在阅读 Basilisk 的源码仓库** —— 检查器、语言服务器、编辑器扩展与网站都在这里。 - -

- 网站  •  - 安装  •  - 快速上手  •  - 规则  •  - 重构  •  - GitHub -

- -

- Basilisk 实战 —— 编辑器中的类型检查、诊断与重构 -

- -> ## ⚠️ 请勿在流水线中使用 Basilisk 的类型检查器 -> -> **类型检查器中仍然存在没有做真正类型检查的代码,它目前还不值得信任。** 有些规则 -> 依据的是代码的**写法**而不是含义,因此两个方向上都可能出错 —— 既可能对正确的代码 -> 报出虚假错误,也可能对真实的缺陷保持沉默。在下文所述的审计完成之前,请不要用 -> `basilisk check` 作为 CI 的门禁,不要用它拦截合并,也不要把一次干净的运行结果当作 -> 代码库是干净的。 -> -> Basilisk 的其余部分 —— 语言服务器、重构、格式化、调试、性能分析 —— 并不依赖这些 -> 规则,因此不受影响。 - -## 重建信任:审计、删除,并倚重真正可靠的检查器 - -我们撤回了此前的一致性宣称与基准测试数字,并主动请求 -[从官方 `python/typing` 结果中移除](https://github.com/python/typing/blob/main/conformance/results/results.html)。 -原因是检查器中存在针对一致性测试文件内容而写的逻辑,而不是对类型规范的通用实现: -那些规则匹配的是代码的**写法**,而不是代码的含义。改一个导入别名或重新格式化文件, -结论就会变。这样得出的分数并不能作为证据。 - -**这是一个错误、一次验证上的失职。** 我们的流程把分数当成了目标,而匹配文本比真正做 -分析更快地提高分数;我们在发布之前,始终没有问过这样一个问题 —— 同一个程序换一种 -写法时,这条规则是否依然成立。Basilisk 作者已发表 -[个人说明与致歉](https://www.christianfindlay.com/blog/basilisk-conformance-apology)。 - -**因此,我们正在逐条审计规则,并删除那些没有做真正类型检查的规则。** 不是重写,不是 -打补丁,也不是标一个 TODO —— 是删除,并留下一个失败的测试,让这个缺口可见而不是被 -掩盖。一条规则只有在依据已解析的语法树做判断、并且在代码换一种写法时给出相同结论的 -情况下,才会保留。 - -**如果一条规则无法以直截了当的方式做到可靠,我们会转而依赖另一个成熟的类型检查器, -而不是端出我们自己那份不可靠的实现。** 一个已经赢得信任的引擎给出的答案,对你而言 -比一个挂着 Basilisk 名号却没有赢得信任的答案更有价值。在通过套件之外的用例与变异 -测试之前,我们不会发布任何替代数字。 - -这意味着 Basilisk 会**先变小,再变好**。规则会更少,诊断会更少,一致性数字也会更低。 -每一次下降我们都会如实报告,而不是设法回避。留下来的,将是对自己所做之事诚实的代码 -—— 仅此而已。 - -### Basilisk 远不只是一个类型检查器 - -类型检查只是其中一部分。其余部分是装在单个 Rust 二进制文件里的完整 Python 工作流 -—— 语言服务器、重构、格式化、集成调试、性能分析,以及各个编辑器扩展 —— 它们都不 -建立在正在接受审计的规则之上。这正是我们在审计期间着力打磨的地方:把真正有用的部分 -做扎实,并移除任何可能给出误导性结果的东西。变小的意义,是最终得到一个你可以信赖的 -工具。 - -[阅读完整更正 →](https://www.basilisk-python.dev/zh/docs/conformance/)  •  -[完整性审计 →](docs/CONFORMANCE-INTEGRITY-AUDIT.md) - -## 你能得到什么 - -一个扩展即可覆盖整套 Python 工作流。一切由单一捆绑的 Rust 二进制文件驱动 —— -无需 Node.js、无需 npm、无需 `pip install`: - -- **随输入实时诊断** —— 由 [Salsa](https://github.com/salsa-rs/salsa) 提供增量分析 -- **自动补全、悬停信息、跳转到定义、查找引用、重命名** -- **重构代码操作** —— 提取、内联、移动符号、整理导入 -- **集成调试** —— 按 F5 即可通过捆绑的 [debugpy](https://github.com/microsoft/debugpy) 调试;无需额外扩展 -- **集成性能分析** —— CPU 热力图、火焰图,以及带泄漏检测的内存面板 -- **活动面板** —— 模块树与逐模块的类型健康度覆盖率,并可切换功能开关 -- 内置 **Inlay hints** 与 **Ruff** 格式化/导入整理 -- **来自 [typeshed](https://github.com/python/typeshed) 的标准库类型** —— 完整的 `stdlib/` 快照已编译进二进制文件,因此悬停与诊断在离线且零配置的情况下依然可用 - -严格程度按**规则**配置,而不是靠模式切换:未配置的默认值即启用类型规范规则集, -每条规则都可以降级为 `warning`/`info`,让代码库能够渐进地采用类型安全。每条诊断 -都附带 `help`、`note` 以及指向每条规则详解页的链接,因此一条红色波浪线总能告诉你 -*为什么*。 - -## 安装 - -**编辑器扩展** —— 从 [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=Nimblesite.basilisk) 或 [Open VSX](https://open-vsx.org/extension/Nimblesite/basilisk) 安装 *Basilisk*(Cursor、Windsurf 等分支读取 Open VSX)。Basilisk 二进制文件已为 macOS(Apple Silicon)、Linux(x86_64、aarch64)与 Windows(x86_64、aarch64)捆绑 —— 无需再安装其他东西。Zed 与 Neovim 0.10+ 的扩展同样可用。 - -**命令行工具** —— 在 [PyPI 上名为 `basilisk-python`](https://pypi.org/project/basilisk-python/);安装后的命令是 `basilisk`: - -```sh -uv tool install basilisk-python # 或:pipx install basilisk-python、pip install basilisk-python -``` - -也可通过 Homebrew(`brew install Nimblesite/tap/basilisk`)、Scoop(`scoop bucket add nimblesite https://github.com/Nimblesite/scoop-bucket && scoop install basilisk`)与 [GitHub Releases](https://github.com/Nimblesite/Basilisk/releases) 获取。每个渠道都发布同一个 Rust 命令行工具,由本仓库在同一版本构建,且没有运行时依赖。把 `basilisk.executablePath` 指向你自己的构建,扩展就会使用它。完整选项:[安装指南](https://www.basilisk-python.dev/zh/docs/installation/)。 - -## 试一试 - -[`examples/`](examples/) 目录中有可直接运行的 Python 文件: - -```sh -basilisk check examples/bad.py # 8 处类型规范错误 —— 始终启用,无需配置 -basilisk analyze examples/bad.py # 同一文件上可选的严格性警告 -basilisk analyze examples/good.py # 即使在完全严格下也是干净的 -basilisk check examples/mixed.py # 一处真实的类型错误 -basilisk check examples/ # 一次检查整个目录 -``` - -供 CI 与工具使用的机器可读输出: - -```sh -basilisk check path/to/your_code.py --output json --color never -``` - -这两条命令读取的是按来源划分的同一套规则宇宙([`CHKARCH-COMMANDS`](docs/specs/CHECKER-ARCHITECTURE-SPEC.md)):`check` -只报告带 `pep` 标签的类型规范规则 —— 该集合始终启用,配置表虽可将其中某条 -降级为 `warning`/`info`,但都不能将其关闭。`analyze` 报告非 `pep` 的自有规则, -它们在被配置表选用之前始终保持沉默。只有 `analyze` 会输出 `BSK-` 诊断。 - -## 标准库类型:始终离线 - -Basilisk 从 [typeshed](https://github.com/python/typeshed) 解析标准库类型, -而且检查**从不下载任何东西**。开箱即用时它使用编译进二进制文件的完整 typeshed -`stdlib/` 快照,并将来源报告为未固定(unpinned)—— 因此在飞机上、防火墙后或 -隔离网络的 CI 中,标准库类型都无需配置即可使用。 - -在 `[tool.basilisk]` 中使用 `typeshed-commit = "<40 位 sha>"` 固定到某个确切提交。 -固定只做一件事:离线校验本地存储库中的 typeshed 树是否哈希为该提交。若该提交 -不在本机上,运行会以 `NO SOURCE` 硬失败,而不会替换为其他来源 —— 请先用 -`basilisk typeshed download` 取回(不带 `--commit` 时会下载最新提交并替你写入 -固定项),或使用编辑器中的 **Download latest** 按钮。或者,把 `typeshed-path` -指向你自己的 typeshed 目录树。完整选项参见[配置指南](https://www.basilisk-python.dev/zh/docs/configuration/)。 - -## 开发 - -```sh -cargo build # build all crates -cargo test # run all tests -cargo clippy # lint (zero warnings policy) -cargo fmt # format -``` - -需要 Rust 1.87+。 - -## 贡献 - -Basilisk 由人类与 AI 的协作打造,并有意地划分了各自的工作。请参阅 -[CONTRIBUTING.md](CONTRIBUTING.md) —— **For Humans**(测试、代码质量审查、 -一致性/安全审计、IDE 功能对等、打磨 AI 指令)以及 -**For AI**(在 [CLAUDE.md](CLAUDE.md) 既定规则下的技术执行)。 - -## 致谢 - -Basilisk 建立在开源社区之上 —— 特别感谢: - -- **[Astral](https://astral.sh/)** —— [Ruff](https://github.com/astral-sh/ruff),Basilisk 嵌入了其解析器、AST 与格式化器 crate(MIT)。我们最倚重的基础。 -- **[typeshed](https://github.com/python/typeshed)** —— 标准库类型存根(Apache-2.0,部分内容采用 MIT 许可证)。 -- **[Salsa](https://github.com/salsa-rs/salsa)** —— 增量查询引擎。 -- **[Rayon](https://github.com/rayon-rs/rayon)** —— 数据并行。 -- **[tower-lsp](https://github.com/ebkalderon/tower-lsp)** —— LSP 脚手架。 -- **[debugpy](https://github.com/microsoft/debugpy)** —— 调试适配器(捆绑于 VS Code 扩展)。 -- [`python/typing`](https://github.com/python/typing) 一致性测试套件。 - -完整的组件、所选许可证与必要声明见 [NOTICES](NOTICES) 和 -[RUST-DEPENDENCY-LICENSES](RUST-DEPENDENCY-LICENSES)。每个发布的产物也各自 -携带副本:VSIX 在 `RUST-DEPENDENCY-LICENSES` 中提供 Rust 声明,在 -`VSCODE-DEPENDENCY-LICENSES` 中提供 npm 声明,并在 `bundled/debugpy` 内保留 -debugpy 自身的许可证与 `ThirdPartyNotices.txt`;wheel 则在 `.dist-info/licenses/` -目录中携带完整的锁定声明。 - ---- - -## 许可证 - -Basilisk 源代码采用 MIT 许可证。二进制发行物还包含第三方组件;其许可证 -随每个发行物一并提供。 - -由 [NIMBLESITE PTY LTD](https://www.nimblesite.co) 构建。 diff --git a/basilisk-zed/README.md b/basilisk-zed/README.md index 7842079ab..49d6a74d1 100644 --- a/basilisk-zed/README.md +++ b/basilisk-zed/README.md @@ -1,68 +1,38 @@ -# basilisk-zed + +# Basilisk is unlisted -

English · 简体中文

+> **You are reading the Basilisk Zed extension listing.** -Zed editor extension for Basilisk — WASM-based Python type checking and language server integration. +**Basilisk's type checker was producing incorrect results.** Rules decided from the way code was *spelled* rather than what it meant, so they could be wrong in both directions — a false error on correct code, or silence on a real bug. -Basilisk is an open-source Python type checker and language server built in Rust: diagnostics, autocomplete, refactoring, debugging, and profiling, with strictness configured per rule. +**We asked for Basilisk to be removed from the `python/typing` conformance results, and it has been removed.** The score it held was not evidence of anything. -

- Basilisk in the Zed editor — Python type checking and diagnostics inline -

+**We cannot tell you how much of the checker this affects.** The code responsible is not isolated to a known set of rules. We will not estimate. That uncertainty is the reason for everything below. -> ## ⚠️ Do not use Basilisk's type checker in your pipeline -> -> **The type checker still contains code that isn't doing real type checking, and it is not yet trustworthy.** Some rules decide from the way code is *spelled* rather than what it means, so they can be wrong in both directions — a false error on correct code, or silence where there is a genuine bug. Don't gate CI on it, and don't read a clean run as a clean codebase. Our former conformance claim and our benchmark figures are withdrawn, and Basilisk was [removed from the official results](https://github.com/python/typing/blob/main/conformance/results/results.html) at our request. -> -> **This was a mistake and a failure to verify.** We published on a green run without ever checking whether our rules survived a semantics-preserving change. Basilisk's author has published a [personal account and apology](https://www.christianfindlay.com/blog/basilisk-conformance-apology). -> -> **We are auditing every rule and deleting the ones that don't hold up** — not rewriting them, not patching them, with a failing test left behind so the gap stays visible. Where a rule can't be made reliable in a straightforward way, we will depend on a different, established type checker rather than ship our own unreliable version of it. -> -> **Basilisk is much more than a type checker.** The language server, refactoring, formatting, debugging, and profiling don't rest on the rules under audit — those are what we are sharpening while it runs, removing anything that could hand you a misleading result. We are doing this to restore trust and turn Basilisk back into a tool you can believe. [Read the correction](https://www.basilisk-python.dev/docs/conformance/). +**A code-quality tool that does not produce correct results is worse than useless.** Basilisk is being delisted everywhere it was published — VS Code Marketplace, Open VSX, PyPI, Homebrew, Scoop — and the type checker is being made inert. Remove it from your pipeline; it checks nothing, and it fails rather than reporting a clean run. -## Install +**We are not fixing Basilisk's type checker code. We are rebuilding Basilisk.** What comes next is a new product, built from the ground up, shipping only what can be shown to be trustworthy. That most likely will not include type checking. Nothing is relisted until it has been rebuilt from components we can vouch for. If type checking ever returns, it will come from established third-party engines, or code audited by a third party. -Command palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) → **zed: install dev extension** → select this directory (clone [`Nimblesite/basilisk-zed`](https://github.com/Nimblesite/basilisk-zed) first if you do not have the monorepo). Zed compiles the extension to WASM itself — you never pre-build or copy a `.wasm` file. +Basilisk's author has published a full public account: [an apology](https://www.christianfindlay.com/blog/basilisk-conformance-apology). -**You do not install the Basilisk binary separately.** On first activation the extension downloads the matching binary for your platform from the [GitHub release](https://github.com/Nimblesite/Basilisk/releases), caches it inside Zed's extension directory, and reuses it until a newer release appears. Override it only for development or a system install, via `lsp.basilisk.binary.path` in `settings.json` or the `BASILISK_PATH` environment variable. +## What to do now -> The extension is not yet listed in the [Zed extension registry](https://github.com/zed-industries/extensions); until that listing lands, the dev-extension flow above is the install path. +**Remove Basilisk from your pipeline, your pre-commit hooks, and your editor.** Uninstall the CLI and the extension. -Full instructions, settings, debugging, and the slash-command reference: [basilisk-python.dev/docs/install-zed](https://www.basilisk-python.dev/docs/install-zed/). +The type checker is being made inert: it checks nothing, and it exits non-zero so a build that still calls it fails loudly rather than reporting a clean run. Do not treat that failure as a finding about your code. -## Role in Basilisk +**Treat every result Basilisk gave you as unverified.** A clean run was never evidence that your code was clean, and an error it reported may never have been real. -This is the **Zed editor integration**. It is a native Zed extension compiled to WASM that connects the Basilisk language server to Zed, providing real-time diagnostics, hover, go-to-definition, code actions, and debugging via DAP. +Every distribution channel is being delisted. Nothing will be relisted until it has been rebuilt from components we can vouch for. -## Key concepts +## Acknowledgments -- **WASM extension** — compiled as a `cdylib` crate targeting `wasm32-wasip2`, loaded natively by Zed. -- **`zed_extension_api`** — uses Zed's official extension API for language server lifecycle management. -- **`basilisk-common`** — shares diagnostic codes and constants with the rest of the Basilisk workspace (also WASM-compatible). -- **Built-in Python, untouched** — binds to Zed's own Python language by name. The extension ships no `languages/` directory and no grammar, so Zed compiles nothing from source and your highlighting, brackets, indent rules, and runnables stay exactly as Zed ships them. -- **DAP debugging** — supports the Debug Adapter Protocol for integrated Python debugging. - -## Building - -From a monorepo checkout, build the extension and set up the local dev loop: - -```sh -make package-zed -``` - -Standalone (this repository on its own), the build is exactly the one the release pipeline gates the publish on: - -```sh -cargo build --release --target wasm32-wasip2 -``` - -## Dependencies - -| Crate | Purpose | -|-------|---------| -| `zed_extension_api` | Zed extension API | -| `basilisk-common` | Shared constants and types | +Basilisk is built on [Ruff](https://github.com/astral-sh/ruff) by [Astral](https://astral.sh/), whose parser, AST, and formatter crates it embeds (MIT), and on standard-library type stubs from [typeshed](https://github.com/python/typeshed) (Apache-2.0, with MIT-licensed parts). Neither project is responsible for how Basilisk used them. Full component list and required notices: [NOTICES](https://github.com/Nimblesite/Basilisk/blob/main/NOTICES) and [RUST-DEPENDENCY-LICENSES](https://github.com/Nimblesite/Basilisk/blob/main/RUST-DEPENDENCY-LICENSES). ## License -MIT. +Basilisk source code is MIT licensed. Binary distributions also contain third-party components under the licenses shipped beside each artifact. + +Built by [NIMBLESITE PTY LTD](https://www.nimblesite.co). diff --git a/basilisk-zed/README.zh.md b/basilisk-zed/README.zh.md deleted file mode 100644 index f2852dcb6..000000000 --- a/basilisk-zed/README.zh.md +++ /dev/null @@ -1,70 +0,0 @@ -

English · 简体中文

- -> 📝 本文档由机器翻译生成,欢迎母语者校对改进。 - -# basilisk-zed - -Basilisk 的 Zed 编辑器扩展 —— 基于 WASM 的 Python 类型检查与语言服务器集成。 - -Basilisk 是用 Rust 打造的开源 Python 类型检查器与语言服务器:诊断、自动补全、重构、调试与性能分析,严格程度按规则配置。 - -

- Zed 编辑器中的 Basilisk —— 行内 Python 类型检查与诊断 -

- -> ## ⚠️ 请勿在流水线中使用 Basilisk 的类型检查器 -> -> **类型检查器中仍然存在没有做真正类型检查的代码,它目前还不值得信任。** 有些规则依据的是代码的**写法**而不是含义,因此两个方向上都可能出错 —— 既可能对正确的代码报出虚假错误,也可能对真实的缺陷保持沉默。请不要用它作为 CI 的门禁,也不要把一次干净的运行结果当作代码库是干净的。此前的一致性宣称与基准测试数字均已撤回,并主动请求[从官方结果中移除](https://github.com/python/typing/blob/main/conformance/results/results.html)。 -> -> **这是一个错误、一次验证上的失职。** 我们仅凭一次全绿的运行就发布了结果,却从未检查过我们的规则能否经受住保持语义的改写。Basilisk 作者已发表[个人说明与致歉](https://www.christianfindlay.com/blog/basilisk-conformance-apology)。 -> -> **我们正在逐条审计规则,并删除那些站不住脚的规则** —— 不是重写,也不是打补丁,而是删除,并留下一个失败的测试,让缺口保持可见。如果一条规则无法以直截了当的方式做到可靠,我们会转而依赖另一个成熟的类型检查器,而不是端出我们自己那份不可靠的实现。 -> -> **Basilisk 远不只是一个类型检查器。** 语言服务器、重构、格式化、调试与性能分析都不建立在正在接受审计的规则之上 —— 审计期间,这些正是我们着力打磨的部分,并移除任何可能给出误导性结果的东西。我们这样做,是为了重建信任,把 Basilisk 变回一个你可以信赖的工具。[阅读更正](https://www.basilisk-python.dev/zh/docs/conformance/)。 - -## 安装 - -命令面板(`Cmd+Shift+P` / `Ctrl+Shift+P`)→ **zed: install dev extension** → 选择本目录(如果没有 monorepo,请先克隆 [`Nimblesite/basilisk-zed`](https://github.com/Nimblesite/basilisk-zed))。Zed 会自行把扩展编译为 WASM —— 你无需预先构建或复制 `.wasm` 文件。 - -**你无需单独安装 Basilisk 二进制文件。** 首次激活时,扩展会从 [GitHub Release](https://github.com/Nimblesite/Basilisk/releases) 下载与你的平台匹配的二进制文件,缓存在 Zed 的扩展目录中,并一直复用到出现更新的发行版为止。仅在开发或指向系统安装时才需要覆盖它:在 `settings.json` 中设置 `lsp.basilisk.binary.path`,或设置 `BASILISK_PATH` 环境变量。 - -> 该扩展尚未收录进 [Zed 扩展注册表](https://github.com/zed-industries/extensions);在收录完成之前,上述开发扩展方式就是安装路径。 - -完整的安装说明、设置项、调试与斜杠命令参考:[basilisk-python.dev/docs/install-zed](https://www.basilisk-python.dev/docs/install-zed/)。 - -## 在 Basilisk 中的角色 - -这是 **Zed 编辑器集成**。它是一个编译为 WASM 的原生 Zed 扩展,将 Basilisk 语言服务器连接到 Zed,提供实时诊断、悬停提示、跳转到定义、代码操作,以及通过 DAP 实现的调试。 - -## 核心概念 - -- **WASM 扩展** —— 编译为面向 `wasm32-wasip2` 的 `cdylib` crate,由 Zed 原生加载。 -- **`zed_extension_api`** —— 使用 Zed 官方扩展 API 管理语言服务器生命周期。 -- **`basilisk-common`** —— 与 Basilisk 工作区的其余部分共享诊断代码和常量(同样兼容 WASM)。 -- **不改动内置 Python** —— 按名称绑定到 Zed 自带的 Python 语言。扩展不附带 `languages/` 目录,也不附带语法,因此 Zed 不会从源码编译任何东西,你的语法高亮、括号匹配、缩进规则和可运行项都保持 Zed 出厂时的样子。 -- **DAP 调试** —— 支持 Debug Adapter Protocol,实现集成的 Python 调试。 - -## 构建 - -在 monorepo 检出中,构建扩展并配置本地开发循环: - -```sh -make package-zed -``` - -独立仓库(仅本仓库)中,构建命令与发布流水线用于放行发布的那一条完全相同: - -```sh -cargo build --release --target wasm32-wasip2 -``` - -## 依赖 - -| Crate | 用途 | -|-------|---------| -| `zed_extension_api` | Zed 扩展 API | -| `basilisk-common` | 共享的常量和类型 | - -## 许可证 - -MIT。 diff --git a/basilisk.nvim/README.md b/basilisk.nvim/README.md index a43155b43..592d51fa6 100644 --- a/basilisk.nvim/README.md +++ b/basilisk.nvim/README.md @@ -1,146 +1,38 @@ -

English · 简体中文

- -# basilisk.nvim - -First-class Neovim plugin for Basilisk — zero-config Python type checking, debugging, profiling, and test exploration. - -Basilisk is an open-source Python type checker and language server built in Rust: diagnostics, autocomplete, refactoring, debugging, and profiling, with strictness configured per rule. - -

- Basilisk in action — type checking, diagnostics, and refactoring in the editor -

+ +# Basilisk is unlisted -> ## ⚠️ Do not use Basilisk's type checker in your pipeline -> -> **The type checker still contains code that isn't doing real type checking, and it is not yet trustworthy.** Some rules decide from the way code is *spelled* rather than what it means, so they can be wrong in both directions — a false error on correct code, or silence where there is a genuine bug. Don't gate CI on it, and don't read a clean run as a clean codebase. Our former conformance claim and our benchmark figures are withdrawn, and Basilisk was [removed from the official results](https://github.com/python/typing/blob/main/conformance/results/results.html) at our request. -> -> **This was a mistake and a failure to verify.** We published on a green run without ever checking whether our rules survived a semantics-preserving change. Basilisk's author has published a [personal account and apology](https://www.christianfindlay.com/blog/basilisk-conformance-apology). -> -> **We are auditing every rule and deleting the ones that don't hold up** — not rewriting them, not patching them, with a failing test left behind so the gap stays visible. Where a rule can't be made reliable in a straightforward way, we will depend on a different, established type checker rather than ship our own unreliable version of it. -> -> **Basilisk is much more than a type checker.** The language server, refactoring, formatting, debugging, and profiling don't rest on the rules under audit — those are what we are sharpening while it runs, removing anything that could hand you a misleading result. We are doing this to restore trust and turn Basilisk back into a tool you can believe. [Read the correction](https://www.basilisk-python.dev/docs/conformance/). +> **You are reading the `basilisk.nvim` plugin listing.** -## Role in Basilisk +**Basilisk's type checker was producing incorrect results.** Rules decided from the way code was *spelled* rather than what it meant, so they could be wrong in both directions — a false error on correct code, or silence on a real bug. -This is the **Neovim editor integration**. It connects Neovim's built-in LSP client to the Basilisk language server, providing the same feature set as the VS Code extension: real-time diagnostics, hover, go-to-definition, code actions, inlay hints, integrated debugging, and profiling. +**We asked for Basilisk to be removed from the `python/typing` conformance results, and it has been removed.** The score it held was not evidence of anything. -## Features +**We cannot tell you how much of the checker this affects.** The code responsible is not isolated to a known set of rules. We will not estimate. That uncertainty is the reason for everything below. -- **Zero-config setup** — detects the `basilisk` binary and connects automatically -- **Real-time diagnostics** — errors appear inline as you type -- **Go-to-definition, hover, find references** — full LSP navigation -- **Code actions & refactoring** — extract, rename, move, inline -- **Inlay hints** — parameter names and inferred types -- **Integrated debugging** — nvim-dap compatible, F5 to debug -- **Test explorer** — discover and run pytest tests from the editor -- **Python profiling** — py-spy heatmaps directly in the editor -- **Memory leak tracking** — detect leaks during development -- **uv integration** — `uv sync` and `uv add` commands -- **Status line** — LSP status in your status line -- **Health checks** — `:checkhealth basilisk` for diagnostics +**A code-quality tool that does not produce correct results is worse than useless.** Basilisk is being delisted everywhere it was published — VS Code Marketplace, Open VSX, PyPI, Homebrew, Scoop — and the type checker is being made inert. Remove it from your pipeline; it checks nothing, and it fails rather than reporting a clean run. -## Requirements +**We are not fixing Basilisk's type checker code. We are rebuilding Basilisk.** What comes next is a new product, built from the ground up, shipping only what can be shown to be trustworthy. That most likely will not include type checking. Nothing is relisted until it has been rebuilt from components we can vouch for. If type checking ever returns, it will come from established third-party engines, or code audited by a third party. -- Neovim 0.11+ (the plugin uses the built-in `vim.lsp.config` / `vim.lsp.enable` API) -- `curl` (used once, to download the `basilisk` binary — see below) +Basilisk's author has published a full public account: [an apology](https://www.christianfindlay.com/blog/basilisk-conformance-apology). -## Install +## What to do now -Two parts get installed: the **plugin** (this repo, via your plugin manager) and the **`basilisk` binary** (downloaded automatically — you normally never install it yourself). +**Remove Basilisk from your pipeline, your pre-commit hooks, and your editor.** Uninstall the CLI and the extension. -### 1. Install the plugin +The type checker is being made inert: it checks nothing, and it exits non-zero so a build that still calls it fails loudly rather than reporting a clean run. Do not treat that failure as a finding about your code. -
-lazy.nvim +**Treat every result Basilisk gave you as unverified.** A clean run was never evidence that your code was clean, and an error it reported may never have been real. -```lua -{ - "Nimblesite/basilisk.nvim", - ft = "python", - dependencies = { "mfussenegger/nvim-dap" }, -- optional, for debugging - opts = {}, -} -``` -
+Every distribution channel is being delisted. Nothing will be relisted until it has been rebuilt from components we can vouch for. -
-packer.nvim +## Acknowledgments -```lua -use { - "Nimblesite/basilisk.nvim", - ft = "python", - config = function() - require("basilisk").setup({}) - end, -} -``` -
- -
-vim-plug - -```vim -Plug 'Nimblesite/basilisk.nvim' -``` - -then somewhere after `plug#end()`: - -```lua -lua require("basilisk").setup({}) -``` -
- -
-vim.pack (built-in, Neovim 0.12+) - -```lua -vim.pack.add({ - { src = "https://github.com/Nimblesite/basilisk.nvim", - version = vim.version.range("*") }, -- latest stable tag; or pin "v0.33.0" -}) -require("basilisk").setup({}) -``` -
- -### 2. The binary installs itself - -Open any Python file. If no `basilisk` binary is found, the plugin downloads the latest [GitHub release](https://github.com/Nimblesite/Basilisk/releases) for your platform into Neovim's data directory and starts the LSP — no PATH setup, no manual step. You can also trigger it explicitly with `:BasiliskInstall`. - -Prefer a package manager? The plugin picks up existing installs automatically: - -```sh -# macOS (Apple Silicon) / Linux -brew tap Nimblesite/tap && brew install basilisk - -# Windows -scoop bucket add nimblesite https://github.com/Nimblesite/scoop-bucket -scoop install basilisk - -# anywhere with a Python toolchain -uv tool install basilisk-python - -# anywhere with a Rust toolchain (builds from source) -cargo install --git https://github.com/Nimblesite/Basilisk basilisk-cli -``` - -That's it — diagnostics, hover, completions, formatting, debugging, tests, and profiling all run through this one plugin. Verify with `:checkhealth basilisk`. - -## Updating - -- **Plugin**: update like any other plugin — `:Lazy update` (lazy.nvim), `:PackerSync` (packer), `:PlugUpdate` (vim-plug). -- **Binary**: when a new release is out, the plugin notifies you on startup. Run **`:BasiliskUpdate`** — it confirms, downloads the new version, and restarts the LSP in place. Installs owned by a package manager are never overwritten; the notice tells you to run `brew upgrade basilisk` / `scoop update basilisk` / `cargo install --git https://github.com/Nimblesite/Basilisk basilisk-cli` instead. - -## Configuration - -Zero-config works out of the box: - -```lua -require("basilisk").setup() -``` - -All options (analysis mode, inlay hints, formatter, debugger, test explorer, uv, keymaps…) are documented in [doc/basilisk.txt](doc/basilisk.txt) — `:h basilisk-configuration`. +Basilisk is built on [Ruff](https://github.com/astral-sh/ruff) by [Astral](https://astral.sh/), whose parser, AST, and formatter crates it embeds (MIT), and on standard-library type stubs from [typeshed](https://github.com/python/typeshed) (Apache-2.0, with MIT-licensed parts). Neither project is responsible for how Basilisk used them. Full component list and required notices: [NOTICES](https://github.com/Nimblesite/Basilisk/blob/main/NOTICES) and [RUST-DEPENDENCY-LICENSES](https://github.com/Nimblesite/Basilisk/blob/main/RUST-DEPENDENCY-LICENSES). ## License -MIT. +Basilisk source code is MIT licensed. Binary distributions also contain third-party components under the licenses shipped beside each artifact. + +Built by [NIMBLESITE PTY LTD](https://www.nimblesite.co). diff --git a/basilisk.nvim/README.zh.md b/basilisk.nvim/README.zh.md deleted file mode 100644 index 05920d9e5..000000000 --- a/basilisk.nvim/README.zh.md +++ /dev/null @@ -1,148 +0,0 @@ -

English · 简体中文

- -> 📝 本文档由机器翻译生成,欢迎母语者校对改进。 - -# basilisk.nvim - -为 Basilisk 打造的一流 Neovim 插件 —— 零配置的 Python 类型检查、调试、性能分析与测试探索。 - -Basilisk 是用 Rust 打造的开源 Python 类型检查器与语言服务器:诊断、自动补全、重构、调试与性能分析,严格程度按规则配置。 - -

- Basilisk in action — type checking, diagnostics, and refactoring in the editor -

- -> ## ⚠️ 请勿在流水线中使用 Basilisk 的类型检查器 -> -> **类型检查器中仍然存在没有做真正类型检查的代码,它目前还不值得信任。** 有些规则依据的是代码的**写法**而不是含义,因此两个方向上都可能出错 —— 既可能对正确的代码报出虚假错误,也可能对真实的缺陷保持沉默。请不要用它作为 CI 的门禁,也不要把一次干净的运行结果当作代码库是干净的。此前的一致性宣称与基准测试数字均已撤回,并主动请求[从官方结果中移除](https://github.com/python/typing/blob/main/conformance/results/results.html)。 -> -> **这是一个错误、一次验证上的失职。** 我们仅凭一次全绿的运行就发布了结果,却从未检查过我们的规则能否经受住保持语义的改写。Basilisk 作者已发表[个人说明与致歉](https://www.christianfindlay.com/blog/basilisk-conformance-apology)。 -> -> **我们正在逐条审计规则,并删除那些站不住脚的规则** —— 不是重写,也不是打补丁,而是删除,并留下一个失败的测试,让缺口保持可见。如果一条规则无法以直截了当的方式做到可靠,我们会转而依赖另一个成熟的类型检查器,而不是端出我们自己那份不可靠的实现。 -> -> **Basilisk 远不只是一个类型检查器。** 语言服务器、重构、格式化、调试与性能分析都不建立在正在接受审计的规则之上 —— 审计期间,这些正是我们着力打磨的部分,并移除任何可能给出误导性结果的东西。我们这样做,是为了重建信任,把 Basilisk 变回一个你可以信赖的工具。[阅读更正](https://www.basilisk-python.dev/zh/docs/conformance/)。 - -## 在 Basilisk 中的角色 - -这是 **Neovim 编辑器集成**。它将 Neovim 内置的 LSP 客户端连接到 Basilisk 语言服务器,提供与 VS Code 扩展相同的功能集:实时诊断、悬停信息、跳转到定义、代码操作、内嵌提示(inlay hints)、集成调试以及性能分析。 - -## 功能特性 - -- **零配置安装** —— 自动检测 `basilisk` 二进制文件并建立连接 -- **实时诊断** —— 错误在你输入时即时内联显示 -- **跳转到定义、悬停信息、查找引用** —— 完整的 LSP 导航 -- **代码操作与重构** —— 提取、重命名、移动、内联 -- **内嵌提示(inlay hints)** —— 参数名称与推断类型 -- **集成调试** —— 兼容 nvim-dap,按 F5 即可调试 -- **测试浏览器** —— 在编辑器中发现并运行 pytest 测试 -- **Python 性能分析** —— 直接在编辑器中查看 py-spy 热力图 -- **内存泄漏追踪** —— 在开发过程中检测泄漏 -- **uv 集成** —— `uv sync` 与 `uv add` 命令 -- **状态栏** —— 在状态栏中显示 LSP 状态 -- **健康检查** —— `:checkhealth basilisk` 进行诊断 - -## 要求 - -- Neovim 0.11+(插件使用内置的 `vim.lsp.config` / `vim.lsp.enable` API) -- `curl`(仅用于一次性下载 `basilisk` 二进制文件,见下文) - -## 安装 - -需要安装两部分:**插件**(本仓库,通过你的插件管理器安装)和 **`basilisk` 二进制文件**(自动下载 —— 通常无需手动安装)。 - -### 1. 安装插件 - -
-lazy.nvim - -```lua -{ - "Nimblesite/basilisk.nvim", - ft = "python", - dependencies = { "mfussenegger/nvim-dap" }, -- 可选,用于调试 - opts = {}, -} -``` -
- -
-packer.nvim - -```lua -use { - "Nimblesite/basilisk.nvim", - ft = "python", - config = function() - require("basilisk").setup({}) - end, -} -``` -
- -
-vim-plug - -```vim -Plug 'Nimblesite/basilisk.nvim' -``` - -然后在 `plug#end()` 之后: - -```lua -lua require("basilisk").setup({}) -``` -
- -
-vim.pack(内置,Neovim 0.12+) - -```lua -vim.pack.add({ - { src = "https://github.com/Nimblesite/basilisk.nvim", - version = vim.version.range("*") }, -- 最新稳定标签;或固定 "v0.33.0" -}) -require("basilisk").setup({}) -``` -
- -### 2. 二进制文件自动安装 - -打开任意 Python 文件。若未找到 `basilisk` 二进制文件,插件会自动从 [GitHub Release](https://github.com/Nimblesite/Basilisk/releases) 下载适合你平台的最新版本到 Neovim 数据目录并启动 LSP —— 无需配置 PATH,无需手动操作。也可以用 `:BasiliskInstall` 显式触发。 - -偏好包管理器?插件会自动识别已有安装: - -```sh -# macOS(Apple Silicon)/ Linux -brew tap Nimblesite/tap && brew install basilisk - -# Windows -scoop bucket add nimblesite https://github.com/Nimblesite/scoop-bucket -scoop install basilisk - -# 任何有 Python 工具链的环境 -uv tool install basilisk-python - -# 任何有 Rust 工具链的环境(从源码构建) -cargo install --git https://github.com/Nimblesite/Basilisk basilisk-cli -``` - -就这样 —— 诊断、悬停、补全、格式化、调试、测试与性能分析全部通过这一个插件运行。用 `:checkhealth basilisk` 验证。 - -## 更新 - -- **插件**:像其他插件一样更新 —— `:Lazy update`(lazy.nvim)、`:PackerSync`(packer)、`:PlugUpdate`(vim-plug)。 -- **二进制文件**:有新版本时插件会在启动时通知你。运行 **`:BasiliskUpdate`** —— 确认后下载新版本并就地重启 LSP。由包管理器管理的安装不会被覆盖;通知会提示你改用 `brew upgrade basilisk` / `scoop update basilisk` / `cargo install --git https://github.com/Nimblesite/Basilisk basilisk-cli`。 - -## 配置 - -零配置即可开箱即用: - -```lua -require("basilisk").setup() -``` - -所有选项(分析模式、内嵌提示、格式化器、调试器、测试浏览器、uv、快捷键等)见 [doc/basilisk.txt](doc/basilisk.txt) —— `:h basilisk-configuration`。 - -## 许可证 - -MIT。 diff --git a/conformance/test_release_attribution.py b/conformance/test_release_attribution.py index 2b3d4849b..c01761c27 100644 --- a/conformance/test_release_attribution.py +++ b/conformance/test_release_attribution.py @@ -268,12 +268,6 @@ def test_readmes_describe_typeshed_composite_license(self) -> None: "Apache-2.0, with MIT-licensed parts", (REPO_ROOT / relative).read_text(), ) - for relative in ("README.zh.md", "vscode-extension/README.zh.md"): - with self.subTest(readme=relative): - self.assertIn( - "Apache-2.0,部分内容采用 MIT 许可证", - (REPO_ROOT / relative).read_text(), - ) def test_package_metadata_names_every_license_in_shipped_binaries(self) -> None: # PEP 639 License-Expression covers the containing distribution, so the diff --git a/crates/basilisk-cli/src/adopt.rs b/crates/basilisk-cli/src/adopt.rs deleted file mode 100644 index 50f5dc376..000000000 --- a/crates/basilisk-cli/src/adopt.rs +++ /dev/null @@ -1,627 +0,0 @@ -//! Implements [AUTOFIX-ADOPTION]. See docs/specs/LSP-MASS-AUTOFIX-SPEC.md#AUTOFIX-ADOPTION -//! `basilisk adopt`, `basilisk unadopt`, and `basilisk adopt --status`. -//! -//! Adoption records current error debt as **ordinary warning-severity rule -//! entries** in the config file of the nearest folder governing each affected -//! file — plain `code -> severity` entries in the one configuration model -//! ([CHKARCH-CONFIG-MODEL]). There are no exact-file overrides, ownership -//! markers, or sidecar state: the adoption state IS the set of -//! warning-severity `[tool.basilisk.rules]` entries, `unadopt` deletes them, -//! and re-running `adopt` recomputes them so rules that no longer fire revert -//! without manual bookkeeping ([AUTOFIX-ADOPTION-FLOW]). - -use std::collections::{BTreeMap, BTreeSet}; -use std::path::{Path, PathBuf}; - -use basilisk_config::{RuleConfigUpdate, RuleSeverity}; -use tracing::{error, info}; - -use crate::pipeline::{ - collect_and_check, find_project_root, first_path_dir, parent_dir_of, pluralise, - DiagnosticScope, PipelineError, -}; - -/// Run the adopt subcommand. -/// -/// Exit codes ([CHKARCH-CLI-EXITCODES]): -/// - `0` — adoption recorded successfully -/// - `2` — invalid configuration -/// - `3` — internal error -pub(crate) fn run_adopt(paths: &[String]) -> u8 { - match adopt_folders(paths) { - Ok(summary) => { - println!( - "Adopted {} folder config{} with {} demoted rule code{}.", - summary.folders_updated, - pluralise(summary.folders_updated), - summary.demoted_count, - pluralise(summary.demoted_count), - ); - 0 - } - Err(err) => report_failure(&err, "adopt failed"), - } -} - -/// Run the unadopt subcommand. -/// -/// Exit codes: `0` on success, `2` on invalid configuration, `3` on -/// internal error. -pub(crate) fn run_unadopt(paths: &[String]) -> u8 { - match unadopt_folders(paths) { - Ok(removed) => { - println!( - "Un-adopted {} rule entr{}.", - removed, - if removed == 1 { "y" } else { "ies" }, - ); - 0 - } - Err(err) => report_failure(&err, "unadopt failed"), - } -} - -/// Run the adopt --status subcommand. -/// -/// Reports, per governing folder config, the warning-severity rule entries -/// that constitute the adoption state ([AUTOFIX-ADOPTION]). -/// -/// Exit codes: `0` on success, `3` on internal error. -pub(crate) fn run_adopt_status(paths: &[String]) -> u8 { - let roots = match governing_roots(paths) { - Ok(roots) => roots, - Err(err) => return report_failure(&err, "adopt --status failed"), - }; - let mut adopted_any = false; - for root in roots { - let entries = match adopted_entries(&root) { - Ok(entries) => entries, - Err(err) => return report_failure(&err, "adopt --status failed"), - }; - if entries.is_empty() { - continue; - } - adopted_any = true; - println!( - "{} ({} demoted code{}):", - root.display(), - entries.len(), - pluralise(entries.len()), - ); - for code in entries { - println!(" {code}"); - } - } - if !adopted_any { - println!("No folders are currently adopted."); - } - 0 -} - -/// Log a pipeline failure and map it to its exit code. -fn report_failure(err: &PipelineError, context: &'static str) -> u8 { - match err { - PipelineError::Config(message) => { - error!(%message, "{context}: configuration error"); - 2 - } - PipelineError::NoSource(message) => { - error!(%message, "{context}"); - 3 - } - PipelineError::Internal(message) => { - error!(%message, "{context}"); - 3 - } - } -} - -/// Summary of an adopt run. -struct AdoptSummary { - /// Number of folder configs that were rewritten. - folders_updated: usize, - /// Total number of rule codes demoted across all folders. - demoted_count: usize, -} - -/// Current debt for one governing folder config. -#[derive(Default)] -struct FolderDebt { - /// Codes firing at `error`/`safety-violation` — the debt to demote. - error_codes: BTreeSet, - /// Codes firing at any severity — existing adoption entries for codes - /// absent here have graduated and are removed on recompute. - firing_codes: BTreeSet, -} - -/// Adopt: check both command scopes at their resolved severities -/// ([CHKARCH-COMMANDS]) and rewrite each governing folder config's adoption -/// entries to exactly the current debt ([AUTOFIX-ADOPTION-FLOW]). -fn adopt_folders(paths: &[String]) -> Result { - let debt_by_root = collect_folder_debt(paths)?; - let mut folders_updated: usize = 0; - let mut demoted_count: usize = 0; - - for (root, debt) in debt_by_root { - let existing = adopted_entries(&root)?; - let mut rules: BTreeMap> = debt - .error_codes - .iter() - .map(|code| (code.clone(), Some(RuleSeverity::Warning))) - .collect(); - // Recompute: an adoption entry whose rule no longer fires anywhere in - // the scanned scope has graduated — delete it ([AUTOFIX-ADOPTION-FLOW]). - for code in existing { - if !debt.firing_codes.contains(&code) { - let _ = rules.entry(code).or_insert(None); - } - } - if rules.is_empty() { - continue; - } - write_rule_entries(&root, rules.clone())?; - folders_updated += 1; - demoted_count += debt.error_codes.len(); - info!( - root = %root.display(), - demoted = debt.error_codes.len(), - "adopted folder config" - ); - } - - Ok(AdoptSummary { - folders_updated, - demoted_count, - }) -} - -/// Unadopt: delete every warning-severity rule entry — the adoption state — -/// from each governing folder config ([AUTOFIX-ADOPTION]). -fn unadopt_folders(paths: &[String]) -> Result { - let mut removed: usize = 0; - for root in governing_roots(paths)? { - let entries = adopted_entries(&root)?; - if entries.is_empty() { - continue; - } - removed += entries.len(); - let rules: BTreeMap> = - entries.into_iter().map(|code| (code, None)).collect(); - write_rule_entries(&root, rules)?; - info!(root = %root.display(), "un-adopted folder config"); - } - Ok(removed) -} - -/// Run the shared pipeline over both scopes and group the result per -/// governing folder config. Every scanned file registers its root even when -/// clean, so recompute can graduate stale entries. -fn collect_folder_debt(paths: &[String]) -> Result, PipelineError> { - // Adoption rewrites the very configuration a cache entry is fingerprinted - // against, so it always runs cold — the project's `cache` key does not - // apply here ([CHKCACHE-CONFIG]). - let no_cache = crate::cache_check::CacheOptions { - enabled: crate::cache_check::CacheOverride::ForceOff, - dir: None, - stats: false, - }; - let mut stats = crate::cache_check::CacheStats::default(); - let outcome = collect_and_check(paths, &no_cache, &mut stats, DiagnosticScope::Union)?; - for failure in &outcome.failures { - tracing::warn!(path = %failure.path, error = %failure.message, "error checking file"); - } - - let mut debt: BTreeMap = BTreeMap::new(); - for source in &outcome.sources { - let _ = debt.entry(governing_root(&source.path)).or_default(); - } - for diagnostic in &outcome.diagnostics { - let entry = debt.entry(governing_root(&diagnostic.path)).or_default(); - let code = diagnostic.code.code.to_owned(); - if matches!( - diagnostic.severity, - basilisk_checker::Severity::Error | basilisk_checker::Severity::SafetyViolation - ) { - let _ = entry.error_codes.insert(code.clone()); - } - let _ = entry.firing_codes.insert(code); - } - Ok(debt) -} - -/// The unique governing folder configs for the Python files under `paths`. -fn governing_roots(paths: &[String]) -> Result, PipelineError> { - let config_root = first_path_dir(paths); - let config = basilisk_config::load_basilisk_config(&config_root); - let excluded = crate::pipeline::excluded_dirs_and_log(&config, &config_root); - let python_files = - crate::pipeline::collect_python_files(paths, &excluded).map_err(PipelineError::Internal)?; - Ok(python_files - .iter() - .map(|file| governing_root(file)) - .collect()) -} - -/// The folder whose config file governs `file`: the nearest ancestor holding -/// a `[tool.basilisk]` table, else the project root (whose `pyproject.toml` -/// becomes the creation target). [CHKARCH-CONFIG-DISCOVERY] -fn governing_root(file: &str) -> PathBuf { - let parent = parent_dir_of(file); - basilisk_config::discover_config_dir(&parent).unwrap_or_else(|| find_project_root(&parent)) -} - -/// The adoption state of one folder config: its warning-severity -/// `[tool.basilisk.rules]` entries ([AUTOFIX-ADOPTION]). -fn adopted_entries(root: &Path) -> Result, PipelineError> { - let document = discover_document(root)?; - Ok(document - .config - .nearest_tables() - .map(|tables| { - tables - .rules - .iter() - .filter(|(_, severity)| **severity == RuleSeverity::Warning) - .map(|(code, _)| code.clone()) - .collect() - }) - .unwrap_or_default()) -} - -/// Apply plain rule-entry updates to the folder config at `root` through the -/// shared configuration mutation service ([AUTOFIX-ADOPTION-FLOW]). -fn write_rule_entries( - root: &Path, - rules: BTreeMap>, -) -> Result<(), PipelineError> { - let document = discover_document(root)?; - let update = RuleConfigUpdate { - rules, - rule_tags: BTreeMap::new(), - }; - let patch = basilisk_config::build_rule_patch(&document, &update) - .map_err(|err| PipelineError::Config(err.to_string()))?; - basilisk_config::apply_config_patch(&patch) - .map_err(|err| PipelineError::Internal(err.to_string())) -} - -fn discover_document(root: &Path) -> Result { - basilisk_config::discover_config_document(root) - .map_err(|err| PipelineError::Config(err.to_string())) -} - -#[cfg(test)] -#[expect( - clippy::unwrap_used, - reason = "test-only code: unwrap acceptable in unit tests" -)] -mod tests { - use super::*; - use std::fs; - - /// Python code with a missing parameter annotation (triggers BSK-0001) - /// and a missing return type annotation (triggers BSK-0002). - // `x` has no default to infer from (BSK-0001) and `return x` is not - // inferable (BSK-0002) — a `pass` body would infer `-> None` and only - // fire BSK-0001 ([TYPEINF-FUNC-RETURN]). - const BAD_PYTHON: &str = "def foo(x):\n return x\n"; - - /// Fully typed Python code that should produce zero errors. - const CLEAN_PYTHON: &str = "def greet(name: str) -> str:\n return name\n"; - - /// Python code with a check-scope (pep) error: wrong return type. - const PEP_ERROR_PYTHON: &str = "def bad() -> int:\n return \"x\"\n"; - - /// Create a fresh temporary project directory (removing any leftover from - /// a prior run) that ships a `pyproject.toml` opting into the annotation - /// house rules. `adopt` records the diagnostics a project has enabled, - /// and those analyze-scope rules are off by default — so the test project - /// turns them on exactly as a real adopter would ([CHKARCH-COMMANDS]). - fn temp_dir(name: &str) -> PathBuf { - // Per-process dir name (same pattern as `stage_project` in - // cli_binary_tests): a stray watcher or leftover harness process from - // a previous run must never touch this run's fixture files. - let dir = - std::env::temp_dir().join(format!("bsk_adopt_test_{name}.{}", std::process::id())); - let _ = fs::remove_dir_all(&dir); - fs::create_dir_all(&dir).unwrap(); - fs::write( - dir.join("pyproject.toml"), - "[tool.basilisk.rules]\n\"BSK-0001\" = \"error\"\n\"BSK-0002\" = \"error\"\n", - ) - .unwrap(); - dir - } - - /// Write a `.py` file inside `dir` and return its absolute path as a `String`. - fn write_py(dir: &Path, filename: &str, content: &str) -> String { - let path = dir.join(filename); - fs::write(&path, content).unwrap(); - path.to_string_lossy().into_owned() - } - - /// The warning-severity rule entries in `dir`'s config — the adoption - /// state ([AUTOFIX-ADOPTION]). - fn adoption(dir: &Path) -> BTreeSet { - adopted_entries(dir).unwrap() - } - - /// The full `[tool.basilisk.rules]` table in `dir`'s config. - fn rule_entries(dir: &Path) -> BTreeMap { - let document = basilisk_config::discover_config_document(dir).unwrap(); - document - .config - .nearest_tables() - .map(|tables| tables.rules.clone().into_iter().collect()) - .unwrap_or_default() - } - - // ── run_adopt ([AUTOFIX-ADOPTION]) ─────────────────────────────────── - - /// [AUTOFIX-ADOPTION]: adopting a folder with analyze-scope error debt - /// demotes the firing codes to plain warning entries in the governing - /// folder config — no exact-file overrides, no markers. - #[test] - fn run_adopt_bad_code_demotes_codes_in_folder_config() { - let dir = temp_dir("adopt_bad"); - let path = write_py(&dir, "bad.py", BAD_PYTHON); - - let exit = run_adopt(&[path]); - assert_eq!(exit, 0, "adopt should succeed with exit code 0"); - - let entries = rule_entries(&dir); - assert_eq!( - entries.get("BSK-0001"), - Some(&RuleSeverity::Warning), - "BSK-0001 must be demoted to a folder-level warning entry, got: {entries:?}" - ); - assert_eq!( - entries.get("BSK-0002"), - Some(&RuleSeverity::Warning), - "BSK-0002 must be demoted to a folder-level warning entry, got: {entries:?}" - ); - } - - /// [AUTOFIX-ADOPTION-FLOW]: pep debt is demoted to `warning` (never below - /// info) as an ordinary folder entry, so `check` reports it as a warning - /// afterwards. - #[test] - fn run_adopt_records_pep_debt_as_warning_entry() { - // Per-process dir name — see `temp_dir` for the rationale. - let dir = - std::env::temp_dir().join(format!("bsk_adopt_test_pep_debt.{}", std::process::id())); - let _ = fs::remove_dir_all(&dir); - fs::create_dir_all(&dir).unwrap(); - fs::write( - dir.join("pyproject.toml"), - "[project]\nname = \"x\"\nversion = \"0.0.0\"\n", - ) - .unwrap(); - let path = write_py(&dir, "bad.py", PEP_ERROR_PYTHON); - - let exit = run_adopt(&[path]); - assert_eq!(exit, 0, "adopt should succeed"); - - let entries = rule_entries(&dir); - let demoted_pep: Vec<_> = entries - .iter() - .filter(|(code, severity)| { - basilisk_checker::is_pep_rule(code) && **severity == RuleSeverity::Warning - }) - .collect(); - assert!( - !demoted_pep.is_empty(), - "the firing pep code must be demoted to warning in the folder config, got: {entries:?}" - ); - } - - #[test] - fn run_adopt_clean_code_produces_no_adoptions() { - let dir = temp_dir("adopt_clean"); - let path = write_py(&dir, "clean.py", CLEAN_PYTHON); - - let exit = run_adopt(&[path]); - assert_eq!(exit, 0); - - assert!( - adoption(&dir).is_empty(), - "clean code should produce no adoption entries" - ); - } - - #[test] - fn run_adopt_nonexistent_path_returns_3() { - let exit = run_adopt(&["/no/such/path/ever.py".to_owned()]); - assert_eq!(exit, 3, "nonexistent path should return exit code 3"); - } - - /// [AUTOFIX-ADOPTION-RULES]: a folder entry is a plain override — two bad - /// files in one folder produce one set of folder entries, not per-file - /// state. - #[test] - fn run_adopt_directory_traversal_writes_one_folder_entry_set() { - let dir = temp_dir("adopt_multi"); - let _ = write_py(&dir, "a.py", BAD_PYTHON); - let _ = write_py(&dir, "b.py", BAD_PYTHON); - - let exit = run_adopt(&[dir.to_string_lossy().into_owned()]); - assert_eq!(exit, 0); - - let adopted = adoption(&dir); - assert_eq!( - adopted, - ["BSK-0001", "BSK-0002"] - .into_iter() - .map(str::to_owned) - .collect::>(), - "both files' debt collapses into the one governing folder config" - ); - } - - /// [AUTOFIX-ADOPTION]: debt in differently-governed folders is demoted in - /// each folder's own config file (the old single-store restriction is - /// gone). - #[test] - fn run_adopt_writes_each_governing_folder_config() { - let first = temp_dir("adopt_cross_root_first"); - let second = temp_dir("adopt_cross_root_second"); - let first_path = write_py(&first, "first.py", BAD_PYTHON); - let second_path = write_py(&second, "second.py", BAD_PYTHON); - - assert_eq!(run_adopt(&[first_path, second_path]), 0); - assert!( - adoption(&first).contains("BSK-0001"), - "first root must hold its own adoption entries" - ); - assert!( - adoption(&second).contains("BSK-0001"), - "second root must hold its own adoption entries" - ); - } - - /// [AUTOFIX-ADOPTION-FLOW]: re-running adopt recomputes — entries for - /// rules that no longer fire anywhere in the folder are deleted. - #[test] - fn run_adopt_rerun_graduates_fixed_rules() { - let dir = temp_dir("adopt_rerun"); - let path = write_py(&dir, "bad.py", BAD_PYTHON); - - assert_eq!(run_adopt(std::slice::from_ref(&path)), 0); - assert!( - !adoption(&dir).is_empty(), - "precondition: adoption entries exist" - ); - - // Fix the debt, re-run adopt: the entries must graduate away. - let _ = write_py(&dir, "bad.py", CLEAN_PYTHON); - assert_eq!(run_adopt(&[path]), 0); - assert!( - adoption(&dir).is_empty(), - "re-running adopt must remove entries whose rules no longer fire, got: {:?}", - adoption(&dir) - ); - } - - // ── run_unadopt ([AUTOFIX-ADOPTION]) ───────────────────────────────── - - /// [AUTOFIX-ADOPTION-FLOW]: unadopt deletes the folder's warning entries, - /// restoring the ancestor severity. - #[test] - fn run_unadopt_removes_adoption_entries() { - let dir = temp_dir("unadopt_remove"); - let path = write_py(&dir, "bad.py", BAD_PYTHON); - - // First adopt. - let exit = run_adopt(std::slice::from_ref(&path)); - assert_eq!(exit, 0); - assert!( - !adoption(&dir).is_empty(), - "precondition: adoption must exist" - ); - - // Then unadopt. - let exit = run_unadopt(&[path]); - assert_eq!(exit, 0); - - assert!( - adoption(&dir).is_empty(), - "active config must have no adoption entries after unadopt" - ); - } - - /// Unadopt leaves non-warning entries (the user's own error opt-ins) - /// untouched — only the adoption state is deleted. [AUTOFIX-ADOPTION] - #[test] - fn run_unadopt_preserves_error_entries() { - let dir = temp_dir("unadopt_preserve"); - let path = write_py(&dir, "bad.py", BAD_PYTHON); - assert_eq!(run_adopt(std::slice::from_ref(&path)), 0); - assert_eq!(run_unadopt(&[path]), 0); - - // BSK-0001/BSK-0002 were rewritten to warning by adopt and removed by - // unadopt; a config with only non-warning entries would keep them. - let entries = rule_entries(&dir); - assert!( - entries - .values() - .all(|severity| *severity != RuleSeverity::Warning), - "no warning entries may remain after unadopt, got: {entries:?}" - ); - } - - #[test] - fn run_unadopt_on_clean_dir_returns_0() { - let dir = temp_dir("unadopt_clean"); - let _ = write_py(&dir, "clean.py", CLEAN_PYTHON); - - let exit = run_unadopt(&[dir.to_string_lossy().into_owned()]); - assert_eq!(exit, 0); - } - - #[test] - fn run_unadopt_nonexistent_path_returns_3() { - let exit = run_unadopt(&["/no/such/path/ever.py".to_owned()]); - assert_eq!(exit, 3); - } - - // ── run_adopt_status ([AUTOFIX-ADOPTION]) ──────────────────────────── - - #[test] - fn run_adopt_status_empty_prints_no_folders() { - let dir = temp_dir("status_empty"); - // Create the directory but no adoptions. - let _ = write_py(&dir, "clean.py", CLEAN_PYTHON); - let exit = run_adopt_status(&[dir.to_string_lossy().into_owned()]); - assert_eq!(exit, 0); - } - - #[test] - fn run_adopt_status_shows_adopted_folders() { - let dir = temp_dir("status_shows"); - let path = write_py(&dir, "bad.py", BAD_PYTHON); - - let exit = run_adopt(&[path]); - assert_eq!(exit, 0); - - let exit = run_adopt_status(&[dir.to_string_lossy().into_owned()]); - assert_eq!(exit, 0); - } - - // ── governing_root ([CHKARCH-CONFIG-DISCOVERY]) ────────────────────── - - #[test] - fn governing_root_file_returns_config_dir() { - let dir = temp_dir("resolve_file"); - let path = write_py(&dir, "foo.py", CLEAN_PYTHON); - - // Discovery preserves the caller's path spelling (no - // canonicalization) — a symlinked temp dir stays as given. - assert_eq!(governing_root(&path), dir); - } - - #[test] - fn governing_root_nested_file_finds_project_config() { - let dir = temp_dir("resolve_nested"); - let src = dir.join("src"); - fs::create_dir_all(&src).unwrap(); - let path = write_py(&src, "nested.py", CLEAN_PYTHON); - assert_eq!(governing_root(&path), dir); - } - - /// A nested folder with its own `[tool.basilisk]` table governs its files - /// — adoption writes there, exactly where `check` discovers. - /// [CHKARCH-CONFIG-DISCOVERY] - #[test] - fn governing_root_prefers_nearest_config_table() { - let dir = temp_dir("resolve_nearest"); - let sub = dir.join("sub"); - fs::create_dir_all(&sub).unwrap(); - fs::write( - sub.join("pyproject.toml"), - "[tool.basilisk.rules]\n\"BSK-0001\" = \"error\"\n", - ) - .unwrap(); - let path = write_py(&sub, "nested.py", CLEAN_PYTHON); - assert_eq!(governing_root(&path), sub); - } -} diff --git a/crates/basilisk-cli/src/cache_check.rs b/crates/basilisk-cli/src/cache_check.rs deleted file mode 100644 index ae4364a9d..000000000 --- a/crates/basilisk-cli/src/cache_check.rs +++ /dev/null @@ -1,421 +0,0 @@ -//! Implements [CHKCACHE-CLI] / [CHKCACHE-FINGERPRINT]. -//! See docs/specs/CHECKER-CACHE-SPEC.md#CHKCACHE-CLI -//! -//! CLI glue for the opt-in result cache: turns the `--cache*` flags into a -//! [`CacheContext`], wraps the per-file cold check with a lookup/store, and -//! tracks hit/miss counts. - -use std::path::{Path, PathBuf}; - -use basilisk_checker::{CachedDiagnostic, Diagnostic}; -use basilisk_common::fs::{content_hash, ReadRecorder}; -use basilisk_config::BasiliskConfig; -use basilisk_db::cache::{CheckCache, Fingerprint}; -use basilisk_lsp::import_resolver::ImportSearchPaths; - -/// What this invocation's flags say about the persistent result cache -/// ([CHKCACHE-CONFIG]). -/// -/// The project states the standing policy in `[tool.basilisk] cache`; a flag -/// is a per-run override of it. `Project` is the flagless case — the config -/// decides, and with no key written the cache stays off exactly as before. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum CacheOverride { - /// No `--cache`/`--no-cache`: `[tool.basilisk] cache` decides. - #[default] - Project, - /// `--cache`: run the cache regardless of configuration. - ForceOn, - /// `--no-cache`: skip the cache regardless of configuration. - ForceOff, -} - -impl CacheOverride { - /// Fold the two mutually reinforcing flags into one decision. - /// - /// `--no-cache` wins when both are passed: an explicit opt-out is the - /// safer reading of a contradictory command line, and it is the flag a - /// user reaches for when they suspect the cache. - #[must_use] - pub const fn from_flags(cache: bool, no_cache: bool) -> Self { - match (cache, no_cache) { - (_, true) => Self::ForceOff, - (true, false) => Self::ForceOn, - (false, false) => Self::Project, - } - } - - /// Resolve against the project configuration ([CHKCACHE-CONFIG]). - fn resolve(self, config: &BasiliskConfig) -> bool { - match self { - Self::ForceOn => true, - Self::ForceOff => false, - Self::Project => config.cache_is_enabled(), - } - } -} - -/// Parsed `--cache*` flags. -#[derive(Debug, Clone)] -pub struct CacheOptions { - /// Per-run override of the configured cache policy. - pub enabled: CacheOverride, - /// Override for the cache directory (`--cache-dir`). - pub dir: Option, - /// Whether to print hit/miss stats (`--cache-stats`). - pub stats: bool, -} - -/// Running hit/miss tally for one `check` invocation. -#[derive(Debug, Default)] -pub struct CacheStats { - /// Number of cache hits. - pub hits: usize, - /// Number of cache misses (full checks). - pub misses: usize, -} - -impl CacheStats { - /// Print the tally to stderr (kept off stdout so JSON output stays clean). - pub fn report(&self) { - eprintln!("cache: {} hit / {} miss", self.hits, self.misses); - } -} - -/// A built cache plus the fingerprint of the non-file inputs for this run. -#[derive(Debug)] -pub struct CacheContext { - cache: CheckCache, - fingerprint: Fingerprint, -} - -/// Build a [`CacheContext`] when the cache is enabled, else `None`. -/// -/// `dir_configs` is the per-directory rule-config map for this run -/// ([CHKARCH-CONFIG-DISCOVERY]) — every directory's config participates in -/// the fingerprint so a child config edit invalidates cached results. -/// `project_config` is the project-root configuration whose `cache`/`cache-dir` -/// keys are this project's standing policy; the flags in `options` override it -/// for this run only ([CHKCACHE-CONFIG]). -#[must_use] -pub fn build_context( - options: &CacheOptions, - project_config: &BasiliskConfig, - dir_configs: &std::collections::BTreeMap>, - search_paths: &ImportSearchPaths, - project_root: &Path, -) -> Option { - let enabled = options.enabled.resolve(project_config); - tracing::debug!( - enabled, - override_source = ?options.enabled, - configured = ?project_config.cache_enabled, - "resolved persistent result-cache policy" - ); - if !enabled { - return None; - } - let dir = options - .dir - .clone() - .unwrap_or_else(|| project_config.cache_directory(project_root)); - let fingerprint = Fingerprint { - version: env!("CARGO_PKG_VERSION").to_owned(), - config_hash: hash_dir_configs(dir_configs), - env_hash: hash_env(search_paths, project_root), - typeshed_id: typeshed_snapshot_identity(search_paths), - }; - Some(CacheContext { - cache: CheckCache::new(dir), - fingerprint, - }) -} - -/// Identity of the active step-3 typeshed snapshot for the fingerprint -/// ([STUBRES-TYPESHED], [CHKCACHE-FINGERPRINT]). -/// -/// The gate-accepted snapshot is the only step-3 identity. Configuration -/// values cannot substitute for bytes the checker actually consumed. -fn typeshed_snapshot_identity(search_paths: &ImportSearchPaths) -> String { - search_paths.typeshed_snapshot.as_ref().map_or_else( - || "unavailable".to_owned(), - basilisk_checker::imports::ActiveTypeshed::identity_fingerprint, - ) -} - -/// Hash the *effective* per-directory configs. Canonicalised through -/// `serde_json::Value` so the hash is stable across runs despite `HashMap` -/// iteration order; the `BTreeMap` fixes the directory order. -fn hash_dir_configs( - dir_configs: &std::collections::BTreeMap>, -) -> u64 { - let parts: Vec = dir_configs - .iter() - .map(|(dir, config)| { - let json = serde_json::to_value(config.as_ref()) - .ok() - .and_then(|value| serde_json::to_string(&value).ok()) - .unwrap_or_default(); - format!("{}={json}", dir.display()) - }) - .collect(); - content_hash(&parts.join("\n")) -} - -/// Hash the resolution environment: search paths plus `uv.lock` contents. -/// -/// This is the v1 boundary: site-packages changes without a `uv.lock` edit are -/// not detected, which is why the cache is opt-in. -// Implements [CHKCACHE-LIMITS] -fn hash_env(search_paths: &ImportSearchPaths, project_root: &Path) -> u64 { - let mut parts = vec![paths_field("roots", &search_paths.roots)]; - parts.push(paths_field("extra", &search_paths.extra_paths)); - parts.push(paths_field("stub", &search_paths.stub_paths)); - parts.push(paths_field("members", &search_paths.workspace_members)); - let site = search_paths - .site_packages - .as_ref() - .map(|p| p.display().to_string()) - .unwrap_or_default(); - parts.push(format!("site={site}")); - parts.push(format!("registry={}", search_paths.registry.is_some())); - if let Ok(lock) = std::fs::read_to_string(project_root.join("uv.lock")) { - parts.push(format!("lock={}", content_hash(&lock))); - } - content_hash(&parts.join("\n")) -} - -/// Render a labelled, order-preserving list of paths for the env fingerprint. -fn paths_field(label: &str, paths: &[PathBuf]) -> String { - let joined = paths - .iter() - .map(|p| p.display().to_string()) - .collect::>() - .join(","); - format!("{label}=[{joined}]") -} - -/// Run a single file's check, served from cache when possible. -/// -/// On a miss, `cold` runs under a [`ReadRecorder`] so the exact read-set is -/// captured and stored. On a hit, the stored diagnostics are replayed and the -/// target source is re-read for rendering. -/// -/// # Errors -/// -/// Propagates `cold`'s error, or an I/O error reading the source on a hit. -pub fn check_file( - context: Option<&CacheContext>, - stats: &mut CacheStats, - path: &str, - cold: F, -) -> Result<(Vec, String), String> -where - F: FnOnce() -> Result<(Vec, String), String>, -{ - let Some(context) = context else { - return cold(); - }; - let target = Path::new(path); - if let Some(hit) = context - .cache - .lookup::>(target, &context.fingerprint) - { - stats.hits += 1; - let source = std::fs::read_to_string(path).map_err(|err| err.to_string())?; - let diagnostics = hit - .into_iter() - .map(CachedDiagnostic::into_diagnostic) - .collect(); - return Ok((diagnostics, source)); - } - stats.misses += 1; - store_fresh(context, target, path, cold) -} - -/// Run `cold` under a recorder and persist the result. -fn store_fresh( - context: &CacheContext, - target: &Path, - path: &str, - cold: F, -) -> Result<(Vec, String), String> -where - F: FnOnce() -> Result<(Vec, String), String>, -{ - let recorder = ReadRecorder::start(); - let result = cold(); - let read_set = recorder.finish(); - let (diagnostics, source) = result?; - let cached: Vec = diagnostics.iter().map(CachedDiagnostic::from).collect(); - match context - .cache - .store(target, &context.fingerprint, read_set, &cached) - { - Ok(()) => tracing::debug!(path, "cache miss: stored fresh result"), - Err(err) => tracing::warn!(path, %err, "failed to write cache entry"), - } - Ok((diagnostics, source)) -} - -#[cfg(test)] -#[expect( - clippy::expect_used, - reason = "test-only fixed Snapshot fixtures must fail loudly" -)] -mod tests { - use std::collections::BTreeMap; - use std::sync::Arc; - - use basilisk_checker::imports::ActiveTypeshed; - use basilisk_stubs::typeshed::archive::{Archive, ArchiveEntry, ArchiveVfs}; - use basilisk_stubs::typeshed::gittree::{FileMode, Oid}; - use basilisk_stubs::typeshed::snapshot::Snapshot; - use basilisk_stubs::typeshed::source::{ - LicenseStatus, SourceIdentity, SourceKind, TypeshedStatus, - }; - - use super::{ - build_context, typeshed_snapshot_identity, BasiliskConfig, CacheContext, CacheOptions, - CacheOverride, - }; - - fn snapshot(identity: SourceIdentity) -> Arc { - let status = TypeshedStatus { - active_source: if matches!(identity, SourceIdentity::Custom { .. }) { - SourceKind::Custom - } else { - SourceKind::ExactCommit - }, - commit: identity.commit(), - tree: identity.commit(), - license_status: if matches!(identity, SourceIdentity::Custom { .. }) { - LicenseStatus::NotSupplied - } else { - LicenseStatus::Approved - }, - license_reference: None, - warnings: Vec::new(), - }; - let archive = Archive::new(vec![ - ArchiveEntry { - path: "stdlib/VERSIONS".to_owned().into(), - mode: FileMode::Regular, - data: b"os: 3.0-\n".to_vec().into(), - }, - ArchiveEntry { - path: "stdlib/os.pyi".to_owned().into(), - mode: FileMode::Regular, - data: b"name: str\n".to_vec().into(), - }, - ]); - let uri = identity.uri_component(); - Arc::new( - Snapshot::build(identity, status, ArchiveVfs::new(uri, archive), None) - .expect("valid cache-identity fixture"), - ) - } - - fn fingerprint(snapshot: Arc) -> String { - let mut paths = crate::import_search::roots_only(Vec::new()); - paths.typeshed_snapshot = Some(ActiveTypeshed::new(snapshot, None)); - typeshed_snapshot_identity(&paths) - } - - fn cache_context(cache_dir: &std::path::Path, snapshot: Arc) -> CacheContext { - let mut paths = crate::import_search::roots_only(Vec::new()); - paths.typeshed_snapshot = Some(ActiveTypeshed::new(snapshot, None)); - build_context( - &CacheOptions { - enabled: CacheOverride::ForceOn, - dir: Some(cache_dir.to_path_buf()), - stats: false, - }, - &BasiliskConfig::default(), - &BTreeMap::new(), - &paths, - cache_dir, - ) - .expect("enabled cache context") - } - - #[test] - fn active_snapshot_identity_distinguishes_commits_custom_and_bundle() { - let commit_a = - Oid::from_hex("1111111111111111111111111111111111111111").expect("valid commit A"); - let commit_b = - Oid::from_hex("2222222222222222222222222222222222222222").expect("valid commit B"); - let a = fingerprint(snapshot(SourceIdentity::Commit { - commit: commit_a, - pinned: true, - })); - let same_a = fingerprint(snapshot(SourceIdentity::Commit { - commit: commit_a, - pinned: false, - })); - let b = fingerprint(snapshot(SourceIdentity::Commit { - commit: commit_b, - pinned: false, - })); - let custom = fingerprint(snapshot(SourceIdentity::Custom { - digest: "custom-tree".to_owned(), - })); - let bundled = fingerprint(snapshot(SourceIdentity::Bundled { commit: commit_a })); - - assert_eq!(a, same_a, "pin policy does not change identical bytes"); - assert_ne!(a, b); - assert_ne!(a, custom); - assert_ne!(a, bundled); - } - - #[test] - fn checker_cache_hits_only_for_the_identical_active_snapshot_identity() { - let directory = tempfile::tempdir().expect("cache directory"); - let target = std::path::Path::new("/workspace/module.py"); - let commit_a = Oid::from_hex("1111111111111111111111111111111111111111").expect("commit A"); - let commit_b = Oid::from_hex("2222222222222222222222222222222222222222").expect("commit B"); - let stored = cache_context( - directory.path(), - snapshot(SourceIdentity::Commit { - commit: commit_a, - pinned: true, - }), - ); - let payload = vec!["cached diagnostics".to_owned()]; - stored - .cache - .store(target, &stored.fingerprint, BTreeMap::new(), &payload) - .expect("store checker result"); - - let identical = cache_context( - directory.path(), - snapshot(SourceIdentity::Commit { - commit: commit_a, - pinned: false, - }), - ); - assert_eq!( - identical - .cache - .lookup::>(target, &identical.fingerprint), - Some(payload) - ); - - for identity in [ - SourceIdentity::Commit { - commit: commit_b, - pinned: false, - }, - SourceIdentity::Custom { - digest: "custom-tree".to_owned(), - }, - SourceIdentity::Bundled { commit: commit_a }, - ] { - let changed = cache_context(directory.path(), snapshot(identity)); - assert!(changed - .cache - .lookup::>(target, &changed.fingerprint) - .is_none()); - } - } -} diff --git a/crates/basilisk-cli/src/fix.rs b/crates/basilisk-cli/src/fix.rs deleted file mode 100644 index ac610f8aa..000000000 --- a/crates/basilisk-cli/src/fix.rs +++ /dev/null @@ -1,547 +0,0 @@ -//! Implements [CHKARCH-CLI]. See docs/specs/CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-CLI -//! `basilisk fix` subcommand — apply autofixes to Python files. -//! -//! For each Python file: parse → resolve → check → generate fixes → apply. -//! Writes the fixed source back to disk. - -use basilisk_lsp::code_actions::mass_fix::{ALL_FIXABLE_RULES, SAFE_FIXABLE_RULES}; -use tower_lsp::lsp_types::{TextEdit, Url}; -use tracing::{info, warn}; - -use crate::pipeline::pluralise; - -/// Run the fix subcommand. -/// -/// Exit codes: -/// - `0` — fixes applied successfully -/// - `1` — some files had errors that couldn't be fixed -/// - `3` — internal error -pub(crate) fn run_fix(paths: &[String], include_unsafe: bool, rules: &[String]) -> u8 { - let allowed_rules = resolve_rules(include_unsafe, rules); - let allowed_refs: Vec<&str> = allowed_rules.iter().map(String::as_str).collect(); - match collect_and_fix(paths, &allowed_refs) { - Ok(summary) => { - println!( - "Fixed {} diagnostic{} in {} file{}.", - summary.fixed_count, - pluralise(summary.fixed_count), - summary.files_fixed, - pluralise(summary.files_fixed), - ); - u8::from(summary.had_unfixable_errors) - } - Err(err) => { - tracing::error!(%err, "internal error"); - 3 - } - } -} - -/// Summary of a fix run. -struct FixSummary { - /// Total number of diagnostics that were fixed. - fixed_count: usize, - /// Number of files that had at least one fix applied. - files_fixed: usize, - /// Whether any file had errors that could not be auto-fixed. - had_unfixable_errors: bool, -} - -/// Resolve which rules to apply based on CLI flags. -/// -/// - Empty `rules` + `include_unsafe` false → safe rules only. -/// - Empty `rules` + `include_unsafe` true → all fixable rules. -/// - Single entry `"all"` (case-insensitive) → all fixable rules. -/// - Otherwise → the provided rules verbatim. -fn resolve_rules(include_unsafe: bool, rules: &[String]) -> Vec { - if rules.is_empty() { - if include_unsafe { - ALL_FIXABLE_RULES.iter().map(|s| (*s).to_owned()).collect() - } else { - SAFE_FIXABLE_RULES.iter().map(|s| (*s).to_owned()).collect() - } - } else if rules.len() == 1 && rules.first().is_some_and(|r| r.eq_ignore_ascii_case("all")) { - ALL_FIXABLE_RULES.iter().map(|s| (*s).to_owned()).collect() - } else { - rules.to_vec() - } -} - -/// Collect Python files, analyse them, apply fixes, and write back. -fn collect_and_fix(paths: &[String], allowed_rules: &[&str]) -> Result { - // [CHKARCH-CONFIG-DISCOVERY] Rule config resolves per file, exactly like - // `basilisk check` (GitHub #311). - let config_root = crate::pipeline::first_path_dir(paths); - let config = basilisk_config::load_basilisk_config(&config_root); - - let excluded = crate::pipeline::excluded_dirs_and_log(&config, &config_root); - - // [CHKARCH-CONFIG-INCLUDE] (GitHub #333): a no-args run walks only the - // configured include roots, exactly like `check`/`analyze`. `fix` mutates - // files, so defaulting to the whole working directory would rewrite - // vendored sources (`venv/`) the user never asked it to touch. - let paths = &crate::pipeline::effective_check_paths(paths, &config, &config_root); - let python_files = crate::pipeline::collect_python_files(paths, &excluded)?; - let dir_configs = crate::pipeline::resolve_dir_configs(&python_files, &config); - - let mut fixed_count: usize = 0; - let mut files_fixed: usize = 0; - let mut had_unfixable_errors = false; - - for path in python_files { - let file_config = crate::pipeline::config_for_path(&dir_configs, &path, &config); - match fix_single_file(&path, allowed_rules, &file_config) { - Ok(count) => { - fixed_count += count; - if count > 0 { - files_fixed += 1; - } - } - Err(err) => { - warn!(path, %err, "error processing file"); - had_unfixable_errors = true; - } - } - } - - Ok(FixSummary { - fixed_count, - files_fixed, - had_unfixable_errors, - }) -} - -/// Analyse a single file and apply fixes matching the allowed rules. -/// -/// Returns the number of fixes applied. -fn fix_single_file( - path: &str, - allowed_rules: &[&str], - config: &basilisk_config::BasiliskConfig, -) -> Result { - let source = std::fs::read_to_string(path).map_err(|e| format!("{path}: {e}"))?; - let uri = Url::from_file_path( - std::path::Path::new(path) - .canonicalize() - .map_err(|e| format!("{path}: {e}"))?, - ) - .map_err(|()| format!("{path}: cannot convert to file URI"))?; - - let parsed = basilisk_parser::parse_source(source.clone(), path.to_owned()) - .map_err(|e| e.to_string())?; - let resolved = basilisk_resolver::resolve(&parsed).map_err(|e| e.to_string())?; - let checker_diags = basilisk_checker::check_with_config(&resolved, config); - - let lsp_diags: Vec<_> = checker_diags - .iter() - .map(|d| basilisk_lsp::workspace_analysis::bsk_to_lsp(d, &source)) - .collect(); - - let Some(action) = basilisk_lsp::code_actions::mass_fix::fix_filtered_in_file( - &uri, - &lsp_diags, - &source, - allowed_rules, - ) else { - return Ok(0); - }; - - let edits = action - .edit - .and_then(|ws| ws.changes) - .and_then(|mut map| map.remove(&uri)) - .unwrap_or_default(); - - if edits.is_empty() { - return Ok(0); - } - - let edit_count = edits.len(); - let fixed_source = apply_text_edits(&source, &edits); - std::fs::write(path, fixed_source).map_err(|e| format!("{path}: {e}"))?; - - info!(path, edit_count, "applied fixes"); - Ok(edit_count) -} - -/// Apply LSP text edits to source text. -/// -/// Edits are sorted by position descending (bottom-to-top) so that earlier -/// offsets remain valid as later text is modified. -fn apply_text_edits(source: &str, edits: &[TextEdit]) -> String { - let mut indexed: Vec<_> = edits - .iter() - .map(|edit| { - let start = basilisk_lsp::util::position_to_byte_offset(source, edit.range.start); - let end = basilisk_lsp::util::position_to_byte_offset(source, edit.range.end); - (start, end, &edit.new_text) - }) - .collect(); - - // Sort descending by start offset so we can apply from the end. - indexed.sort_by(|a, b| b.0.cmp(&a.0).then(b.1.cmp(&a.1))); - - let mut result = source.to_owned(); - for (start, end, new_text) in indexed { - let clamped_start = start.min(result.len()); - let clamped_end = end.min(result.len()); - result.replace_range(clamped_start..clamped_end, new_text); - } - - result -} - -#[cfg(test)] -#[expect( - clippy::expect_used, - reason = "test-only code: expect acceptable in unit tests" -)] -mod tests { - use super::*; - use tower_lsp::lsp_types::{Position, Range}; - - /// Write `source` to a uniquely-named temp `.py` file inside an isolated - /// project dir that ships a `pyproject.toml` opting into the annotation - /// house rules. `fix` targets those rules (`BSK-0001`/`BSK-0002`/ - /// `BSK-0005`/`BSK-0050`), which are OFF by default — a real user enables - /// them in configuration, so the test project does too. The command loads - /// that config from disk exactly as it would in production. No modes; this - /// is configuration. See [CHKARCH-CONFIGURATION-ONLY]. - fn write_temp(name: &str, source: &str) -> (std::path::PathBuf, String) { - // Per-process dir name (same pattern as `stage_project` in - // cli_binary_tests): a stray watcher or leftover harness process from - // a previous run must never touch this run's fixture files. - let dir = std::env::temp_dir().join(format!("{name}.{}.proj", std::process::id())); - std::fs::create_dir_all(&dir).expect("create temp project dir"); - std::fs::write( - dir.join("pyproject.toml"), - "[tool.basilisk.rules]\n\"BSK-0001\" = \"error\"\n\"BSK-0002\" = \"error\"\n\"BSK-0005\" = \"error\"\n\"BSK-0050\" = \"warning\"\n", - ) - .expect("write pyproject.toml"); - let py = dir.join(name); - std::fs::write(&py, source).expect("write temp file"); - let path = py.to_string_lossy().into_owned(); - (py, path) - } - - /// Remove the isolated project dir created by [`write_temp`]. - fn cleanup(py: &std::path::Path) { - if let Some(dir) = py.parent() { - let _ = std::fs::remove_dir_all(dir); - } - } - - /// Run fix, read back the file, clean up, and return `(exit_code, content)`. - fn fix_and_read( - path_str: &str, - py: &std::path::Path, - include_unsafe: bool, - rules: &[String], - ) -> (u8, String) { - let code = run_fix(&[path_str.to_owned()], include_unsafe, rules); - let content = std::fs::read_to_string(py).expect("read back"); - cleanup(py); - (code, content) - } - - #[test] - fn apply_text_edits_empty_edits() { - let source = "hello world"; - let result = apply_text_edits(source, &[]); - assert_eq!(result, "hello world"); - } - - #[test] - fn apply_text_edits_single_insert() { - let source = "x = 42\n"; - let edits = vec![TextEdit { - range: Range::new(Position::new(0, 1), Position::new(0, 1)), - new_text: ": int".to_owned(), - }]; - assert_eq!(apply_text_edits(source, &edits), "x: int = 42\n"); - } - - #[test] - fn apply_text_edits_single_delete() { - let source = "x: int = 42\n"; - let edits = vec![TextEdit { - range: Range::new(Position::new(0, 1), Position::new(0, 7)), - new_text: String::new(), - }]; - assert_eq!(apply_text_edits(source, &edits), "x= 42\n"); - } - - #[test] - fn apply_text_edits_multiple_non_overlapping() { - let source = "x = 1\ny = 2\n"; - let edits = vec![ - TextEdit { - range: Range::new(Position::new(0, 1), Position::new(0, 1)), - new_text: ": int".to_owned(), - }, - TextEdit { - range: Range::new(Position::new(1, 1), Position::new(1, 1)), - new_text: ": int".to_owned(), - }, - ]; - assert_eq!(apply_text_edits(source, &edits), "x: int = 1\ny: int = 2\n"); - } - - #[test] - fn pluralise_returns_empty_for_one() { - assert_eq!(pluralise(1), ""); - } - - #[test] - fn pluralise_returns_s_for_zero() { - assert_eq!(pluralise(0), "s"); - } - - #[test] - fn pluralise_returns_s_for_many() { - assert_eq!(pluralise(5), "s"); - } - - #[test] - fn run_fix_nonexistent_path_returns_three() { - assert_eq!(run_fix(&["/no/such/path.py".to_owned()], false, &[]), 3); - } - - #[test] - fn run_fix_clean_code_returns_zero() { - let (py, path) = write_temp( - "basilisk_test_fix_clean.py", - "def greet(name: str) -> str:\n return name\n", - ); - let code = run_fix(&[path], false, &[]); - cleanup(&py); - assert_eq!(code, 0, "clean code must return 0"); - } - - #[test] - fn run_fix_applies_fixes_to_file() { - let (py, path) = write_temp("basilisk_test_fix_apply.py", "x: int = 42\n"); - let (code, fixed) = fix_and_read(&path, &py, false, &[]); - assert_eq!(code, 0, "fixable code must return 0"); - assert_eq!(fixed, "x = 42\n", "redundant annotation should be removed"); - } - - #[test] - fn run_fix_with_specific_rule_only_fixes_that_rule() { - let (py, path) = write_temp("basilisk_test_fix_specific_rule.py", "x: int = 42\n"); - let (code, fixed) = fix_and_read(&path, &py, false, &["BSK-0050".to_owned()]); - assert_eq!(code, 0); - assert_eq!( - fixed, "x = 42\n", - "BSK-0050 fix should be applied when specified" - ); - } - - #[test] - fn run_fix_with_unmatched_rule_does_not_fix() { - let (py, path) = write_temp("basilisk_test_fix_unmatched_rule.py", "x: int = 42\n"); - let (code, fixed) = fix_and_read(&path, &py, false, &["BSK-0001".to_owned()]); - assert_eq!(code, 0); - assert_eq!( - fixed, "x: int = 42\n", - "file unchanged when rule does not match" - ); - } - - #[test] - fn run_fix_with_rules_all_applies_all_rules() { - let (py, path) = write_temp("basilisk_test_fix_rules_all.py", "x: int = 42\n"); - let (code, fixed) = fix_and_read(&path, &py, false, &["all".to_owned()]); - assert_eq!(code, 0); - assert_eq!(fixed, "x = 42\n", "--rules all should apply all fixes"); - } - - #[test] - fn run_fix_empty_rules_applies_all_safe_rules() { - let (py, path) = write_temp("basilisk_test_fix_default_safe.py", "x: int = 42\n"); - let (code, fixed) = fix_and_read(&path, &py, false, &[]); - assert_eq!(code, 0); - assert_eq!( - fixed, "x = 42\n", - "default (safe) rules should fix BSK-0050" - ); - } - - #[test] - fn resolve_rules_empty_safe() { - let result = resolve_rules(false, &[]); - let expected: Vec = SAFE_FIXABLE_RULES - .iter() - .map(|s| (*s).to_string()) - .collect(); - assert_eq!(result, expected); - } - - #[test] - fn resolve_rules_empty_unsafe() { - let result = resolve_rules(true, &[]); - let expected: Vec = ALL_FIXABLE_RULES.iter().map(|s| (*s).to_string()).collect(); - assert_eq!(result, expected); - } - - #[test] - fn resolve_rules_all_keyword() { - let result = resolve_rules(false, &["ALL".to_owned()]); - let expected: Vec = ALL_FIXABLE_RULES.iter().map(|s| (*s).to_string()).collect(); - assert_eq!(result, expected); - } - - #[test] - fn resolve_rules_specific_list() { - let input = vec!["BSK-0001".to_owned(), "BSK-0050".to_owned()]; - assert_eq!(resolve_rules(false, &input), input); - } - - // ── New e2e tests ──────────────────────────────────────────────────── - - #[test] - fn run_fix_applies_e0001_missing_param_annotation() { - let (py, path) = write_temp("basilisk_test_fix_e0001.py", "def foo(x):\n pass\n"); - let (code, fixed) = fix_and_read(&path, &py, false, &["BSK-0001".to_owned()]); - assert_eq!(code, 0, "BSK-0001 fix must return 0"); - assert!(fixed.contains("def foo(x: Any)"), "got: {fixed}"); - } - - #[test] - fn run_fix_applies_e0002_missing_return_annotation() { - // The returned method call is not inferable, so BSK-0002 fires and the - // fix inserts the honest `-> Any` placeholder ([TYPEINF-FUNC-RETURN]). - // A `pass` body would infer `-> None` and leave nothing to fix. - let (py, path) = write_temp( - "basilisk_test_fix_e0002.py", - "def foo(x: int):\n return x.bit_length()\n", - ); - let (code, fixed) = fix_and_read(&path, &py, false, &["BSK-0002".to_owned()]); - assert_eq!(code, 0, "BSK-0002 fix must return 0"); - assert_eq!( - fixed, - "def foo(x: int) -> Any:\n return x.bit_length()\n" - ); - } - - #[test] - fn run_fix_applies_e0005_missing_attribute_annotation() { - let (py, path) = write_temp("basilisk_test_fix_e0005.py", "class Foo:\n bar = []\n"); - let (code, fixed) = fix_and_read(&path, &py, false, &["BSK-0005".to_owned()]); - assert_eq!(code, 0, "BSK-0005 fix must return 0"); - assert!(fixed.contains("bar: Any = []"), "got: {fixed}"); - } - - #[test] - fn run_fix_applies_multiple_rules_in_one_file() { - // `x` has no default to infer from (BSK-0001) and `return x` is not - // inferable (BSK-0002); `y: int = 42` is redundant (BSK-0050). - let (py, path) = write_temp( - "basilisk_test_fix_multi_rules.py", - "def foo(x):\n return x\n\ny: int = 42\n", - ); - let (code, fixed) = fix_and_read(&path, &py, false, &[]); - assert_eq!(code, 0); - assert!( - fixed.contains("x: Any"), - "BSK-0001 not applied, got: {fixed}" - ); - assert!( - fixed.contains("-> Any"), - "BSK-0002 not applied, got: {fixed}" - ); - assert!( - fixed.contains("y = 42"), - "BSK-0050 not applied, got: {fixed}" - ); - } - - #[test] - fn run_fix_directory_traversal() { - // Per-process dir name — see `write_temp` for the rationale. - let dir = std::env::temp_dir().join(format!( - "basilisk_test_fix_dir_traversal.{}", - std::process::id() - )); - let _ = std::fs::create_dir_all(&dir); - std::fs::write( - dir.join("pyproject.toml"), - "[tool.basilisk.rules]\n\"BSK-0050\" = \"warning\"\n", - ) - .expect("write pyproject.toml"); - let file_a = dir.join("a_fix.py"); - let file_b = dir.join("b_fix.py"); - std::fs::write(&file_a, "x: int = 42\n").expect("write a"); - std::fs::write(&file_b, "y: str = \"hello\"\n").expect("write b"); - let code = run_fix( - &[dir.to_string_lossy().into_owned()], - false, - &["BSK-0050".to_owned()], - ); - let fixed_a = std::fs::read_to_string(&file_a).expect("read a"); - let fixed_b = std::fs::read_to_string(&file_b).expect("read b"); - let _ = std::fs::remove_dir_all(&dir); - assert_eq!(code, 0); - assert_eq!(fixed_a, "x = 42\n", "BSK-0050 not applied to first file"); - assert_eq!( - fixed_b, "y = \"hello\"\n", - "BSK-0050 not applied to second file" - ); - } - - #[test] - fn run_fix_is_idempotent() { - let (py, path) = write_temp("basilisk_test_fix_idempotent.py", "x: int = 42\n"); - let first = run_fix(std::slice::from_ref(&path), false, &[]); - let after_first = std::fs::read_to_string(&py).expect("read after first"); - assert_eq!(first, 0); - assert_eq!(after_first, "x = 42\n"); - - let second = run_fix(&[path], false, &[]); - let after_second = std::fs::read_to_string(&py).expect("read after second"); - cleanup(&py); - assert_eq!(second, 0); - assert_eq!(after_second, "x = 42\n", "second pass should be a no-op"); - } - - #[test] - fn run_fix_with_unsafe_flag() { - let (py, path) = write_temp("basilisk_test_fix_unsafe.py", "x: int = 42\n"); - let (code, fixed) = fix_and_read(&path, &py, true, &[]); - assert_eq!(code, 0); - assert_eq!( - fixed, "x = 42\n", - "include_unsafe=true should apply BSK-0050" - ); - } - - #[test] - fn run_fix_preserves_surrounding_content() { - let source = "# This is a comment\n\nx: int = 42\n\n\ - # Another comment\ndef greet(name: str) -> str:\n\ - \x20 \"\"\"Say hello.\"\"\"\n return f\"Hello, {name}\"\n"; - let (py, path) = write_temp("basilisk_test_fix_preserves.py", source); - let (code, fixed) = fix_and_read(&path, &py, false, &[]); - assert_eq!(code, 0); - assert!( - fixed.contains("# This is a comment"), - "leading comment lost" - ); - assert!(fixed.contains("# Another comment"), "middle comment lost"); - assert!(fixed.contains("\"\"\"Say hello.\"\"\""), "docstring lost"); - assert!( - fixed.contains("def greet(name: str) -> str:"), - "clean fn changed" - ); - assert!(fixed.contains("x = 42"), "BSK-0050 fix not applied"); - } - - #[test] - fn run_fix_no_fixable_diagnostics_leaves_file_unchanged() { - let source = "def foo(x: int) -> int:\n return \"hello\"\n"; - let (py, path) = write_temp("basilisk_test_fix_unfixable_diags.py", source); - let (code, fixed) = fix_and_read(&path, &py, false, &[]); - assert_eq!(code, 0, "unfixable diagnostics should not cause errors"); - assert_eq!(fixed, source, "file must be unchanged when no fixes apply"); - } -} diff --git a/crates/basilisk-cli/src/format.rs b/crates/basilisk-cli/src/format.rs deleted file mode 100644 index 65e97d14c..000000000 --- a/crates/basilisk-cli/src/format.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! Implements [LSPFMT-CLIENTS] and [CHKARCH-CLI-COMMANDS]. See -//! docs/specs/LSP-FORMATTING-SPEC.md#LSPFMT-CLIENTS -//! `basilisk format` subcommand — format Python files in place, or verify -//! them with `--check`, using the embedded Ruff formatter. -//! -//! Same engine, same style source as LSP `textDocument/formatting` -//! ([LSPFMT-ENGINE]): for identical input and configuration the output bytes -//! are identical. No `ruff` executable is ever spawned ([LSPFMT-DECISION]). - -use basilisk_lsp::config::{FormatStyle, FormatterEngine}; -use basilisk_lsp::formatting::{format_document, EMBEDDED_RUFF_FORMATTER_VERSION}; -use tracing::warn; - -use crate::pipeline::pluralise; - -/// Run the format subcommand. -/// -/// Exit codes: -/// - `0` — write mode completed, or check mode found every file formatted -/// - `1` — check mode found unformatted files, or a file failed to parse -/// - `3` — internal error (path collection, config discovery) -pub(crate) fn run_format(paths: &[String], check: bool) -> u8 { - let config_root = crate::pipeline::first_path_dir(paths); - let workspace = basilisk_lsp::config::load_config(&config_root); - if workspace.formatter == FormatterEngine::Disabled { - // [LSPFMT-CONFIG]: `"none"` disables formatting; mirror the LSP, - // which stops advertising the formatting capabilities. - println!("Formatter is disabled (formatter = \"none\"); nothing to do."); - return 0; - } - match collect_and_format(paths, &workspace.format_style, check) { - Ok(summary) => summarise(&summary, check), - Err(err) => { - tracing::error!(%err, "internal error"); - 3 - } - } -} - -/// Outcome of formatting one file. -enum FileOutcome { - /// The file was rewritten (write mode) or would be (check mode). - Changed, - /// The file is already formatted. - Clean, -} - -/// Result of a whole format run. -#[derive(Default)] -struct FormatSummary { - /// Files rewritten (write mode) or needing a rewrite (check mode). - changed: usize, - /// Files already formatted. - unchanged: usize, - /// Files that could not be read or parsed. - failures: usize, -} - -/// Collect Python files under `paths` and format each one. -/// -/// Path collection honours the same `[tool.basilisk]` `exclude` semantics as -/// `check` and `fix` ([CHKARCH-CONFIG-EXCLUDE]). -fn collect_and_format( - paths: &[String], - style: &FormatStyle, - check: bool, -) -> Result { - let config_root = crate::pipeline::first_path_dir(paths); - let config = basilisk_config::load_basilisk_config(&config_root); - let excluded = crate::pipeline::excluded_dirs_and_log(&config, &config_root); - let python_files = crate::pipeline::collect_python_files(paths, &excluded)?; - - let mut summary = FormatSummary::default(); - for path in python_files { - match format_single_file(&path, style, check) { - Ok(FileOutcome::Changed) => summary.changed += 1, - Ok(FileOutcome::Clean) => summary.unchanged += 1, - Err(err) => { - warn!(path, %err, "cannot format file"); - summary.failures += 1; - } - } - } - Ok(summary) -} - -/// Format one file: rewrite it in write mode, report it in check mode. -fn format_single_file(path: &str, style: &FormatStyle, check: bool) -> Result { - let source = std::fs::read_to_string(path).map_err(|e| e.to_string())?; - let Some(formatted) = formatted_text(&source, style) else { - // `format_document` returns `None` for already-formatted AND for - // unparseable sources; parse to tell them apart. Like `ruff format`, - // invalid syntax is refused, never rewritten. - return match basilisk_parser::parse_source(source, path.to_owned()) { - Ok(_) => Ok(FileOutcome::Clean), - Err(err) => Err(err.to_string()), - }; - }; - if check { - println!("Would reformat: {path}"); - return Ok(FileOutcome::Changed); - } - std::fs::write(path, formatted).map_err(|e| e.to_string())?; - Ok(FileOutcome::Changed) -} - -/// The full formatted text, or `None` when the source is already formatted -/// or does not parse ([LSPFMT-ENGINE] pure passthrough). -fn formatted_text(source: &str, style: &FormatStyle) -> Option { - format_document(source, style)? - .into_iter() - .next() - .map(|edit| edit.new_text) -} - -/// Print the run summary and derive the exit code. -/// -/// The summary names the engine and version — the CLI face of the -/// provenance contract ([LSPFMT-PROVENANCE]). -fn summarise(summary: &FormatSummary, check: bool) -> u8 { - let changed = summary.changed; - let verb = if check { - format!("{changed} file{} would be reformatted", pluralise(changed)) - } else { - format!("Reformatted {changed} file{}", pluralise(changed)) - }; - println!( - "{verb}, {} already formatted (embedded Ruff {EMBEDDED_RUFF_FORMATTER_VERSION}).", - summary.unchanged - ); - if summary.failures > 0 { - println!( - "{} file{} failed to parse and {} left unchanged.", - summary.failures, - pluralise(summary.failures), - if summary.failures == 1 { "was" } else { "were" } - ); - } - u8::from(summary.failures > 0 || (check && changed > 0)) -} diff --git a/crates/basilisk-cli/src/import_search.rs b/crates/basilisk-cli/src/import_search.rs deleted file mode 100644 index f4b4677fd..000000000 --- a/crates/basilisk-cli/src/import_search.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! CLI import-search setup fast paths. -//! -//! Implements [ANALYSIS-CROSSLSP-IMPORT]. Search-path discovery probes Python, -//! uv metadata, and nested projects. None of those paths can affect a resolved -//! module with no import statements, so a proven no-import batch keeps only its -//! roots and avoids that fixed setup cost. - -use std::path::PathBuf; - -use basilisk_lsp::import_resolver::ImportSearchPaths; - -const IMPORT_KEYWORD: &[u8] = b"import"; - -/// Return `true` unless every source file proves it contains no Python import -/// keyword. -/// -/// The byte search is deliberately conservative. Strings, comments, and names -/// such as `important` can produce a false positive and take the full discovery -/// path. They cannot produce a false negative: Python's `import` keyword is -/// always the literal lowercase ASCII token. An unreadable file also fails open -/// so the ordinary analysis path retains its existing error behaviour. -pub(crate) fn files_might_import(paths: &[String]) -> bool { - paths.iter().any(|path| match std::fs::read(path) { - Ok(source) => source_might_import(&source), - Err(_) => true, - }) -} - -fn source_might_import(source: &[u8]) -> bool { - source - .windows(IMPORT_KEYWORD.len()) - .any(|window| window == IMPORT_KEYWORD) -} - -/// Build the complete search-path value required by the resolver when there -/// are no imports to resolve. Roots are retained for API consistency; all -/// import-only fields are empty by proof from [`files_might_import`]. -pub(crate) fn roots_only(roots: Vec) -> ImportSearchPaths { - ImportSearchPaths { - roots, - extra_paths: Vec::new(), - stub_paths: Vec::new(), - workspace_members: Vec::new(), - site_packages: None, - registry: None, - typeshed_snapshot: None, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn import_statements_take_full_discovery_path() { - assert!(source_might_import(b"import package\n")); - assert!(source_might_import(b"from package import symbol\n")); - } - - #[test] - fn import_free_source_takes_roots_only_path() { - assert!(!source_might_import(b"value: int = 42\n")); - } - - #[test] - fn false_positives_are_conservative() { - assert!(source_might_import(b"important = 'not syntax'\n")); - assert!(source_might_import(b"# import mentioned in a comment\n")); - } - - #[test] - fn unreadable_source_fails_open() { - assert!(files_might_import(&[format!( - "/path/that/does/not/exist/{}", - std::process::id() - )])); - } - - #[test] - fn roots_only_retains_roots_and_empties_import_fields() { - let root = PathBuf::from("/workspace"); - let paths = roots_only(vec![root.clone()]); - - assert_eq!(paths.roots, vec![root]); - assert!(paths.extra_paths.is_empty()); - assert!(paths.stub_paths.is_empty()); - assert!(paths.workspace_members.is_empty()); - assert!(paths.site_packages.is_none()); - assert!(paths.registry.is_none()); - assert!(paths.typeshed_snapshot.is_none()); - } -} diff --git a/crates/basilisk-cli/src/mcp.rs b/crates/basilisk-cli/src/mcp.rs deleted file mode 100644 index 343c25ad5..000000000 --- a/crates/basilisk-cli/src/mcp.rs +++ /dev/null @@ -1,415 +0,0 @@ -//! Implements [MCP-TYPESHED-STATUS]. See -//! docs/specs/CHECKER-MCP-SPEC.md#MCP-TYPESHED-STATUS -//! -//! Minimal Model Context Protocol server over stdio. The transport is kept -//! deliberately small: one read-only tool reports the exact typeshed status -//! produced by the shared acquisition subsystem. JSON-RPC messages are one -//! UTF-8 JSON value per line; stdout is reserved exclusively for responses. - -use std::path::Path; -use std::sync::OnceLock; - -use serde_json::{json, Value}; - -const PROTOCOL_VERSION: &str = "2025-11-25"; -const STATUS_TOOL: &str = "basilisk_typeshed_status"; -const MAX_MESSAGE_BYTES: usize = 1_048_576; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Lifecycle { - AwaitingInitialize, - AwaitingInitialized, - Ready, -} - -#[derive(Debug)] -enum IncomingLine { - Json(String), - InvalidUtf8, - TooLarge, -} - -/// Run the MCP server on process stdin/stdout for `workspace`. -/// -/// # Errors -/// -/// Returns a descriptive error when the transport cannot be read or written. -/// Acquisition failures are reported as MCP tool errors so the stdio session -/// remains valid for subsequent protocol requests. -pub(crate) fn run(workspace: &Path) -> Result<(), String> { - let input = std::io::stdin(); - let output = std::io::stdout(); - let status = OnceLock::new(); - run_transport(input.lock(), output.lock(), || { - status - .get_or_init(|| status_for_workspace(workspace)) - .clone() - }) -} - -/// Serve requests using injected streams and status provider. -/// -/// The seam makes protocol behavior hermetic while production still consumes -/// the same runtime status object as the CLI and LSP. -fn run_transport(mut reader: R, mut writer: W, status: F) -> Result<(), String> -where - R: std::io::BufRead, - W: std::io::Write, - F: Fn() -> Result, -{ - let mut lifecycle = Lifecycle::AwaitingInitialize; - while let Some(line) = read_line_limited(&mut reader)? { - let response = match line { - IncomingLine::Json(line) => handle_line(&line, &mut lifecycle, &status), - IncomingLine::InvalidUtf8 => Some(error_response( - Value::Null, - -32700, - "MCP message is not valid UTF-8", - )), - IncomingLine::TooLarge => Some(error_response( - Value::Null, - -32600, - "MCP message exceeds 1 MiB", - )), - }; - if let Some(response) = response { - serde_json::to_writer(&mut writer, &response) - .map_err(|error| format!("failed to encode MCP response: {error}"))?; - writer - .write_all(b"\n") - .and_then(|()| writer.flush()) - .map_err(|error| format!("failed to write MCP stdout: {error}"))?; - } - } - Ok(()) -} - -fn read_line_limited(reader: &mut R) -> Result, String> { - let mut bytes = Vec::new(); - let mut too_large = false; - loop { - let available = reader - .fill_buf() - .map_err(|error| format!("failed to read MCP stdin: {error}"))?; - if available.is_empty() { - if bytes.is_empty() && !too_large { - return Ok(None); - } - break; - } - let newline = available.iter().position(|byte| *byte == b'\n'); - let consumed = newline.map_or(available.len(), |position| position + 1); - if !too_large { - // Keep at most one byte beyond the content limit. That byte is - // either the permitted newline or proof that the line is too big. - let remaining = (MAX_MESSAGE_BYTES + 1).saturating_sub(bytes.len()); - let copied = consumed.min(remaining); - let prefix = available - .get(..copied) - .ok_or_else(|| "MCP input prefix exceeded buffered input".to_owned())?; - bytes.extend_from_slice(prefix); - too_large = copied < consumed; - } - reader.consume(consumed); - if newline.is_some() { - break; - } - } - - if bytes.last() == Some(&b'\n') { - let _ = bytes.pop(); - } - if bytes.last() == Some(&b'\r') { - let _ = bytes.pop(); - } - if too_large || bytes.len() > MAX_MESSAGE_BYTES { - return Ok(Some(IncomingLine::TooLarge)); - } - match String::from_utf8(bytes) { - Ok(line) => Ok(Some(IncomingLine::Json(line))), - Err(_) => Ok(Some(IncomingLine::InvalidUtf8)), - } -} - -fn handle_line(line: &str, lifecycle: &mut Lifecycle, status: &F) -> Option -where - F: Fn() -> Result, -{ - let request: Value = match serde_json::from_str(line) { - Ok(value) => value, - Err(error) => { - return Some(error_response( - Value::Null, - -32700, - &format!("invalid JSON: {error}"), - )); - } - }; - handle_message(&request, lifecycle, status) -} - -fn handle_message(request: &Value, lifecycle: &mut Lifecycle, status: &F) -> Option -where - F: Fn() -> Result, -{ - let Some(object) = request.as_object() else { - return Some(error_response( - Value::Null, - -32600, - "request must be an object", - )); - }; - let id = object.get("id").cloned(); - if id - .as_ref() - .is_some_and(|id| !(id.is_string() || id.is_number() || id.is_null())) - { - return Some(error_response(Value::Null, -32600, "invalid request id")); - } - let Some(method) = object.get("method").and_then(Value::as_str) else { - let response_id = id.clone().map_or(Value::Null, std::convert::identity); - return Some(error_response( - response_id, - -32600, - "invalid JSON-RPC request", - )); - }; - if object.get("jsonrpc").and_then(Value::as_str) != Some("2.0") { - let response_id = id.clone().map_or(Value::Null, std::convert::identity); - return Some(error_response( - response_id, - -32600, - "invalid JSON-RPC request", - )); - } - let Some(id) = id else { - handle_notification(method, lifecycle); - return None; - }; - match method { - "initialize" => Some(initialize_response(id, object.get("params"), lifecycle)), - _ if *lifecycle != Lifecycle::Ready => { - Some(error_response(id, -32002, "server is not initialized")) - } - "ping" => Some(success_response(id, json!({}))), - "tools/list" => Some(success_response(id, tools_result())), - "tools/call" => Some(call_tool(id, object.get("params"), status)), - _ => Some(error_response(id, -32601, "method not found")), - } -} - -fn handle_notification(method: &str, lifecycle: &mut Lifecycle) { - if method == "notifications/initialized" && *lifecycle == Lifecycle::AwaitingInitialized { - *lifecycle = Lifecycle::Ready; - } -} - -fn initialize_response(id: Value, params: Option<&Value>, lifecycle: &mut Lifecycle) -> Value { - if *lifecycle != Lifecycle::AwaitingInitialize { - return error_response(id, -32600, "server is already initialized"); - } - let requested = params - .and_then(|params| params.get("protocolVersion")) - .and_then(Value::as_str); - if requested.is_none() { - return error_response(id, -32602, "protocolVersion is required"); - } - // If the client requests an unsupported version, MCP requires the server - // to return a version it does support so the client can decide whether to - // continue or disconnect. - *lifecycle = Lifecycle::AwaitingInitialized; - success_response( - id, - json!({ - "protocolVersion": PROTOCOL_VERSION, - "capabilities": { "tools": { "listChanged": false } }, - "serverInfo": { - "name": "basilisk", - "title": "Basilisk Type Checker", - "version": env!("CARGO_PKG_VERSION"), - "description": "Read-only Basilisk service status" - }, - "instructions": "Use basilisk_typeshed_status to inspect the active standard-library source and its status warnings." - }), - ) -} - -fn tools_result() -> Value { - json!({ - "tools": [{ - "name": STATUS_TOOL, - "title": "Typeshed source status", - "description": "Return the active typeshed source, exact commit/tree identities, licensing state, and ordered warnings.", - "inputSchema": { - "type": "object", - "additionalProperties": false - }, - "outputSchema": status_schema(), - "annotations": { - "readOnlyHint": true, - "destructiveHint": false, - "idempotentHint": true, - // Resolution is offline by construction [STUBRES-TYPESHED-OFFLINE]: - // status never contacts an upstream, so the tool is closed-world. - "openWorldHint": false - }, - "execution": { "taskSupport": "forbidden" } - }] - }) -} - -fn status_schema() -> Value { - json!({ - "type": "object", - "properties": { - "active_source": { - "type": "string", - "enum": ["custom", "exact-commit", "bundled"] - }, - "commit_identity": { - "anyOf": [ - { "type": "string", "pattern": "^[0-9a-f]{40}$" }, - { "type": "null" } - ] - }, - "tree_identity": { - "anyOf": [ - { "type": "string", "pattern": "^[0-9a-f]{40}$" }, - { "type": "null" } - ] - }, - "license_status": { - "type": "string", - "enum": ["approved", "changed", "not supplied"] - }, - "license_reference": { "type": ["string", "null"] }, - "warnings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "code": { "type": "string" }, - "message": { "type": "string" }, - "docs_url": { "type": "string" } - }, - "required": ["code", "message", "docs_url"], - "additionalProperties": false - } - } - }, - "required": [ - "active_source", "commit_identity", "tree_identity", - "license_status", "license_reference", "warnings" - ], - "additionalProperties": false - }) -} - -fn call_tool(id: Value, params: Option<&Value>, status: &F) -> Value -where - F: Fn() -> Result, -{ - let name = params - .and_then(|params| params.get("name")) - .and_then(Value::as_str); - if name != Some(STATUS_TOOL) || !empty_arguments(params) { - return error_response(id, -32602, "unknown tool or invalid arguments"); - } - match status() { - Ok(document) => match serde_json::to_string(&document) { - Ok(text) => success_response( - id, - json!({ - "content": [{ "type": "text", "text": text }], - "structuredContent": document, - "isError": false - }), - ), - Err(error) => error_response(id, -32603, &format!("status encoding failed: {error}")), - }, - Err(error) => success_response( - id, - json!({ - "content": [{ "type": "text", "text": error }], - "isError": true - }), - ), - } -} - -fn empty_arguments(params: Option<&Value>) -> bool { - params - .and_then(|params| params.get("arguments")) - .is_none_or(|arguments| arguments.as_object().is_some_and(serde_json::Map::is_empty)) -} - -fn success_response(id: Value, result: Value) -> Value { - Value::Object(serde_json::Map::from_iter([ - ("jsonrpc".to_owned(), Value::String("2.0".to_owned())), - ("id".to_owned(), id), - ("result".to_owned(), result), - ])) -} - -fn error_response(id: Value, code: i64, message: &str) -> Value { - let error = serde_json::Map::from_iter([ - ("code".to_owned(), Value::Number(code.into())), - ("message".to_owned(), Value::String(message.to_owned())), - ]); - Value::Object(serde_json::Map::from_iter([ - ("jsonrpc".to_owned(), Value::String("2.0".to_owned())), - ("id".to_owned(), id), - ("error".to_owned(), Value::Object(error)), - ])) -} - -/// Resolve the shared runtime status for the MCP tool. -/// -/// This adapter is intentionally the only acquisition dependency in the MCP -/// transport; CLI/LSP/MCP therefore serialize one status model and preserve -/// its warning order. [STUBRES-TYPESHED-WARN] -fn status_for_workspace(workspace: &Path) -> Result { - let mut config = basilisk_lsp::config::load_analysis_config(workspace); - basilisk_lsp::config::apply_uv_typeshed_override(&mut config, workspace); - let request = basilisk_lsp::config::typeshed_request(&config)?; - let manager = basilisk_stubs::typeshed::runtime::production_manager(request); - let status = manager.status().map_err(|error| error.to_string())?; - Ok(status_document(&status)) -} - -/// The active source IS the trust story — custom = user-managed, bundled = -/// build-vetted, exact commit = attested at download and re-proven offline — -/// so there are no separate transport/provenance fields to drift out of sync -/// ([STUBRES-TYPESHED-WARN]). -fn status_document(status: &basilisk_stubs::typeshed::source::TypeshedStatus) -> Value { - let license_status = match status.license_status { - basilisk_stubs::typeshed::source::LicenseStatus::Approved => "approved", - basilisk_stubs::typeshed::source::LicenseStatus::Changed => "changed", - basilisk_stubs::typeshed::source::LicenseStatus::NotSupplied => "not supplied", - }; - let warnings: Vec = status - .warnings - .iter() - .map(|warning| { - json!({ - "code": warning.code.as_str(), - "message": warning.message.as_str(), - "docs_url": warning.docs_url.as_str(), - }) - }) - .collect(); - let commit = status.commit.map(|identity| identity.to_hex()); - let tree = status.tree.map(|identity| identity.to_hex()); - json!({ - "active_source": status.active_source.as_str(), - "commit_identity": commit.as_deref(), - "tree_identity": tree.as_deref(), - "license_status": license_status, - "license_reference": status.license_reference.as_deref(), - "warnings": warnings - }) -} - -#[cfg(test)] -#[path = "mcp/tests.rs"] -mod tests; diff --git a/crates/basilisk-cli/src/mcp/tests.rs b/crates/basilisk-cli/src/mcp/tests.rs deleted file mode 100644 index 70bb060a1..000000000 --- a/crates/basilisk-cli/src/mcp/tests.rs +++ /dev/null @@ -1,445 +0,0 @@ -use super::*; - -fn status() -> Value { - json!({ - "active_source": "bundled", - "commit_identity": "0123456789012345678901234567890123456789", - "tree_identity": "abcdefabcdefabcdefabcdefabcdefabcdefabcd", - "license_status": "approved", - "license_reference": "typeshed://LICENSE", - "warnings": [ - { "code": "typeshed_source_unpinned", "message": "Pin a commit to make this reproducible", "docs_url": "https://www.basilisk-python.dev/errors/typeshed_source_unpinned" }, - { "code": "typeshed_source_license_changed", "message": "Basilisk update/review required", "docs_url": "https://www.basilisk-python.dev/errors/typeshed_source_license_changed" }, - { "code": "typeshed_source_user_managed", "message": "Folder supplies its own license", "docs_url": "https://www.basilisk-python.dev/errors/typeshed_source_user_managed" } - ] - }) -} - -fn exchange(messages: &[Value]) -> Result, String> { - exchange_with_status(messages, || Ok(status())) -} - -fn exchange_with_status(messages: &[Value], status: F) -> Result, String> -where - F: Fn() -> Result, -{ - let input = messages - .iter() - .map(serde_json::to_string) - .collect::, _>>() - .map_err(|error| error.to_string())? - .join("\n"); - let mut output = Vec::new(); - run_transport(std::io::Cursor::new(input), &mut output, status)?; - String::from_utf8(output) - .map_err(|error| error.to_string())? - .lines() - .map(|line| serde_json::from_str(line).map_err(|error| error.to_string())) - .collect() -} - -#[test] -fn lifecycle_lists_and_calls_structured_status() -> Result<(), String> { - let responses = exchange(&[ - json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":PROTOCOL_VERSION,"capabilities":{},"clientInfo":{"name":"test","version":"1"}}}), - json!({"jsonrpc":"2.0","method":"notifications/initialized"}), - json!({"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}), - json!({"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":STATUS_TOOL,"arguments":{}}}), - ])?; - assert_eq!(responses.len(), 3); - assert_eq!( - responses - .first() - .and_then(|value| value.pointer("/result/protocolVersion")) - .and_then(Value::as_str), - Some(PROTOCOL_VERSION) - ); - assert_eq!( - responses - .get(1) - .and_then(|value| value.pointer("/result/tools/0/name")) - .and_then(Value::as_str), - Some(STATUS_TOOL) - ); - let call = responses - .get(2) - .ok_or_else(|| "tool response missing".to_owned())?; - let document = call - .pointer("/result/structuredContent") - .ok_or_else(|| "structured status missing".to_owned())?; - let text = call - .pointer("/result/content/0/text") - .and_then(Value::as_str) - .ok_or_else(|| "text status missing".to_owned())?; - let text_document: Value = serde_json::from_str(text).map_err(|error| error.to_string())?; - assert_eq!(text_document, *document); - let warnings = document - .get("warnings") - .and_then(Value::as_array) - .ok_or_else(|| "structured warnings missing".to_owned())?; - assert_eq!( - warnings - .first() - .and_then(|warning| warning.get("code")) - .and_then(Value::as_str), - Some("typeshed_source_unpinned") - ); - assert_eq!( - warnings - .first() - .and_then(|warning| warning.get("docs_url")) - .and_then(Value::as_str), - Some("https://www.basilisk-python.dev/errors/typeshed_source_unpinned") - ); - assert_eq!( - warnings - .get(1) - .and_then(|warning| warning.get("code")) - .and_then(Value::as_str), - Some("typeshed_source_license_changed") - ); - assert_eq!( - warnings - .get(2) - .and_then(|warning| warning.get("code")) - .and_then(Value::as_str), - Some("typeshed_source_user_managed") - ); - Ok(()) -} - -#[test] -fn tool_contract_declares_closed_output_and_honest_annotations() { - let result = tools_result(); - assert_eq!( - result - .pointer("/tools/0/inputSchema/additionalProperties") - .and_then(Value::as_bool), - Some(false) - ); - assert_eq!( - result - .pointer("/tools/0/outputSchema/additionalProperties") - .and_then(Value::as_bool), - Some(false) - ); - assert_eq!( - result - .pointer("/tools/0/outputSchema/properties/commit_identity/anyOf/0/pattern") - .and_then(Value::as_str), - Some("^[0-9a-f]{40}$") - ); - assert_eq!( - result - .pointer("/tools/0/outputSchema/properties/active_source/enum/0") - .and_then(Value::as_str), - Some("custom") - ); - assert_eq!( - result - .pointer("/tools/0/outputSchema/properties/license_status/enum/2") - .and_then(Value::as_str), - Some("not supplied") - ); - assert!( - result - .pointer("/tools/0/outputSchema/properties/transport") - .is_none(), - "the closed envelope must not resurrect the removed transport field" - ); - assert!( - result - .pointer("/tools/0/outputSchema/properties/signed_release") - .is_none(), - "the closed envelope must not resurrect the removed signed_release field" - ); - assert_eq!( - result - .pointer("/tools/0/annotations/readOnlyHint") - .and_then(Value::as_bool), - Some(true) - ); - assert_eq!( - result - .pointer("/tools/0/annotations/openWorldHint") - .and_then(Value::as_bool), - Some(false), - "status resolution is offline by construction [STUBRES-TYPESHED-OFFLINE] — \ - the tool must declare itself closed-world" - ); -} - -#[test] -fn acquisition_failure_is_a_tool_error_without_partial_status() -> Result<(), String> { - let responses = exchange_with_status( - &[ - json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":PROTOCOL_VERSION}}), - json!({"jsonrpc":"2.0","method":"notifications/initialized"}), - json!({"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":STATUS_TOOL,"arguments":{}}}), - ], - || Err("custom typeshed failed without fallback".to_owned()), - )?; - let call = responses - .get(1) - .ok_or_else(|| "tool error response missing".to_owned())?; - assert_eq!( - call.pointer("/result/isError").and_then(Value::as_bool), - Some(true) - ); - assert!(call.pointer("/result/structuredContent").is_none()); - assert!(call.pointer("/error").is_none()); - Ok(()) -} - -#[test] -fn shared_custom_status_projects_to_the_closed_mcp_envelope() { - use basilisk_stubs::typeshed::source::{ - LicenseStatus, SourceKind, StatusWarning, TypeshedStatus, - }; - use basilisk_stubs::typeshed::warning::{TypeshedWarning, UnpinnedKind}; - - let shared = TypeshedStatus { - active_source: SourceKind::Custom, - commit: None, - tree: None, - license_status: LicenseStatus::NotSupplied, - license_reference: None, - warnings: StatusWarning::list(&[ - TypeshedWarning::UserManaged, - TypeshedWarning::Unpinned(UnpinnedKind::CustomFolder), - ]), - }; - let document = status_document(&shared); - assert_eq!( - document.get("active_source").and_then(Value::as_str), - Some("custom") - ); - assert_eq!( - document.get("license_status").and_then(Value::as_str), - Some("not supplied") - ); - assert!( - document.get("transport").is_none(), - "active_source IS the trust story — no transport field may reappear" - ); - assert!( - document.get("provenance").is_none(), - "active_source IS the trust story — no provenance field may reappear" - ); - assert!( - document.get("signed_release").is_none(), - "active_source IS the trust story — no signed_release field may reappear" - ); - assert!(document.pointer("/warnings/0/severity").is_none()); - assert_eq!( - document.pointer("/warnings/0/code").and_then(Value::as_str), - Some("typeshed_source_unpinned") - ); - assert_eq!( - document - .pointer("/warnings/0/docs_url") - .and_then(Value::as_str), - Some("https://www.basilisk-python.dev/errors/typeshed_source_unpinned") - ); -} - -#[test] -fn shared_oid_type_rejects_truncated_git_identity() { - assert_eq!( - basilisk_stubs::typeshed::gittree::Oid::from_hex("83c2518").ok(), - None, - "a truncated SHA is never a valid object identity" - ); -} - -#[test] -fn malformed_and_pre_initialization_requests_are_protocol_errors() -> Result<(), String> { - let mut lifecycle = Lifecycle::AwaitingInitialize; - let parse = handle_line("not-json", &mut lifecycle, &|| Ok(status())) - .ok_or_else(|| "parse error response missing".to_owned())?; - assert_eq!( - parse.pointer("/error/code").and_then(Value::as_i64), - Some(-32700) - ); - let early = handle_line( - &serde_json::to_string(&json!({"jsonrpc":"2.0","id":"early","method":"tools/list"})) - .map_err(|error| error.to_string())?, - &mut lifecycle, - &|| Ok(status()), - ) - .ok_or_else(|| "initialization error response missing".to_owned())?; - assert_eq!( - early.pointer("/error/code").and_then(Value::as_i64), - Some(-32002) - ); - Ok(()) -} - -#[test] -fn negotiation_and_notification_order_follow_lifecycle() -> Result<(), String> { - let mut lifecycle = Lifecycle::AwaitingInitialize; - let _ = handle_line( - r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#, - &mut lifecycle, - &|| Ok(status()), - ); - assert_eq!(lifecycle, Lifecycle::AwaitingInitialize); - - let response = handle_line( - r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2099-01-01"}}"#, - &mut lifecycle, - &|| Ok(status()), - ) - .ok_or_else(|| "initialize response missing".to_owned())?; - assert_eq!( - response - .pointer("/result/protocolVersion") - .and_then(Value::as_str), - Some(PROTOCOL_VERSION) - ); - assert_eq!(lifecycle, Lifecycle::AwaitingInitialized); - Ok(()) -} - -/// [MCP-STDIO]: every malformed request shape gets the prescribed JSON-RPC -/// error — nothing is silently dropped and nothing kills the session. -#[test] -fn malformed_request_shapes_each_get_the_prescribed_error() -> Result<(), String> { - let responses = exchange(&[ - json!([1, 2, 3]), - json!({"jsonrpc":"2.0","id":true,"method":"ping"}), - json!({"jsonrpc":"2.0","id":4}), - json!({"jsonrpc":"1.0","id":5,"method":"ping"}), - ])?; - let codes: Vec> = responses - .iter() - .map(|response| response.pointer("/error/code").and_then(Value::as_i64)) - .collect(); - assert_eq!(codes, vec![Some(-32600); 4]); - Ok(()) -} - -/// [MCP-STDIO]: lifecycle guards — initialize without a protocol version, -/// re-initialize, unknown methods, unknown tools, and ping. -#[test] -fn lifecycle_guards_cover_reinit_unknown_methods_and_bad_tools() -> Result<(), String> { - let responses = exchange(&[ - json!({"jsonrpc":"2.0","id":0,"method":"initialize","params":{}}), - json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":PROTOCOL_VERSION,"capabilities":{},"clientInfo":{"name":"t","version":"1"}}}), - json!({"jsonrpc":"2.0","method":"notifications/initialized"}), - json!({"jsonrpc":"2.0","id":2,"method":"ping"}), - json!({"jsonrpc":"2.0","id":3,"method":"resources/list"}), - json!({"jsonrpc":"2.0","id":4,"method":"initialize","params":{"protocolVersion":PROTOCOL_VERSION}}), - json!({"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"unknown_tool","arguments":{}}}), - ])?; - let codes: Vec> = responses - .iter() - .map(|response| response.pointer("/error/code").and_then(Value::as_i64)) - .collect(); - assert_eq!( - codes, - vec![ - Some(-32602), - None, - None, - Some(-32601), - Some(-32600), - Some(-32602) - ] - ); - assert_eq!( - responses - .get(2) - .and_then(|response| response.pointer("/result")), - Some(&json!({})), - "ping must answer with an empty result" - ); - Ok(()) -} - -/// [MCP-STDIO]: a line that is not UTF-8 is a parse error, and the session -/// keeps serving afterwards. -#[test] -fn invalid_utf8_input_is_a_parse_error_and_the_session_survives() -> Result<(), String> { - let mut input: Vec = vec![0xFF, 0xFE, b'\n']; - input.extend_from_slice( - br#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25"}}"#, - ); - let mut output = Vec::new(); - run_transport(std::io::Cursor::new(input), &mut output, || Ok(status()))?; - let responses = String::from_utf8(output) - .map_err(|error| error.to_string())? - .lines() - .map(|line| serde_json::from_str::(line).map_err(|error| error.to_string())) - .collect::, _>>()?; - assert_eq!( - responses - .first() - .and_then(|response| response.pointer("/error/code")) - .and_then(Value::as_i64), - Some(-32700) - ); - assert!( - responses - .get(1) - .and_then(|response| response.pointer("/result/protocolVersion")) - .is_some(), - "the session must keep serving after a non-UTF-8 line" - ); - Ok(()) -} - -/// [STUBRES-TYPESHED-WARN]: every license state projects to its wire word. -#[test] -fn status_document_maps_every_license_state() { - use basilisk_stubs::typeshed::source::{LicenseStatus, SourceKind, TypeshedStatus}; - for (state, expected) in [ - (LicenseStatus::Approved, "approved"), - (LicenseStatus::Changed, "changed"), - (LicenseStatus::NotSupplied, "not supplied"), - ] { - let document = status_document(&TypeshedStatus { - active_source: SourceKind::Bundled, - commit: None, - tree: None, - license_status: state, - license_reference: None, - warnings: Vec::new(), - }); - assert_eq!( - document.get("license_status").and_then(Value::as_str), - Some(expected) - ); - } -} - -#[test] -fn oversized_line_is_drained_before_the_next_request() -> Result<(), String> { - let mut input = vec![b' '; MAX_MESSAGE_BYTES + 1]; - input.push(b'\n'); - input.extend_from_slice( - br#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25"}}"#, - ); - let mut output = Vec::new(); - run_transport(std::io::Cursor::new(input), &mut output, || Ok(status()))?; - let responses = String::from_utf8(output) - .map_err(|error| error.to_string())? - .lines() - .map(|line| serde_json::from_str::(line).map_err(|error| error.to_string())) - .collect::, _>>()?; - assert_eq!( - responses - .first() - .and_then(|response| response.pointer("/error/code")) - .and_then(Value::as_i64), - Some(-32600) - ); - assert_eq!( - responses - .get(1) - .and_then(|response| response.pointer("/result/protocolVersion")) - .and_then(Value::as_str), - Some(PROTOCOL_VERSION) - ); - Ok(()) -} diff --git a/crates/basilisk-cli/src/output/json.rs b/crates/basilisk-cli/src/output/json.rs deleted file mode 100644 index d795622da..000000000 --- a/crates/basilisk-cli/src/output/json.rs +++ /dev/null @@ -1,120 +0,0 @@ -//! Implements [CHKARCH-CLI]. See docs/specs/CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-CLI -//! Machine-readable JSON output for diagnostics. -//! -//! JSON output is a flat array consumed by the VS Code extension: -//! ```json -//! [ -//! { -//! "code": "BSK-0001", -//! "severity": "error", -//! "message": "Missing parameter type annotation for `x`", -//! "path": "src/utils.py", -//! "line": 1, -//! "col": 9, -//! "end_line": 1, -//! "end_col": 10 -//! } -//! ] -//! ``` -//! -//! Implements [CHKARCH-CLI-OUTPUT-FAILURES]. See -//! docs/specs/CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-CLI-OUTPUT-FAILURES -//! A file the run could not read at all is reported in the same array with a -//! `null` code, because no rule produced it. Leaving it out rendered `[]` — the -//! answer a clean file gets — for a file that was never checked, so every -//! consumer that reads the report rather than the exit status was told a file -//! with a syntax error had no problems. - -use serde::Serialize; - -use basilisk_checker::Diagnostic; - -use super::FileSource; - -/// Serialisable form of a single diagnostic for JSON output. -#[derive(Serialize)] -pub(super) struct JsonDiagnostic<'a> { - /// The diagnostic error/warning code (e.g. `BSK-0001`), or `None` for a - /// file the run could not analyse — no rule ran, so none can be named. - pub(super) code: Option<&'a str>, - /// Severity string: `"error"`, `"warning"`, `"info"`, or `"safety violation"`. - pub(super) severity: &'a str, - /// Human-readable diagnostic message. - pub(super) message: &'a str, - /// Path to the file containing the diagnostic. - pub(super) path: &'a str, - /// 1-based line number of the start of the span. - pub(super) line: usize, - /// 1-based column number of the start of the span. - pub(super) col: usize, - /// 1-based line number of the end of the span. - pub(super) end_line: usize, - /// 1-based column number of the end of the span (exclusive). - pub(super) end_col: usize, -} - -/// A file the run could not analyse at all, rendered alongside the diagnostics. -pub struct JsonFailure<'a> { - /// Path of the file that could not be analysed. - pub path: &'a str, - /// Why it could not be analysed, as the parser or reader reported it. - pub message: &'a str, -} - -/// Render every diagnostic, and every file that failed outright, to stdout. -pub fn render_diagnostics_json( - diagnostics: &[Diagnostic], - sources: &[FileSource], - failures: &[JsonFailure<'_>], -) { - // One line index per source, reused for every diagnostic in that file — the - // span→line/col conversions become O(log n) instead of prefix rescans. - let indexes = super::SourceIndexes::new(sources); - let items: Vec> = diagnostics - .iter() - .map(|d| { - let index = indexes.for_path(&d.path).map(|(_, index)| index); - let (line, col) = index.map_or((1, 1), |index| index.line_col(d.span.start_usize())); - let (end_line, end_col) = - index.map_or((line, col + 1), |index| index.line_col(d.span.end_usize())); - JsonDiagnostic { - code: Some(d.code.code), - severity: match d.severity { - basilisk_checker::Severity::Error => "error", - basilisk_checker::Severity::Warning => "warning", - basilisk_checker::Severity::Info => "info", - basilisk_checker::Severity::SafetyViolation => "safety violation", - }, - message: &d.message, - path: &d.path, - line, - col, - end_line, - end_col, - } - }) - .chain(failures.iter().map(failure_entry)) - .collect(); - - match serde_json::to_string_pretty(&items) { - Ok(json) => println!("{json}"), - Err(e) => eprintln!("basilisk: failed to serialize diagnostics: {e}"), - } -} - -/// One unanalysable file as a JSON entry. -/// -/// The location is the start of the file: the failure is about the file as a -/// whole, and the parser's own message carries whatever position it knows. -pub(super) fn failure_entry<'a>(failure: &'a JsonFailure<'a>) -> JsonDiagnostic<'a> { - JsonDiagnostic { - code: None, - severity: "error", - message: failure.message, - path: failure.path, - line: 1, - col: 1, - end_line: 1, - end_col: 1, - } -} diff --git a/crates/basilisk-cli/src/output/mod.rs b/crates/basilisk-cli/src/output/mod.rs deleted file mode 100644 index 37ce52ca3..000000000 --- a/crates/basilisk-cli/src/output/mod.rs +++ /dev/null @@ -1,1012 +0,0 @@ -//! Diagnostic output rendering — rustc-style text and machine-readable JSON. -//! -//! Text example: -//! ```text -//! error[BSK-0001]: Missing parameter type annotation for `data` -//! --> src/utils.py:14:5 -//! | -//! 14 | def process(data): -//! | ^^^^ parameter `data` has no type annotation -//! | -//! = help: Add a type annotation: `data: ` -//! = note: In Basilisk, all function parameters require explicit types -//! = see: https://www.basilisk-python.dev/errors/BSK-0001 -//! ``` -//! -//! JSON output is a flat array consumed by the VS Code extension: -//! ```json -//! [ -//! { -//! "code": "BSK-0001", -//! "severity": "error", -//! "message": "Missing parameter type annotation for `x`", -//! "path": "src/utils.py", -//! "line": 1, -//! "col": 9, -//! "end_line": 1, -//! "end_col": 10 -//! } -//! ] -//! ``` - -use clap::ValueEnum; - -mod json; -mod text; - -pub use json::{render_diagnostics_json, JsonFailure}; -pub use text::render_diagnostics; - -/// Output format for the `check` subcommand. -/// -/// Implements [CHKARCH-CLI-OUTPUT]: only the `text` (default) and `json` -/// variants exist. The spec also lists `sarif` and `junit`; those are not yet -/// implemented (see report). -/// See docs/specs/CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-CLI-OUTPUT -#[derive(Clone, Copy, Debug, ValueEnum)] -pub enum OutputFormat { - /// Human-readable rustc-style text (default). - Text, - /// Machine-readable JSON array consumed by the VS Code extension. - Json, -} - -/// Terminal colour mode. -#[derive(Clone, Copy, Debug, ValueEnum)] -pub enum ColorMode { - /// Detect automatically (colours when stdout is a terminal). - Auto, - /// Always emit ANSI colour codes. - Always, - /// Never emit ANSI colour codes. - Never, -} - -impl ColorMode { - /// Configure the `colored` crate based on the chosen mode. - pub fn apply(self) { - match self { - Self::Auto => {} // `colored` auto-detects by default - Self::Always => colored::control::set_override(true), - Self::Never => colored::control::set_override(false), - } - } -} - -/// Associates a file path with its source text for span-to-line-col mapping. -pub struct FileSource { - /// The file path. - pub path: String, - /// The full source text. - pub text: String, -} - -/// One precomputed [`LineIndex`](basilisk_common::text::LineIndex) per source -/// file, so a batch of diagnostics resolves its byte spans to `(line, col)` in -/// O(log n) instead of rescanning the source from the top for every span. -/// -/// Rendering a file with many diagnostics used to be O(diagnostics · length) — -/// each `--> path:line:col` and each snippet rescanned the whole prefix. Both -/// the text and JSON renderers now build this once and share it. -pub(super) struct SourceIndexes<'a> { - entries: Vec<(&'a FileSource, basilisk_common::text::LineIndex)>, -} - -impl<'a> SourceIndexes<'a> { - /// Build a line index for every source file up front (one O(n) pass each). - pub(super) fn new(sources: &'a [FileSource]) -> Self { - Self { - entries: sources - .iter() - .map(|source| (source, basilisk_common::text::LineIndex::new(&source.text))) - .collect(), - } - } - - /// Source text plus its line index for the file `path` belongs to, if known. - /// - /// Linear scan over the file list, matching the renderers' prior lookup — the - /// win is in the per-span conversion, not this (files-per-run is small). - pub(super) fn for_path(&self, path: &str) -> Option<(&str, &basilisk_common::text::LineIndex)> { - self.entries - .iter() - .find(|(source, _)| source.path == path) - .map(|(source, index)| (source.text.as_str(), index)) - } -} - -#[cfg(test)] -#[expect( - clippy::indexing_slicing, - reason = "test-only code: indexing acceptable in unit tests" -)] -mod tests { - use super::*; - use json::JsonDiagnostic; - use text::{byte_offset_to_line_col, format_one, format_snippet}; - - use basilisk_checker::Diagnostic; - use basilisk_checker::{ErrorCode, Severity}; - use basilisk_resolver::Span; - - /// Format one diagnostic against a fresh line index for `text`. - /// - /// The production renderer builds the index once per file and threads - /// `(text, &LineIndex)` into `format_one`; this wrapper rebuilds it per call - /// so the focused formatting tests stay terse. - fn render_one(diag: &Diagnostic, text: &str) -> String { - let index = basilisk_common::text::LineIndex::new(text); - format_one(diag, Some((text, &index))) - } - - /// Format a snippet for a span against a fresh line index for `text`. - fn render_snippet(text: &str, start: usize, end: usize, severity: Severity) -> String { - let index = basilisk_common::text::LineIndex::new(text); - format_snippet(text, &index, start, end, severity) - } - - // ── ANSI escape sequences produced by the `colored` crate ──────────────── - - /// Bold red (used for error severity labels and underlines). - const BOLD_RED: &str = "\x1b[1;31m"; - /// Bold yellow (used for warning severity labels and underlines). - const BOLD_YELLOW: &str = "\x1b[1;33m"; - /// Bold blue (used for info labels, line numbers, pipes, arrows). - const BOLD_BLUE: &str = "\x1b[1;34m"; - /// Bold cyan (used for help/note/see annotation labels). - const BOLD_CYAN: &str = "\x1b[1;36m"; - /// Bold (used for error codes and messages). - const BOLD: &str = "\x1b[1m"; - /// ANSI reset sequence. - const RESET: &str = "\x1b[0m"; - - /// Force colours on for the duration of a test. - /// - /// `colored` uses a global atomic; we force it to `true` so that - /// `format_one` / `format_snippet` always emit ANSI codes regardless - /// of whether the test runner's stdout is a TTY. - fn force_colors() { - colored::control::set_override(true); - } - - fn make_diag(help: Option<&str>, note: Option<&str>) -> Diagnostic { - make_diag_with_severity(Severity::Error, help, note) - } - - fn make_diag_with_severity( - severity: Severity, - help: Option<&str>, - note: Option<&str>, - ) -> Diagnostic { - Diagnostic { - code: ErrorCode { - code: "BSK-0001", - docs_url: "https://www.basilisk-python.dev/errors/BSK-0001", - }, - severity, - message: "missing annotation for `x`".to_owned(), - span: Span { start: 8, end: 9 }, - path: "test.py".to_owned(), - help: help.map(|value| value.to_owned().into()), - note: note.map(|value| value.to_owned().into()), - provenance: None, - } - } - - #[test] - fn render_diagnostics_counts_only_errors() { - let diag = make_diag(Some("add a type"), Some("all params need types")); - let sources = vec![FileSource { - path: "test.py".to_owned(), - text: "def foo(x): pass".to_owned(), - }]; - let count = render_diagnostics(&[diag], &sources); - assert_eq!(count, 1); - } - - #[test] - fn render_diagnostics_does_not_count_warnings_as_errors() { - let warning = make_diag_with_severity(Severity::Warning, None, None); - let error = make_diag(None, None); - let sources = vec![FileSource { - path: "test.py".to_owned(), - text: "def foo(x): pass".to_owned(), - }]; - let count = render_diagnostics(&[warning, error], &sources); - assert_eq!(count, 1, "only errors should be counted, not warnings"); - } - - // ── format_one: colour assertions ──────────────────────────────────────── - - #[test] - fn format_one_error_header_is_bold_red() { - force_colors(); - let diag = make_diag(Some("help"), Some("note")); - let out = render_one(&diag, "def foo(x): pass"); - assert!( - out.contains(&format!("{BOLD_RED}error{RESET}")), - "error label must be bold red, got:\n{out}" - ); - } - - #[test] - fn format_one_error_code_is_bold() { - force_colors(); - let diag = make_diag(None, None); - let out = render_one(&diag, "def foo(x): pass"); - assert!( - out.contains(&format!("{BOLD}[BSK-0001]{RESET}")), - "error code must be bold, got:\n{out}" - ); - } - - #[test] - fn format_one_message_is_bold() { - force_colors(); - let diag = make_diag(None, None); - let out = render_one(&diag, "def foo(x): pass"); - assert!( - out.contains(&format!("{BOLD}missing annotation for `x`{RESET}")), - "message must be bold, got:\n{out}" - ); - } - - #[test] - fn format_one_arrow_is_bold_blue() { - force_colors(); - let diag = make_diag(None, None); - let out = render_one(&diag, "def foo(x): pass"); - assert!( - out.contains(&format!("{BOLD_BLUE}-->{RESET}")), - "arrow must be bold blue, got:\n{out}" - ); - } - - #[test] - fn format_one_help_label_is_bold_cyan() { - force_colors(); - let diag = make_diag(Some("add a type"), None); - let out = render_one(&diag, "def foo(x): pass"); - assert!( - out.contains(&format!("{BOLD_CYAN}help{RESET}")), - "help label must be bold cyan, got:\n{out}" - ); - } - - #[test] - fn format_one_note_label_is_bold_cyan() { - force_colors(); - let diag = make_diag(None, Some("all params need types")); - let out = render_one(&diag, "def foo(x): pass"); - assert!( - out.contains(&format!("{BOLD_CYAN}note{RESET}")), - "note label must be bold cyan, got:\n{out}" - ); - } - - #[test] - fn format_one_see_label_is_bold_cyan() { - force_colors(); - let diag = make_diag(None, None); - let out = render_one(&diag, "def foo(x): pass"); - assert!( - out.contains(&format!("{BOLD_CYAN}see{RESET}")), - "see label must be bold cyan, got:\n{out}" - ); - } - - #[test] - fn format_one_equals_sign_is_bold_blue() { - force_colors(); - let diag = make_diag(Some("help"), None); - let out = render_one(&diag, "def foo(x): pass"); - assert!( - out.contains(&format!("{BOLD_BLUE}={RESET}")), - "equals sign must be bold blue, got:\n{out}" - ); - } - - #[test] - fn format_one_warning_header_is_bold_yellow() { - force_colors(); - let diag = make_diag_with_severity(Severity::Warning, None, None); - let out = render_one(&diag, "def foo(x): pass"); - assert!( - out.contains(&format!("{BOLD_YELLOW}warning{RESET}")), - "warning label must be bold yellow, got:\n{out}" - ); - } - - #[test] - fn format_one_info_header_is_bold_blue() { - force_colors(); - let diag = make_diag_with_severity(Severity::Info, None, None); - let out = render_one(&diag, "def foo(x): pass"); - assert!( - out.contains(&format!("{BOLD_BLUE}info{RESET}")), - "info label must be bold blue, got:\n{out}" - ); - } - - #[test] - fn format_one_safety_violation_header_is_bold_red() { - force_colors(); - let diag = make_diag_with_severity(Severity::SafetyViolation, None, None); - let out = render_one(&diag, "def foo(x): pass"); - assert!( - out.contains(&format!("{BOLD_RED}safety violation{RESET}")), - "safety violation label must be bold red, got:\n{out}" - ); - } - - #[test] - fn format_one_without_source_falls_back_to_path() { - force_colors(); - let diag = make_diag(Some("help"), Some("note")); - let out = format_one(&diag, None); - assert!( - out.contains("test.py"), - "must fall back to path when source is None" - ); - // No snippet section when source is missing. - assert!( - !out.contains("def foo"), - "must not contain source snippet when source is None" - ); - } - - #[test] - fn format_one_without_help_omits_help_line() { - force_colors(); - let diag = make_diag(None, Some("note text")); - let out = render_one(&diag, "def foo(x): pass"); - assert!( - !out.contains("help"), - "must omit help line when help is None" - ); - assert!(out.contains("note text"), "must include note text"); - } - - #[test] - fn format_one_without_note_omits_note_line() { - force_colors(); - let diag = make_diag(Some("help text"), None); - let out = render_one(&diag, "def foo(x): pass"); - assert!(out.contains("help text"), "must include help text"); - assert!( - !out.contains("note"), - "must omit note line when note is None" - ); - } - - #[test] - fn format_one_without_help_or_note() { - force_colors(); - let diag = make_diag(None, None); - let out = render_one(&diag, "def foo(x): pass"); - assert!(!out.contains("help"), "must omit help when None"); - assert!(!out.contains("note"), "must omit note when None"); - // Still must contain the see URL. - assert!(out.contains("BSK-0001"), "must contain error code"); - assert!(out.contains("basilisk-python.dev"), "must contain docs URL"); - } - - // ── format_snippet: colour assertions ──────────────────────────────────── - - /// Format the standard sample snippet under forced colour mode. - /// All `format_snippet_*_is_*` tests share this fixture. - fn snippet_sample(severity: Severity) -> String { - force_colors(); - render_snippet("def foo(x): pass", 8, 9, severity) - } - - fn assert_contains_colour(out: &str, expected: &str, label: &str) { - assert!( - out.contains(expected), - "{label} must contain {expected:?}, got:\n{out}" - ); - } - - #[test] - fn format_snippet_pipe_is_bold_blue() { - let out = snippet_sample(Severity::Error); - assert_contains_colour(&out, &format!("{BOLD_BLUE}|{RESET}"), "pipe"); - } - - #[test] - fn format_snippet_line_number_is_bold_blue() { - let out = snippet_sample(Severity::Error); - assert_contains_colour(&out, &format!("{BOLD_BLUE}1{RESET}"), "line number"); - } - - #[test] - fn format_snippet_error_underline_is_bold_red() { - let out = snippet_sample(Severity::Error); - assert_contains_colour(&out, &format!("{BOLD_RED}^{RESET}"), "error underline"); - } - - #[test] - fn format_snippet_warning_underline_is_bold_yellow() { - let out = snippet_sample(Severity::Warning); - assert_contains_colour(&out, &format!("{BOLD_YELLOW}^{RESET}"), "warning underline"); - } - - #[test] - fn format_snippet_info_underline_is_bold_blue() { - let out = snippet_sample(Severity::Info); - assert_contains_colour(&out, &format!("{BOLD_BLUE}^{RESET}"), "info underline"); - } - - #[test] - fn format_snippet_safety_violation_underline_is_bold_red() { - let out = snippet_sample(Severity::SafetyViolation); - assert_contains_colour( - &out, - &format!("{BOLD_RED}^{RESET}"), - "safety violation underline", - ); - } - - #[test] - fn format_snippet_multi_char_underline_length() { - force_colors(); - // span covers "foo" at bytes 4..7 → 3 carets - let out = render_snippet("def foo(x): pass", 4, 7, Severity::Error); - assert_contains_colour(&out, &format!("{BOLD_RED}^^^{RESET}"), "3-caret underline"); - } - - #[test] - fn format_snippet_contains_source_line() { - force_colors(); - let out = render_snippet("def foo(x): pass", 8, 9, Severity::Error); - assert!( - out.contains("def foo(x): pass"), - "snippet must contain the source line, got:\n{out}" - ); - } - - #[test] - fn format_snippet_on_second_line() { - force_colors(); - let source = "def foo(): pass\ndef bar(x): pass"; - let out = render_snippet(source, 20, 23, Severity::Error); - // Line 2 contains "def bar(x): pass", line number should be 2. - assert!( - out.contains(&format!("{BOLD_BLUE}2{RESET}")), - "second-line snippet must show line number 2, got:\n{out}" - ); - assert!( - out.contains("def bar(x): pass"), - "must contain second source line, got:\n{out}" - ); - } - - // ── format_one: structural content assertions ──────────────────────────── - - #[test] - fn format_one_contains_file_location() { - force_colors(); - let diag = make_diag(None, None); - let out = render_one(&diag, "def foo(x): pass"); - assert!( - out.contains("test.py:1:9"), - "must contain file:line:col location, got:\n{out}" - ); - } - - #[test] - fn format_one_contains_source_snippet() { - force_colors(); - let diag = make_diag(None, None); - let out = render_one(&diag, "def foo(x): pass"); - assert!( - out.contains("def foo(x): pass"), - "must contain source snippet, got:\n{out}" - ); - } - - #[test] - fn format_one_contains_docs_url() { - force_colors(); - let diag = make_diag(None, None); - let out = render_one(&diag, "def foo(x): pass"); - assert!( - out.contains("https://www.basilisk-python.dev/errors/BSK-0001"), - "must contain docs URL, got:\n{out}" - ); - } - - // ── byte_offset_to_line_col ────────────────────────────────────────────── - - #[test] - fn byte_offset_to_line_col_second_line() { - let source = "def foo(): pass\ndef bar(x): pass"; - // byte 16 is the 'd' starting "def bar" - let (line, col) = byte_offset_to_line_col(source, 16); - assert_eq!(line, 2); - assert_eq!(col, 1); - } - - // ── JSON output ─────────────────────────────────────────────────────────── - - #[test] - fn json_produces_valid_array_with_correct_fields() -> Result<(), Box> { - let source = "def foo(x): pass"; - // byte 8 = 'x', so line=1, col=9 (1-based) - let (line, col) = byte_offset_to_line_col(source, 8); - let (end_line, end_col) = byte_offset_to_line_col(source, 9); - let item = JsonDiagnostic { - code: Some("BSK-0001"), - severity: "error", - message: "missing annotation for `x`", - path: "test.py", - line, - col, - end_line, - end_col, - }; - let json = serde_json::to_string(&item)?; - assert!(json.contains("BSK-0001")); - assert!(json.contains("\"line\":1")); - assert!(json.contains("\"col\":9")); - assert!(json.contains("\"end_line\":1")); - assert!(json.contains("\"end_col\":10")); - Ok(()) - } - - #[test] - fn json_empty_diagnostics_produces_empty_array() -> Result<(), Box> { - let items: Vec> = vec![]; - let json = serde_json::to_string(&items)?; - assert_eq!(json, "[]"); - Ok(()) - } - - #[test] - fn render_diagnostics_json_smoke_test() { - let diag = make_diag(None, None); - let sources = vec![FileSource { - path: "test.py".to_owned(), - text: "def foo(x): pass".to_owned(), - }]; - // Just verify it doesn't panic. - render_diagnostics_json(&[diag], &sources, &[]); - } - - #[test] - fn render_diagnostics_json_empty_is_safe() { - render_diagnostics_json(&[], &[], &[]); - } - - #[test] - fn render_diagnostics_json_warning_severity() { - let diag = make_diag_with_severity(Severity::Warning, None, None); - let sources = vec![FileSource { - path: "test.py".to_owned(), - text: "def foo(x): pass".to_owned(), - }]; - render_diagnostics_json(&[diag], &sources, &[]); - } - - #[test] - fn render_diagnostics_json_info_severity() { - let diag = make_diag_with_severity(Severity::Info, None, None); - let sources = vec![FileSource { - path: "test.py".to_owned(), - text: "def foo(x): pass".to_owned(), - }]; - render_diagnostics_json(&[diag], &sources, &[]); - } - - #[test] - fn render_diagnostics_json_safety_violation_severity() { - let diag = make_diag_with_severity(Severity::SafetyViolation, None, None); - let sources = vec![FileSource { - path: "test.py".to_owned(), - text: "def foo(x): pass".to_owned(), - }]; - render_diagnostics_json(&[diag], &sources, &[]); - } - - #[test] - fn json_severity_warning() -> Result<(), Box> { - let source = "def foo(x): pass"; - let (line, col) = byte_offset_to_line_col(source, 8); - let (end_line, end_col) = byte_offset_to_line_col(source, 9); - let item = JsonDiagnostic { - code: Some("BSK-0001"), - severity: "warning", - message: "test warning", - path: "test.py", - line, - col, - end_line, - end_col, - }; - let json = serde_json::to_string(&item)?; - assert!(json.contains("\"warning\"")); - Ok(()) - } - - /// A file the run could not analyse is serialised with an explicit `null` - /// code. It must not be omitted — a consumer that reads the key's absence - /// as "no entry here" would drop the only report an unparseable file gets. - #[test] - fn json_failure_entry_serialises_an_explicit_null_code( - ) -> Result<(), Box> { - let item = JsonDiagnostic { - code: None, - severity: "error", - message: "syntax error: Expected `:`, found newline", - path: "broken.py", - line: 1, - col: 1, - end_line: 1, - end_col: 1, - }; - let json = serde_json::to_string(&item)?; - assert!( - json.contains("\"code\":null"), - "the code key must be present and null: {json}" - ); - assert!( - !json.contains("BSK-"), - "no rule ran, so no code may be claimed: {json}" - ); - assert!( - json.contains("\"severity\":\"error\""), - "a failed file is an error: {json}" - ); - assert!( - json.contains("broken.py"), - "the entry must name the file that failed: {json}" - ); - assert!( - json.contains("syntax error"), - "the entry must say why it failed: {json}" - ); - Ok(()) - } - - /// The failure entries are appended to the diagnostics, not substituted for - /// them: a run that both found problems and failed a file reports both. - #[test] - fn render_diagnostics_json_appends_failures_after_diagnostics() { - let diagnostic = Diagnostic { - code: ErrorCode { - code: "BSK-0001", - docs_url: "https://www.basilisk-python.dev/errors/BSK-0001", - }, - severity: Severity::Error, - message: "missing annotation".to_owned(), - span: Span { start: 0, end: 3 }, - path: "ok.py".to_owned(), - help: None, - note: None, - provenance: None, - }; - let sources = vec![FileSource { - path: "ok.py".to_owned(), - text: "def foo(x): pass".to_owned(), - }]; - let failures = vec![JsonFailure { - path: "broken.py", - message: "syntax error: Expected `:`, found newline", - }]; - // Renders to stdout, so what it wrote cannot be read back here; the - // call proves the mixed report does not panic, and the entry the - // failure half contributes is asserted field by field below. - render_diagnostics_json(&[diagnostic], &sources, &failures); - let entry = json::failure_entry(&failures[0]); - assert_eq!(entry.code, None, "no rule ran, so the entry claims no code"); - assert_eq!( - entry.severity, "error", - "a file that could not be read is an error" - ); - assert_eq!( - entry.path, "broken.py", - "the entry names the file that failed" - ); - assert_eq!( - entry.message, "syntax error: Expected `:`, found newline", - "the parser's own message is carried through verbatim", - ); - assert_eq!(entry.line, 1, "the failure anchors at the first line"); - assert_eq!(entry.col, 1, "the failure anchors at the first column"); - assert_eq!( - entry.end_line, 1, - "the failure spans no further than its anchor" - ); - assert_eq!( - entry.end_col, 1, - "the failure spans no further than its anchor" - ); - } - - // ── render_diagnostics_json: FnValue→() mutant at output.rs:87 ────────── - - /// `render_diagnostics_json` — `FnValue → ()` at line 87. - /// The function must actually produce output for non-empty diagnostics. - /// We verify by checking the JSON serialisation round-trips correctly. - #[test] - fn render_diagnostics_json_produces_correct_item_count() { - let d1 = Diagnostic { - code: ErrorCode { - code: "BSK-0001", - docs_url: "https://www.basilisk-python.dev/errors/BSK-0001", - }, - severity: Severity::Error, - message: "missing annotation".to_owned(), - span: Span { start: 0, end: 3 }, - path: "a.py".to_owned(), - help: None, - note: None, - provenance: None, - }; - let d2 = Diagnostic { - code: ErrorCode { - code: "BSK-0002", - docs_url: "https://www.basilisk-python.dev/errors/BSK-0002", - }, - severity: Severity::Error, - message: "missing return annotation".to_owned(), - span: Span { start: 4, end: 7 }, - path: "a.py".to_owned(), - help: None, - note: None, - provenance: None, - }; - let sources = [FileSource { - path: "a.py".to_owned(), - text: "def foo(x): pass".to_owned(), - }]; - // Can't easily capture stdout, but verify items array construction is correct - // by constructing directly. - let items: Vec> = [&d1, &d2] - .iter() - .map(|d| { - let source = sources - .iter() - .find(|s| s.path == d.path) - .map(|s| s.text.as_str()); - let (line, col) = source.map_or((1, 1), |src| { - byte_offset_to_line_col(src, usize::try_from(d.span.start).unwrap_or(0)) - }); - let (end_line, end_col) = source.map_or((line, col + 1), |src| { - byte_offset_to_line_col(src, usize::try_from(d.span.end).unwrap_or(0)) - }); - JsonDiagnostic { - code: Some(d.code.code), - severity: "error", - message: &d.message, - path: &d.path, - line, - col, - end_line, - end_col, - } - }) - .collect(); - assert_eq!(items.len(), 2, "must produce one item per diagnostic"); - assert_eq!(items[0].code, Some("BSK-0001")); - assert_eq!(items[1].code, Some("BSK-0002")); - } - - // ── render_diagnostics_json: != mutant at output.rs:92 ────────────────── - - /// `!=` mutant at line 92: `sources.iter().find(|s| s.path == d.path)`. - /// If `==` becomes `!=`, wrong source is matched → wrong line/col. - /// Test that the right source file is used for offset resolution. - #[test] - fn render_diagnostics_json_matches_correct_source_file() { - let diag = Diagnostic { - code: ErrorCode { - code: "BSK-0001", - docs_url: "https://www.basilisk-python.dev/errors/BSK-0001", - }, - severity: Severity::Error, - message: "test".to_owned(), - span: Span { start: 0, end: 1 }, - path: "b.py".to_owned(), - help: None, - note: None, - provenance: None, - }; - let sources = [ - FileSource { - path: "a.py".to_owned(), - text: "aaaa\nbbbb".to_owned(), - }, - FileSource { - path: "b.py".to_owned(), - text: "x = 1\n".to_owned(), - }, - ]; - let source = sources - .iter() - .find(|s| s.path == diag.path) - .map(|s| s.text.as_str()); - let (line, col) = source.map_or((1, 1), |src| byte_offset_to_line_col(src, 0)); - // b.py offset 0 → line 1, col 1 - assert_eq!(line, 1); - assert_eq!(col, 1); - } - - // ── render_diagnostics_json: - / * mutants at output.rs:97 ────────────── - - /// `BinaryOperator` `-`/`*` mutants at line 97 in `render_diagnostics_json`. - /// Line 97 computes end position: `byte_offset_to_line_col(src, d.span.end as usize)`. - /// We verify `end_col` > col for a span that crosses characters. - #[test] - fn render_diagnostics_json_end_position_after_start() { - let source = "def foo(x): pass"; - // span covers "foo" at bytes 4..7 - let (start_line, start_col) = byte_offset_to_line_col(source, 4); - let (end_line, end_col) = byte_offset_to_line_col(source, 7); - assert_eq!(start_line, 1); - assert_eq!(end_line, 1); - assert!(end_col > start_col, "end_col must be after start_col"); - } - - // ── byte_offset_to_line_col: - → / mutant at output.rs:158 ──────────── - - /// The column formula is: `(clamped - pos - 1) + 1` where pos is the last '\n'. - /// `/` mutant replaces `-` with `/` in `clamped - pos - 1`. - /// e.g. with clamped=8, pos=5: correct = 8-5-1+1 = 3; mutant = 8/5-1+1 = 1+1 = 2 (wrong). - /// Assert the exact column value to kill this mutant. - #[test] - fn byte_offset_to_line_col_column_arithmetic_exact() { - // "hello\nworld" — "world" starts at byte 6 - // At byte 8 ('r'): line=2, col=3 (1-based: w=1, o=2, r=3) - let source = "hello\nworld"; - let (line, col) = byte_offset_to_line_col(source, 8); - assert_eq!(line, 2, "byte 8 must be line 2"); - assert_eq!(col, 3, "byte 8 ('r') must be col 3"); - } - - /// Further column test: first char of second line must be col 1. - #[test] - fn byte_offset_to_line_col_first_char_of_second_line() { - let source = "hello\nworld"; - // byte 6 is 'w' — first char of line 2 - let (line, col) = byte_offset_to_line_col(source, 6); - assert_eq!(line, 2); - assert_eq!(col, 1, "first char of line must be col 1"); - } - - /// Multi-line: byte 12 is 'l' in "line3" (3rd line, 1st char). - #[test] - fn byte_offset_to_line_col_multi_line_correct() { - let source = "line1\nline2\nline3"; - // "line3" starts at byte 12 - let (line, col) = byte_offset_to_line_col(source, 12); - assert_eq!(line, 3, "byte 12 must be line 3"); - assert_eq!(col, 1, "byte 12 must be col 1"); - } - - /// Last char of first line (just before '\n'). - #[test] - fn byte_offset_to_line_col_last_char_first_line() { - // "hello\nworld": byte 4 is 'o' (5th char of first line) - let source = "hello\nworld"; - let (line, col) = byte_offset_to_line_col(source, 4); - assert_eq!(line, 1); - assert_eq!(col, 5, "byte 4 ('o') must be col 5"); - } - - /// Offset past end is clamped — doesn't panic. - #[test] - fn byte_offset_to_line_col_offset_beyond_end() { - let source = "abc"; - let (line, col) = byte_offset_to_line_col(source, 9999); - assert_eq!(line, 1); - assert_eq!(col, 4, "clamped to len=3, col=4 (1-based after last char)"); - } - - // ── format_snippet: structural / mutant tests ──────────────────────────── - - /// `BinaryOperator` `-`/`*` mutants at `line_start = rfind('\n').map_or(0, |p| p + 1)`. - /// The `+ 1` skips the newline byte. Without it, `line_start` points at '\n' itself. - #[test] - fn format_snippet_line_start_skips_newline() { - force_colors(); - let source = "hello\nworld"; - let out = render_snippet(source, 8, 10, Severity::Error); - // Must contain "world" (the source line), not "\nworld". - assert!( - out.contains("world"), - "snippet must contain source line, got:\n{out}" - ); - // Underline must be 2 carets for a 2-byte span. - assert!( - out.contains(&format!("{BOLD_RED}^^{RESET}")), - "underline must be 2 carets (bold red), got:\n{out}" - ); - } - - /// `col_start = start - line_start`. If this becomes `start + line_start`, - /// `col_start` would be huge and the underline position would be wrong. - #[test] - fn format_snippet_col_start_no_overflow() { - force_colors(); - let source = "abcdef\nghijkl"; - let out = render_snippet(source, 9, 12, Severity::Error); - // Must contain the source line. - assert!(out.contains("ghijkl"), "must contain source line"); - // Underline must be 3 carets for "ijk". - assert!( - out.contains(&format!("{BOLD_RED}^^^{RESET}")), - "underline must be 3 carets, got:\n{out}" - ); - } - - /// `col_end = (end - line_start).min(len)`. The span 12..14 extends past - /// the end of "ghijkl" (6 chars at `line_start=7`), so `col_end` is clamped - /// to line length. `underline_len` = 6 - 5 = 1. - #[test] - fn format_snippet_col_end_no_overflow() { - force_colors(); - let source = "abcdef\nghijkl"; - let out = render_snippet(source, 12, 14, Severity::Error); - assert!(out.contains("ghijkl"), "must contain source line"); - assert!( - out.contains(&format!("{BOLD_RED}^{RESET}")), - "underline must be clamped to 1 caret, got:\n{out}" - ); - } - - /// Strip ANSI escape sequences (`ESC [ ... m`) so column positions can be - /// measured on the plain text. - fn strip_ansi(text: &str) -> String { - let mut out = String::new(); - let mut chars = text.chars(); - while let Some(c) = chars.next() { - if c == '\x1b' { - for next in chars.by_ref() { - if next == 'm' { - break; - } - } - } else { - out.push(c); - } - } - out - } - - /// Issue #279: every gutter row must place its `|` in the same column as - /// the source row's `|`, otherwise the caret underline renders shifted - /// relative to the source text. Span 8..9 covers `x`, so the caret must - /// sit exactly beneath it. - #[test] - fn format_snippet_gutter_pipes_align_with_source_row() { - force_colors(); - let out = strip_ansi(&render_snippet("def foo(x): pass", 8, 9, Severity::Error)); - assert_eq!( - out, " |\n1 | def foo(x): pass\n | ^\n |\n", - "gutter rows must align with the source row (issue #279)" - ); - } - - /// Verify `format_snippet` produces correct underline length. - #[test] - fn format_snippet_arithmetic_properties() { - let source = "hello world\n"; - // span covers "world" at bytes 6..11 - // line_start = 0 (no newline before), col_start = 6, col_end = 11, underline = 5 - let start = 6usize; - let end = 11usize; - let line_start = source[..start].rfind('\n').map_or(0, |p| p + 1); - let line_text = source[line_start..].lines().next().unwrap_or(""); - let col_start = start - line_start; - let col_end = (end - line_start).min(line_text.len()); - let underline_len = col_end.saturating_sub(col_start).max(1); - assert_eq!(line_start, 0); - assert_eq!(col_start, 6); - assert_eq!(col_end, 11); - assert_eq!(underline_len, 5, "underline for 'world' must be 5 chars"); - } - - // ColorMode::Never / Always are tested via subprocess in cli_binary_tests.rs - // because `colored` uses a global atomic that races with parallel unit tests. -} diff --git a/crates/basilisk-cli/src/output/text.rs b/crates/basilisk-cli/src/output/text.rs deleted file mode 100644 index 11b0d6df5..000000000 --- a/crates/basilisk-cli/src/output/text.rs +++ /dev/null @@ -1,277 +0,0 @@ -//! Rustc-style text rendering for diagnostics with terminal colours. -//! -//! Example output (without ANSI codes): -//! ```text -//! error[BSK-0001]: Missing parameter type annotation for `data` -//! --> src/utils.py:14:5 -//! | -//! 14 | def process(data): -//! | ^^^^ parameter `data` has no type annotation -//! | -//! = help: Add a type annotation: `data: ` -//! = note: In Basilisk, all function parameters require explicit types -//! = see: https://www.basilisk-python.dev/errors/BSK-0001 -//! ``` - -use std::fmt::Write as _; - -use basilisk_checker::{Diagnostic, Severity}; -use colored::Colorize as _; - -use super::FileSource; - -/// Render all diagnostics to stdout in rustc style. -/// -/// Returns the count of error-severity diagnostics. -pub fn render_diagnostics(diagnostics: &[Diagnostic], sources: &[FileSource]) -> usize { - use std::io::Write; - - // Precompute one line index per source; every diagnostic then converts its - // span to line/col in O(log n) instead of rescanning the source prefix. - let indexes = super::SourceIndexes::new(sources); - let colorize = colored::control::SHOULD_COLORIZE.should_colorize(); - let count = diagnostics - .iter() - .filter(|diagnostic| diagnostic.severity == Severity::Error) - .count(); - - let mut stdout = std::io::stdout().lock(); - if colorize { - // Keep terminal output incremental so a large project starts showing - // useful diagnostics immediately. - let mut out = std::io::BufWriter::new(stdout); - for diagnostic in diagnostics { - let rendered = format_one(diagnostic, indexes.for_path(&diagnostic.path)); - let _ = out.write_all(rendered.as_bytes()); - } - let _ = out.flush(); - } else { - // Pipes, CI, editors, and benchmark runs are the overwhelmingly common - // high-volume path. Build their plain render once and write it in one - // operation instead of feeding the 8 KiB BufWriter once per diagnostic. - // Cap only the initial reservation; String can still grow for genuinely - // large output without an attacker-controlled eager allocation. - let initial_capacity = diagnostics.len().saturating_mul(384).min(8 * 1024 * 1024); - let mut rendered = String::with_capacity(initial_capacity); - for diagnostic in diagnostics { - format_one_plain_into( - &mut rendered, - diagnostic, - indexes.for_path(&diagnostic.path), - ); - } - let _ = stdout.write_all(rendered.as_bytes()); - let _ = stdout.flush(); - } - - count -} - -/// Format directly into a reusable buffer when ANSI colour is disabled. -/// -/// CLI output is normally piped in editor, CI, and benchmark use. Avoiding the -/// temporary coloured strings and per-diagnostic output allocation keeps that -/// common path proportional to bytes written, even for error-dense files. -fn format_one_plain_into( - out: &mut String, - diag: &Diagnostic, - source: Option<(&str, &basilisk_common::text::LineIndex)>, -) { - let _ = writeln!( - out, - "{}[{}]: {}", - diag.severity, diag.code.code, diag.message - ); - - if let Some((_, index)) = source { - let (line, col) = index.line_col(diag.span.start_usize()); - let _ = writeln!(out, " --> {}:{line}:{col}", diag.path); - } else { - let _ = writeln!(out, " --> {}", diag.path); - } - - if let Some((src, index)) = source { - format_snippet_plain_into( - out, - src, - index, - diag.span.start_usize(), - diag.span.end_usize(), - ); - } - - if let Some(help) = &diag.help { - let _ = writeln!(out, " = help: {help}"); - } - if let Some(note) = &diag.note { - let _ = writeln!(out, " = note: {note}"); - } - let _ = writeln!(out, " = see: {}\n", diag.code.docs_url); -} - -fn format_snippet_plain_into( - out: &mut String, - source: &str, - index: &basilisk_common::text::LineIndex, - start: usize, - end: usize, -) { - let line_num = index.line(start); - let line_start = index.line_start(start); - let line_text = source - .get(line_start..) - .and_then(|tail| tail.lines().next()) - .unwrap_or(""); - let col_start = start - line_start; - let col_end = (end - line_start).min(line_text.len()); - let underline_len = col_end.saturating_sub(col_start).max(1); - let line_num_width = decimal_width(line_num); - - push_repeated(out, ' ', line_num_width); - out.push_str(" |\n"); - let _ = writeln!(out, "{line_num} | {line_text}"); - push_repeated(out, ' ', line_num_width); - out.push_str(" | "); - push_repeated(out, ' ', col_start); - push_repeated(out, '^', underline_len); - out.push('\n'); - push_repeated(out, ' ', line_num_width); - out.push_str(" |\n"); -} - -fn push_repeated(out: &mut String, character: char, count: usize) { - out.extend(std::iter::repeat_n(character, count)); -} - -fn decimal_width(mut value: usize) -> usize { - let mut width = 1; - while value >= 10 { - value /= 10; - width += 1; - } - width -} - -/// Apply the appropriate colour to a severity label. -fn color_severity(severity: Severity, text: &str) -> String { - match severity { - Severity::Error | Severity::SafetyViolation => text.red().bold().to_string(), - Severity::Warning => text.yellow().bold().to_string(), - Severity::Info => text.blue().bold().to_string(), - } -} - -/// Format a single diagnostic as a rustc-style string with ANSI colours. -/// -/// Implements [CHKARCH-DIAGEXP-QUALITY]: emits the rustc-standard layout — -/// `severity[CODE]: message`, `--> path:line:col`, source snippet with caret -/// underline, then `= help:` / `= note:` / `= see:` annotation lines. -/// See docs/specs/CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-DIAGEXP-QUALITY -pub(super) fn format_one( - diag: &Diagnostic, - source: Option<(&str, &basilisk_common::text::LineIndex)>, -) -> String { - let mut out = String::new(); - - // Header: error[BSK-0001]: Message - let severity_label = color_severity(diag.severity, &format!("{}", diag.severity)); - let code = format!("[{}]", diag.code.code).bold(); - let message = diag.message.bold(); - let _ = writeln!(out, "{severity_label}{code}: {message}"); - - // Location: --> path:line:col - let location = source.map_or_else( - || diag.path.clone(), - |(_, index)| { - let (line, col) = index.line_col(diag.span.start_usize()); - format!("{}:{}:{}", diag.path, line, col) - }, - ); - - let _ = writeln!(out, " {} {location}", "-->".blue().bold()); - - // Source snippet with underline - if let Some((src, index)) = source { - out.push_str(&format_snippet( - src, - index, - diag.span.start_usize(), - diag.span.end_usize(), - diag.severity, - )); - } - - // Annotations - if let Some(help) = &diag.help { - let _ = writeln!( - out, - " {} {}: {help}", - "=".blue().bold(), - "help".cyan().bold(), - ); - } - if let Some(note) = &diag.note { - let _ = writeln!( - out, - " {} {}: {note}", - "=".blue().bold(), - "note".cyan().bold(), - ); - } - let _ = writeln!( - out, - " {} {}: {}", - "=".blue().bold(), - "see".cyan().bold(), - diag.code.docs_url, - ); - out.push('\n'); - out -} - -/// Convert a byte offset into (1-based line number, 1-based column number). -/// -/// Production rendering builds a [`LineIndex`](basilisk_common::text::LineIndex) -/// once per file and calls its `line_col`; this single-shot wrapper remains only -/// for the focused line/col unit tests below. -#[cfg(test)] -pub(super) fn byte_offset_to_line_col(source: &str, offset: usize) -> (usize, usize) { - basilisk_common::text::line_col(source, offset) -} - -/// Format a source line with a `^^^^` underline for the highlighted span. -pub(super) fn format_snippet( - source: &str, - index: &basilisk_common::text::LineIndex, - start: usize, - end: usize, - severity: Severity, -) -> String { - let line_num = index.line(start); - let line_start = index.line_start(start); - let line_text = source - .get(line_start..) - .and_then(|tail| tail.lines().next()) - .unwrap_or(""); - - let col_start = start - line_start; - let col_end = (end - line_start).min(line_text.len()); - let underline_len = col_end.saturating_sub(col_start).max(1); - - let line_num_width = line_num.to_string().len(); - let pad = " ".repeat(line_num_width); - let pipe = "|".blue().bold(); - let line_num_str = line_num.to_string().blue().bold(); - let underline = color_severity(severity, &"^".repeat(underline_len)); - - let mut out = String::new(); - let _ = writeln!(out, "{pad} {pipe}"); - let _ = writeln!(out, "{line_num_str} {pipe} {line_text}"); - let _ = writeln!( - out, - "{pad} {pipe} {spaces}{underline}", - spaces = " ".repeat(col_start), - ); - let _ = writeln!(out, "{pad} {pipe}"); - out -} diff --git a/crates/basilisk-cli/src/pipeline/mod.rs b/crates/basilisk-cli/src/pipeline/mod.rs deleted file mode 100644 index 30e357c1c..000000000 --- a/crates/basilisk-cli/src/pipeline/mod.rs +++ /dev/null @@ -1,587 +0,0 @@ -//! Implements [CHKARCH-CLI] and [CHKARCH-COMMANDS]. See -//! docs/specs/CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-CLI -//! -//! The shared collect-and-check pipeline behind `basilisk check` and -//! `basilisk analyze`. Both commands run the identical pipeline — file -//! collection, per-directory config discovery, import resolution, caching, -//! `check_with_config` — and differ only in the [`DiagnosticScope`] edge -//! filter applied to the resulting diagnostics ([CHKARCH-COMMANDS]). - -use std::collections::HashSet; - -use tracing::{info, warn}; - -use crate::cache_check; -use crate::output::FileSource; - -mod typeshed; - -pub(crate) use typeshed::build_import_search_paths; -use typeshed::{ - activate_production_typeshed, build_import_search_paths_with_config, load_cli_workspace_config, -}; - -/// Which command's diagnostics to keep at the CLI edge. -/// -/// Implements [CHKARCH-COMMANDS]: one rule universe, partitioned exactly once -/// by provenance tag. A rule is check-scope iff it carries the `pep` tag; -/// everything else is analyze-scope. The checker runs all selected rules; the -/// CLI edge filters by command. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum DiagnosticScope { - /// `basilisk check` — `pep`-tagged rules only, always on. - Check, - /// `basilisk analyze` — every rule *not* tagged `pep`, config-selected. - Analyze, - /// Both scopes — `adopt` reads the union at resolved severities - /// ([AUTOFIX-ADOPTION]). - Union, -} - -impl DiagnosticScope { - /// Whether a diagnostic with `code` belongs to this scope. - pub(crate) fn retains(self, code: &str) -> bool { - match self { - Self::Check => basilisk_checker::is_pep_rule(code), - Self::Analyze => !basilisk_checker::is_pep_rule(code), - Self::Union => true, - } - } -} - -/// A pipeline failure, mapped to the [CHKARCH-CLI-EXITCODES] contract. -#[derive(Debug)] -pub(crate) enum PipelineError { - /// Invalid configuration (exit code `2`) — e.g. a config that resolves a - /// `pep` rule to `disabled` ([CHKARCH-CONFIG-MODEL]). - Config(String), - /// A terminal typeshed source failure (exit code `3`) — the configured - /// source is not on this machine or failed verification. The message is - /// the spec's `NO SOURCE` status line with its recovery command, so it - /// reads as a user-actionable failure, not a Basilisk bug - /// ([STUBRES-TYPESHED-OFFLINE]). - NoSource(String), - /// Internal failure (exit code `3`). - Internal(String), -} - -impl std::fmt::Display for PipelineError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Config(message) => write!(f, "invalid configuration: {message}"), - // Both already carry a fully-formed message: `NoSource` the spec's - // `NO SOURCE` status line with its recovery command, `Internal` the - // underlying failure. Neither takes a prefix. - Self::NoSource(message) | Self::Internal(message) => write!(f, "{message}"), - } - } -} - -#[derive(Debug)] -pub(crate) struct FileAnalysisFailure { - pub(crate) path: String, - pub(crate) message: String, -} - -pub(crate) struct CheckOutcome { - pub(crate) diagnostics: Vec, - pub(crate) sources: Vec, - pub(crate) failures: Vec, - /// How many rules configuration selected that this scope never evaluated - /// — always `0` outside [`DiagnosticScope::Check`] - /// ([CHKARCH-CLI-SCOPE-NOTICE]). - pub(crate) unrun_selected_rules: usize, -} - -/// Resolve the paths a check run walks. Implements [CHKARCH-CONFIG-INCLUDE]: -/// explicit CLI paths win, then the configured `include` roots, then `.`. -pub(crate) fn effective_check_paths( - paths: &[String], - config: &basilisk_config::BasiliskConfig, - config_root: &std::path::Path, -) -> Vec { - if !paths.is_empty() { - return paths.to_vec(); - } - if config.include.is_empty() { - return vec![".".to_owned()]; - } - config - .include - .iter() - .map(|inc| config_root.join(inc).to_string_lossy().into_owned()) - .collect() -} - -/// The per-directory rule configuration of a run, keyed by owning directory -/// ([CHKARCH-CONFIG-DISCOVERY]). -pub(crate) type DirConfigs = - std::collections::BTreeMap>; - -/// The union of the codes `select` reports across the base config and every -/// per-directory config in this run — one file's config never speaks for the -/// whole run ([CHKARCH-CONFIG-DISCOVERY]). -fn codes_across_configs( - dir_configs: &DirConfigs, - base: &basilisk_config::BasiliskConfig, - select: fn(&basilisk_config::BasiliskConfig) -> Vec<&'static str>, -) -> std::collections::BTreeSet<&'static str> { - let mut codes: std::collections::BTreeSet<&'static str> = select(base).into_iter().collect(); - for config in dir_configs.values() { - codes.extend(select(config)); - } - codes -} - -/// The codes an invalid configuration resolves to `disabled` although they -/// are `pep`-tagged, across every per-directory config in this run. -/// -/// Implements [CHKARCH-CONFIG-MODEL]: `disabled` never applies to a `pep` -/// rule — such a configuration is invalid and fails the run before checking. -fn pep_disable_config_error( - dir_configs: &DirConfigs, - base: &basilisk_config::BasiliskConfig, -) -> Option { - let violations = - codes_across_configs(dir_configs, base, basilisk_checker::pep_disable_violations); - if violations.is_empty() { - return None; - } - let codes = violations.into_iter().collect::>().join(", "); - Some(format!( - "configuration resolves PEP typing-spec rules to `disabled`, which is invalid \ - ([CHKARCH-CONFIG-MODEL]): {codes}. PEP rules always run; grade them \ - `error`/`warning`/`info` instead." - )) -} - -/// Collect Python files and check each one under its own discovered config, -/// keeping only diagnostics in `scope` ([CHKARCH-COMMANDS]). -/// -/// # Errors -/// -/// [`PipelineError::Config`] when any discovered configuration invalidly -/// resolves a `pep` rule to `disabled`; [`PipelineError::Internal`] on -/// collection failures (e.g. nonexistent paths). -pub(crate) fn collect_and_check( - paths: &[String], - cache: &cache_check::CacheOptions, - stats: &mut cache_check::CacheStats, - scope: DiagnosticScope, -) -> Result { - collect_and_check_with_typeshed(paths, cache, stats, scope, activate_production_typeshed) -} - -fn collect_and_check_with_typeshed( - paths: &[String], - cache: &cache_check::CacheOptions, - stats: &mut cache_check::CacheStats, - scope: DiagnosticScope, - activate_typeshed: F, -) -> Result -where - F: Fn( - &mut basilisk_lsp::import_resolver::ImportSearchPaths, - &basilisk_lsp::config::WorkspaceConfig, - &basilisk_config::BasiliskConfig, - ) -> Result<(), PipelineError>, -{ - // [CHKARCH-CONFIG-DISCOVERY] The first path only anchors project-level - // concerns (include expansion, version detection, cache location); rule - // config is resolved per checked file below, so diagnostics never depend - // on argument order (GitHub #311). - let config_root = first_path_dir(paths); - // Project metadata can live above the checked path (for example, - // `conformance/tests/case.py` inherits `conformance/pyproject.toml`). - // Resolve the project root before reading target-version evidence so a - // nested invocation observes the same explicit project target as the LSP. - let project_root = find_project_root(&config_root); - let mut config = basilisk_config::load_basilisk_config(&config_root); - // [CHKARCH-VERSION-TARGET] Detect the target version from project files - // when the config does not pin one, matching the LSP (issue #93). - if config.python_version.is_none() { - config.python_version = - basilisk_uv::python_version::resolve_target_python_version(&project_root); - } - - let workspace_config = - load_cli_workspace_config(&project_root, config.python_version.as_deref()); - if config.python_platform.is_none() { - config - .python_platform - .clone_from(&workspace_config.python_platform); - } - - // Activate the typeshed source FIRST: the bundled default resolves its - // snapshot (and prewarms the builtins index) on a background thread, so - // kicking it off before file collection and search-path discovery - // maximises the overlap with the lead-in work. The activation seam only - // populates `typeshed_snapshot`, transplanted into the real search paths - // below. The source-status advisories resolve severity through the same - // project `[tool.basilisk]` tables as any rule - // ([STUBRES-TYPESHED-CONFIG]), so the project-root config is handed in. - let mut typeshed_activation = crate::import_search::roots_only(Vec::new()); - activate_typeshed(&mut typeshed_activation, &workspace_config, &config)?; - - let excluded = excluded_dirs_and_log(&config, &config_root); - - // Implements [CHKARCH-CONFIG-INCLUDE] (issue #37): a no-args run walks - // only the configured include roots, never the whole repository. - let paths = &effective_check_paths(paths, &config, &config_root); - let python_files = collect_python_files(paths, &excluded).map_err(PipelineError::Internal)?; - - // Build import search paths (venv, uv registry, workspace members). - // pyproject.toml, uv.lock, and .venv live at the discovered project root, - // not necessarily in the checked path. - let roots = analysis_roots(paths, &project_root); - let mut search_paths = if crate::import_search::files_might_import(&python_files) { - build_import_search_paths_with_config(roots, &workspace_config) - } else { - crate::import_search::roots_only(roots) - }; - search_paths.typeshed_snapshot = typeshed_activation.typeshed_snapshot.take(); - - // Per-file rule config, memoized per directory ([CHKARCH-CONFIG-DISCOVERY]). - // The cache fingerprint covers every directory's config so a child config - // edit invalidates cached results. - let dir_configs = resolve_dir_configs(&python_files, &config); - - // A config that disables a PEP rule is invalid and fails the run before - // any checking ([CHKARCH-CONFIG-MODEL], [CHKARCH-CLI-EXITCODES] code 2). - if let Some(message) = pep_disable_config_error(&dir_configs, &config) { - return Err(PipelineError::Config(message)); - } - - // [CHKARCH-CLI-SCOPE-NOTICE] (GitHub #334): the edge filter below drops - // every analyze-scope diagnostic from a `check` run. Count the rules - // configuration selected but this scope will never evaluate, so the - // renderer can say so — a silent clean run is indistinguishable from a - // clean project. - let unrun_selected_rules = match scope { - DiagnosticScope::Check => codes_across_configs( - &dir_configs, - &config, - basilisk_checker::analyze_selected_rules, - ) - .len(), - DiagnosticScope::Analyze | DiagnosticScope::Union => 0, - }; - - // [CHKCACHE-CONFIG]: the project-root config carries the standing - // `cache`/`cache-dir` policy; the CLI flags override it for this run only. - let cache_context = - cache_check::build_context(cache, &config, &dir_configs, &search_paths, &project_root); - - let mut all_diagnostics = Vec::new(); - let mut sources = Vec::new(); - let mut failures = Vec::new(); - - for path in python_files { - let file_config = config_for_path(&dir_configs, &path, &config); - let outcome = cache_check::check_file(cache_context.as_ref(), stats, &path, || { - process_file(&path, &search_paths, &file_config) - }); - match outcome { - Ok((diags, source)) => { - // The command's edge filter ([CHKARCH-COMMANDS]). Applied - // after the cache layer so cached entries stay scope-free - // and both commands share them. - all_diagnostics.extend(diags.into_iter().filter(|d| scope.retains(d.code.code))); - sources.push(FileSource { path, text: source }); - } - Err(err) => { - failures.push(FileAnalysisFailure { path, message: err }); - } - } - } - - // A deferred typeshed load that failed must fail the run loudly — inside - // the loop the miss can only surface as unresolved imports. A run that - // never needed the archive (all cache hits) never forces the load and is - // unaffected. - if let Some(error) = search_paths - .typeshed_snapshot - .as_ref() - .and_then(basilisk_lsp::import_resolver::ActiveTypeshed::deferred_error) - { - return Err(PipelineError::Internal(error)); - } - - Ok(CheckOutcome { - diagnostics: all_diagnostics, - sources, - failures, - unrun_selected_rules, - }) -} - -/// Canonical project and checked-directory roots used for import resolution. -pub(crate) fn analysis_roots( - paths: &[String], - project_root: &std::path::Path, -) -> Vec { - let canonical = std::fs::canonicalize(project_root).unwrap_or_else(|_| project_root.into()); - paths.iter().fold(vec![canonical], |mut roots, path| { - let candidate = std::path::Path::new(path); - let directory = if candidate.is_dir() { - candidate.to_path_buf() - } else { - parent_dir_of(path) - }; - if let Ok(absolute) = std::fs::canonicalize(directory) { - if !roots.contains(&absolute) { - roots.push(absolute); - } - } - roots - }) -} - -fn process_file( - path: &str, - search_paths: &basilisk_lsp::import_resolver::ImportSearchPaths, - config: &basilisk_config::BasiliskConfig, -) -> Result<(Vec, String), String> { - let target_version = - basilisk_checker::context::CheckContext::from_config(config).target_version; - let (resolved, source) = resolve_file_imports(path, search_paths, target_version)?; - // Apply the project's `[tool.basilisk]` tables so the CLI and editor - // agree on selection and severity ([CHKARCH-CONFIG-MODEL]). Using `check` - // here would silently drop config. - let diagnostics = basilisk_checker::check_with_config(&resolved, config); - Ok((diagnostics, source)) -} - -/// Parse a source file and resolve its imports through the shared CLI/LSP paths. -pub(crate) fn resolve_file_imports( - path: &str, - search_paths: &basilisk_lsp::import_resolver::ImportSearchPaths, - target_version: Option<(u32, u32)>, -) -> Result<(basilisk_resolver::ResolvedModule, String), String> { - let parsed = basilisk_parser::parse_file(path).map_err(|e| e.to_string())?; - let source = parsed.source.clone(); - let mut resolved = match target_version { - Some(target_version) => basilisk_resolver::resolve_with_target(&parsed, target_version), - None => basilisk_resolver::resolve(&parsed), - } - .map_err(|e| e.to_string())?; - - // Resolve imports against venv/site-packages and uv registry using the same - // routine the LSP uses, so the CLI and editor agree on what resolves and on - // package-dependency metadata (BSK-0011 transitive-import warnings, etc.). - basilisk_lsp::import_resolver::resolve_module_imports(&mut resolved, search_paths); - Ok((resolved, source)) -} - -/// The directory anchoring project-level concerns for a CLI invocation: the -/// first path argument's own directory (or its parent for a file), else cwd. -pub(crate) fn first_path_dir(paths: &[String]) -> std::path::PathBuf { - paths.first().map(std::path::Path::new).map_or_else( - || std::path::PathBuf::from("."), - |p| { - if p.is_dir() { - p.to_path_buf() - } else { - p.parent() - .filter(|parent| !parent.as_os_str().is_empty()) - .unwrap_or_else(|| std::path::Path::new(".")) - .to_path_buf() - } - }, - ) -} - -/// The directory owning `path` (its parent, or `.` for a bare filename). -pub(crate) fn parent_dir_of(path: &str) -> std::path::PathBuf { - std::path::Path::new(path) - .parent() - .filter(|dir| !dir.as_os_str().is_empty()) - .map_or_else( - || std::path::PathBuf::from("."), - std::path::Path::to_path_buf, - ) -} - -/// Resolve the merged rule config for each checked file's directory. -/// -/// Implements [CHKARCH-CONFIG-DISCOVERY] (GitHub #311): every file is checked -/// with the config discovered from its own ancestor chain, so diagnostics are -/// independent of argument order, path spelling, and cwd. Memoized per -/// directory; `fallback` supplies the detected Python version when a -/// directory's chain does not pin one ([CHKARCH-VERSION-TARGET]). -pub(crate) fn resolve_dir_configs( - python_files: &[String], - fallback: &basilisk_config::BasiliskConfig, -) -> DirConfigs { - let mut dir_configs = std::collections::BTreeMap::new(); - for path in python_files { - let _ = dir_configs - .entry(parent_dir_of(path)) - .or_insert_with_key(|dir| { - let mut cfg = basilisk_config::load_basilisk_config(dir); - if cfg.python_version.is_none() { - cfg.python_version.clone_from(&fallback.python_version); - } - if cfg.python_platform.is_none() { - cfg.python_platform.clone_from(&fallback.python_platform); - } - std::sync::Arc::new(cfg) - }); - } - dir_configs -} - -/// The per-directory config for `path`, falling back to `fallback` (only -/// reachable if `path` was not in the file list the map was built from). -pub(crate) fn config_for_path( - dir_configs: &DirConfigs, - path: &str, - fallback: &basilisk_config::BasiliskConfig, -) -> std::sync::Arc { - dir_configs - .get(&parent_dir_of(path)) - .cloned() - .unwrap_or_else(|| std::sync::Arc::new(fallback.clone())) -} - -/// Walk up from `start` to find the project root (directory containing -/// `pyproject.toml` or `uv.lock`). Falls back to cwd, then `start`. -pub(crate) fn find_project_root(start: &std::path::Path) -> std::path::PathBuf { - let abs = std::fs::canonicalize(start).unwrap_or_else(|_| start.to_path_buf()); - let mut current = abs.as_path(); - loop { - if current.join("pyproject.toml").is_file() || current.join("uv.lock").is_file() { - return current.to_path_buf(); - } - match current.parent() { - Some(parent) => current = parent, - None => break, - } - } - // Fallback: cwd, then the original start path. - std::env::current_dir().unwrap_or_else(|_| start.to_path_buf()) -} - -/// Return `"s"` for counts != 1, empty string otherwise. -pub(crate) fn pluralise(count: usize) -> &'static str { - if count == 1 { - "" - } else { - "s" - } -} - -/// Whether `path` is excluded by any configured pattern, matched -/// gitignore-style against the path relative to the walk `root`. -/// -/// Implements [CHKARCH-CONFIG-EXCLUDE]. See -/// docs/specs/CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-CONFIG-EXCLUDE -/// -/// Uses the same [`basilisk_config::path_matches_pattern`] matcher as the LSP -/// workspace scan, so `basilisk check` and the editor agree on what is skipped: -/// bare names (`build`) at any depth, directory globs (`**/generated/**`), -/// and file globs (`*.pb.py`) all work — not just literal directory names. -fn is_excluded_path( - path: &std::path::Path, - root: &std::path::Path, - excluded: &HashSet<&str>, -) -> bool { - let relative = path.strip_prefix(root).unwrap_or(path); - excluded - .iter() - .any(|pattern| basilisk_config::path_matches_pattern(relative, pattern)) -} - -/// Build the excluded-directory set from `config` and log that the config at -/// `config_root` was loaded. Shared setup prologue for the CLI subcommands. -pub(crate) fn excluded_dirs_and_log<'a>( - config: &'a basilisk_config::BasiliskConfig, - config_root: &std::path::Path, -) -> HashSet<&'a str> { - let excluded: HashSet<&str> = config.exclude.iter().map(String::as_str).collect(); - info!( - excluded_dirs = ?config.exclude, - "loaded config from {}", - config_root.display() - ); - excluded -} - -/// `true` for the Python source extensions Basilisk type-checks: `.py` -/// implementation files and `.pyi` stub files (whose overload-definition and -/// `@final`/`@override` rules differ — see `overloads_*`). Stubs were silently -/// dropped before, so a `basilisk check foo.pyi` produced no diagnostics. -fn is_python_source_ext(ext: &std::ffi::OsStr) -> bool { - ext.eq_ignore_ascii_case("py") || ext.eq_ignore_ascii_case("pyi") -} - -pub(crate) fn collect_python_files( - paths: &[String], - excluded: &HashSet<&str>, -) -> Result, String> { - let mut files = Vec::new(); - - for root in paths { - let meta = match std::fs::metadata(root) { - Ok(m) => m, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - return Err(format!("cannot access {root}: {e}")); - } - Err(e) => { - warn!(root, %e, "cannot access path"); - continue; - } - }; - - if meta.is_file() { - if std::path::Path::new(root) - .extension() - .is_some_and(is_python_source_ext) - { - files.push(root.clone()); - } - } else { - let root_path = std::path::Path::new(root); - for entry in walkdir::WalkDir::new(root) - .follow_links(false) - .into_iter() - .filter_entry(|e| { - if !e.file_type().is_dir() { - return true; - } - // Never exclude the root entry (depth 0) — the user - // explicitly asked to check this path (often `.`). - if e.depth() == 0 { - return true; - } - let name = e.file_name().to_string_lossy(); - // Hidden directories are always excluded. - if name.starts_with('.') { - return false; - } - // Virtualenvs are pruned structurally by their `pyvenv.cfg` - // marker, whatever `exclude` says ([CHKARCH-CONFIG-EXCLUDE], - // GitHub #341). The depth-0 exemption above still lets an - // explicit `basilisk check ./venv` in. - if basilisk_config::is_virtualenv_dir(e.path()) { - return false; - } - !is_excluded_path(e.path(), root_path, excluded) - }) - .filter_map(Result::ok) - .filter(|e| e.file_type().is_file()) - .filter(|e| e.path().extension().is_some_and(is_python_source_ext)) - // File-level globs (e.g. `*.pb.py`, `**/conftest.py`) are honoured - // here; directory globs are already pruned above before recursing. - .filter(|e| !is_excluded_path(e.path(), root_path, excluded)) - { - files.push(entry.path().to_string_lossy().into_owned()); - } - } - } - - Ok(files) -} - -#[cfg(test)] -mod tests; diff --git a/crates/basilisk-cli/src/pipeline/tests.rs b/crates/basilisk-cli/src/pipeline/tests.rs deleted file mode 100644 index ddf33c165..000000000 --- a/crates/basilisk-cli/src/pipeline/tests.rs +++ /dev/null @@ -1,1016 +0,0 @@ -//! Unit tests for the shared collect-and-check pipeline. -//! -//! Cross-references [CHKARCH-CLI], [CHKARCH-COMMANDS], [CHKARCH-CONFIG-MODEL], -//! and [CHKARCH-CONFIG-DISCOVERY] (GitHub #311); the code under test is -//! `crate::pipeline`. - -use super::*; -use crate::cache_check; - -/// Default excludes for test helpers. -fn test_excludes() -> HashSet<&'static str> { - basilisk_config::DEFAULT_EXCLUDES.iter().copied().collect() -} - -/// Disabled cache options for tests that exercise the plain check pipeline. -fn no_cache() -> cache_check::CacheOptions { - cache_check::CacheOptions { - enabled: cache_check::CacheOverride::ForceOff, - dir: None, - stats: false, - } -} - -/// Run `collect_and_check` with the cache disabled and a throwaway tally. -fn collect_uncached( - paths: &[String], - scope: DiagnosticScope, -) -> Result { - collect_and_check_with_typeshed( - paths, - &no_cache(), - &mut cache_check::CacheStats::default(), - scope, - activate_bundled_typeshed, - ) -} - -fn activate_bundled_typeshed( - search_paths: &mut basilisk_lsp::import_resolver::ImportSearchPaths, - config: &basilisk_lsp::config::WorkspaceConfig, - _rule_config: &basilisk_config::BasiliskConfig, -) -> Result<(), PipelineError> { - let snapshot = basilisk_stubs::typeshed::bundle::bundled_snapshot() - .map(std::sync::Arc::new) - .map_err(|error| PipelineError::Internal(error.to_string()))?; - search_paths.typeshed_snapshot = Some(basilisk_checker::imports::ActiveTypeshed::new( - snapshot, - basilisk_lsp::import_resolver::stub_target_from_config(config), - )); - Ok(()) -} - -/// Unique temp dir for tests that need an isolated project root. -fn unique_project_dir(prefix: &str) -> std::path::PathBuf { - use std::sync::atomic::{AtomicU64, Ordering}; - static CTR: AtomicU64 = AtomicU64::new(0); - let n = CTR.fetch_add(1, Ordering::Relaxed); - std::env::temp_dir().join(format!("{prefix}_{}_{n}", std::process::id())) -} - -/// [STUBRES-TYPESHED-WARN]: ordinary CLI analysis activates the configured -/// production source once and attaches its exact status/target to resolution. -#[test] -fn cli_activation_uses_custom_snapshot_and_target() -> Result<(), Box> { - let project = unique_project_dir("basilisk_cli_custom_typeshed"); - let stdlib = project.join("typeshed").join("stdlib"); - std::fs::create_dir_all(&stdlib)?; - std::fs::write(stdlib.join("VERSIONS"), "sentinel: 3.8-\n")?; - std::fs::write(stdlib.join("sentinel.pyi"), "VALUE: str\n")?; - let config = basilisk_lsp::config::WorkspaceConfig { - typeshed_path: Some(project.join("typeshed")), - python_version: Some("3.12".to_owned()), - python_platform: Some("Linux".to_owned()), - ..basilisk_lsp::config::WorkspaceConfig::default() - }; - let mut search_paths = crate::import_search::roots_only(vec![project.clone()]); - super::typeshed::activate_production_typeshed( - &mut search_paths, - &config, - &basilisk_config::BasiliskConfig::default(), - ) - .map_err(|error| std::io::Error::other(error.to_string()))?; - let active = search_paths - .typeshed_snapshot - .as_ref() - .ok_or("active Typeshed missing")?; - assert_eq!( - active - .snapshot() - .ok_or("snapshot must resolve")? - .status - .active_source, - basilisk_stubs::typeshed::source::SourceKind::Custom - ); - assert_eq!( - active - .snapshot() - .ok_or("snapshot must resolve")? - .status - .warnings - .iter() - .map(|warning| warning.code.as_str()) - .collect::>(), - vec!["typeshed_source_unpinned", "typeshed_source_user_managed"] - ); - assert_eq!( - active.target().map(|target| target.python_version), - Some((3, 12)) - ); - let _ = std::fs::remove_dir_all(project); - Ok(()) -} - -/// [STUBRES-TYPESHED-OFFLINE]: a pinned commit that is not on this machine -/// tanks the run hard — no download, no bundle fallback, no diagnostics. The -/// production activation path itself surfaces the terminal `NO SOURCE` -/// failure and the checked project's config reaches it verbatim. -#[test] -fn a_missing_pin_tanks_the_check_instead_of_downloading() -> Result<(), Box> -{ - let project = unique_project_dir("basilisk_cli_typeshed_no_source"); - std::fs::create_dir_all(&project)?; - let store = project.join("empty-store"); - std::fs::create_dir_all(&store)?; - // A valid full-SHA pin that no store on this machine holds. - let missing = "0123456789abcdef0123456789abcdef01234567"; - std::fs::write( - project.join("pyproject.toml"), - format!( - "[tool.basilisk]\ntypeshed-commit = \"{missing}\"\ntypeshed-store-path = \"{}\"\n", - store.display() - ), - )?; - let source = project.join("clean.py"); - std::fs::write(&source, "value: int = 1\n")?; - let path = source.to_string_lossy().into_owned(); - - let result = collect_and_check_with_typeshed( - &[path], - &no_cache(), - &mut cache_check::CacheStats::default(), - DiagnosticScope::Check, - super::typeshed::activate_production_typeshed, - ); - // The store stays byte-for-byte inert: resolution never writes, repairs, - // or downloads ([STUBRES-TYPESHED-STORE]). - let store_entries = std::fs::read_dir(&store)?.count(); - let _ = std::fs::remove_dir_all(project); - let Err(PipelineError::NoSource(message)) = result else { - return Err("a missing pin must tank the run".into()); - }; - assert!(message.contains("NO SOURCE"), "got: {message}"); - assert!(message.contains(missing), "got: {message}"); - assert!( - message.contains("basilisk typeshed download"), - "the failure must say how to materialise the pin: {message}" - ); - assert_eq!(store_entries, 0, "resolution must never write to the store"); - Ok(()) -} - -/// Project-level target evidence applies when the checked file is nested -/// below the project root. The official python/typing suite has exactly this -/// layout: `conformance/pyproject.toml` declares Python 3.12 and fixtures live -/// in `conformance/tests/`. -#[test] -fn nested_file_inherits_project_python_target_for_analysis( -) -> Result<(), Box> { - let project = unique_project_dir("basilisk_cli_nested_python_target"); - let tests = project.join("tests"); - std::fs::create_dir_all(&tests)?; - std::fs::write( - project.join("pyproject.toml"), - "[project]\nname = \"fixture\"\nversion = \"0.0.0\"\nrequires-python = \"==3.12.*\"\n", - )?; - let source = tests.join("fixture.py"); - std::fs::write(&source, "value: int = 1\n")?; - - let result = collect_and_check_with_typeshed( - &[source.to_string_lossy().into_owned()], - &no_cache(), - &mut cache_check::CacheStats::default(), - DiagnosticScope::Check, - |_search_paths, config, _rule_config| { - assert_eq!(config.python_version.as_deref(), Some("3.12")); - Ok(()) - }, - ); - let _ = std::fs::remove_dir_all(project); - let _outcome = - result.map_err(|error| format!("the nested file must analyse cleanly: {error:?}"))?; - Ok(()) -} - -/// The official python/typing layout supplies a project Python version but no -/// `python-platform`. The selected interpreter is concrete environment -/// evidence, so both forms of an impossible platform guard must still narrow. -#[test] -fn nested_file_inherits_selected_interpreter_platform_for_analysis( -) -> Result<(), Box> { - let project = unique_project_dir("basilisk_cli_nested_platform_target"); - let tests = project.join("tests"); - std::fs::create_dir_all(&tests)?; - std::fs::write( - project.join("pyproject.toml"), - "[project]\nname = \"fixture\"\nversion = \"0.0.0\"\nrequires-python = \"==3.12.*\"\n", - )?; - let source = tests.join("directives_version_platform.py"); - std::fs::write( - &source, - concat!( - "import sys\n\n", - "def test():\n", - " if sys.version_info < (3, 8):\n", - " val3 = ''\n", - " else:\n", - " live3 = ''\n", - " use3 = val3\n", - " if sys.platform == 'bogus_platform':\n", - " val6 = ''\n", - " else:\n", - " live6 = ''\n", - " use6 = val6\n", - " if sys.platform != 'bogus_platform':\n", - " live9 = ''\n", - " else:\n", - " val9 = ''\n", - " use9 = val9\n", - " return live3, live6, live9\n", - ), - )?; - - let outcome = collect_uncached( - &[source.to_string_lossy().into_owned()], - DiagnosticScope::Check, - ) - .map_err(|error| error.to_string())?; - let _ = std::fs::remove_dir_all(project); - let messages = outcome - .diagnostics - .iter() - .filter(|diagnostic| diagnostic.code.code == "directives_version_platform") - .map(|diagnostic| diagnostic.message.as_str()) - .collect::>(); - - for dead in ["val3", "val6", "val9"] { - assert!( - messages.iter().any(|message| message.contains(dead)), - "missing dead-branch diagnostic for {dead}: {messages:?}" - ); - } - for live in ["live3", "live6", "live9"] { - assert!( - messages.iter().all(|message| !message.contains(live)), - "selected-interpreter narrowing must not flag live {live}: {messages:?}" - ); - } - Ok(()) -} - -#[test] -fn explicit_all_platform_does_not_narrow_checker_branches() -> Result<(), Box> -{ - let project = unique_project_dir("basilisk_cli_all_platform_target"); - std::fs::create_dir_all(&project)?; - std::fs::write( - project.join("pyproject.toml"), - concat!( - "[project]\nname = \"fixture\"\nversion = \"0.0.0\"\n", - "requires-python = \"==3.12.*\"\n\n", - "[tool.basilisk]\npython-platform = \"All\"\n", - ), - )?; - let source = project.join("platform.py"); - std::fs::write( - &source, - concat!( - "import sys\n\n", - "def test():\n", - " if sys.platform == 'win32':\n", - " windows = ''\n", - " else:\n", - " other = ''\n", - " return windows, other\n", - ), - )?; - - let outcome = collect_uncached( - &[source.to_string_lossy().into_owned()], - DiagnosticScope::Check, - ) - .map_err(|error| error.to_string())?; - let _ = std::fs::remove_dir_all(project); - let platform_diagnostics = outcome - .diagnostics - .iter() - .filter(|diagnostic| diagnostic.code.code == "directives_version_platform") - .collect::>(); - - assert!( - platform_diagnostics.is_empty(), - "cross-platform analysis cannot call either platform branch dead: {platform_diagnostics:#?}" - ); - Ok(()) -} - -#[test] -fn bare_filename_anchors_project_discovery_at_current_directory() { - assert_eq!( - first_path_dir(&["fixture.py".to_owned()]), - std::path::PathBuf::from(".") - ); -} - -#[test] -fn bare_filename_adds_current_directory_to_import_roots() -> Result<(), Box> -{ - let roots = analysis_roots(&["fixture.py".to_owned()], std::path::Path::new("..")); - let current = std::fs::canonicalize(".")?; - assert!(roots.contains(¤t), "{roots:#?}"); - Ok(()) -} - -#[test] -fn configured_target_prunes_inactive_conditional_constructor_fields( -) -> Result<(), Box> { - let project = unique_project_dir("basilisk_cli_conditional_constructor_fields"); - std::fs::create_dir_all(&project)?; - let source = project.join("fixture.py"); - std::fs::write( - &source, - concat!( - "from typing import NamedTuple\n", - "import sys\n\n", - "class ConditionalField(NamedTuple):\n", - " x: int\n", - " if sys.version_info >= (3, 12):\n", - " y: int\n", - " if sys.version_info >= (4, 0):\n", - " z: int\n\n", - "ConditionalField(1, 2)\n", - "ConditionalField(1, 2, 3)\n", - ), - )?; - let search_paths = crate::import_search::roots_only(vec![project.clone()]); - let config = basilisk_config::BasiliskConfig { - python_version: Some("3.12".to_owned()), - ..basilisk_config::BasiliskConfig::default() - }; - let (diagnostics, _) = process_file(&source.to_string_lossy(), &search_paths, &config)?; - let constructor_messages = diagnostics - .iter() - .filter(|diagnostic| diagnostic.code.code == "constructors_call_init") - .map(|diagnostic| diagnostic.message.as_str()) - .collect::>(); - assert_eq!(constructor_messages.len(), 1, "{diagnostics:#?}"); - assert!( - constructor_messages - .first() - .is_some_and(|message| message.contains("accepts at most 2 positional arguments")), - "{diagnostics:#?}" - ); - let _ = std::fs::remove_dir_all(project); - Ok(()) -} - -// ── DiagnosticScope ([CHKARCH-COMMANDS]) ────────────────────────────────── - -/// [CHKARCH-COMMANDS]: the partition is exact — `check` keeps only -/// `pep`-tagged codes, `analyze` keeps only the rest, `union` keeps both. -#[test] -fn diagnostic_scope_partitions_by_pep_tag() { - // `imports_unresolved` is a conformance (pep) rule; `BSK-0001` is a - // Basilisk-original opt-in rule ([CHKTAG-PROVENANCE]). - assert!(basilisk_checker::is_pep_rule("imports_unresolved")); - assert!(!basilisk_checker::is_pep_rule("BSK-0001")); - - assert!(DiagnosticScope::Check.retains("imports_unresolved")); - assert!(!DiagnosticScope::Check.retains("BSK-0001")); - assert!(!DiagnosticScope::Analyze.retains("imports_unresolved")); - assert!(DiagnosticScope::Analyze.retains("BSK-0001")); - assert!(DiagnosticScope::Union.retains("imports_unresolved")); - assert!(DiagnosticScope::Union.retains("BSK-0001")); -} - -/// [CHKARCH-COMMANDS]: every catalog code lands in exactly one command scope. -#[test] -fn every_rule_belongs_to_exactly_one_command() { - for descriptor in basilisk_checker::rule_catalog() { - let check = DiagnosticScope::Check.retains(descriptor.code); - let analyze = DiagnosticScope::Analyze.retains(descriptor.code); - assert!( - check ^ analyze, - "rule {} must belong to exactly one command scope", - descriptor.code - ); - } -} - -// ── collect_python_files ────────────────────────────────────────────────── - -#[test] -fn collect_python_files_returns_err_for_nonexistent_path() { - let result = collect_python_files(&["/no/such/path/ever.py".to_owned()], &test_excludes()); - assert!(result.is_err(), "nonexistent path must return Err"); -} - -#[test] -fn collect_python_files_skips_non_py_file() -> Result<(), Box> { - let dir = std::env::temp_dir(); - let txt = dir.join("basilisk_test_skip.txt"); - std::fs::write(&txt, b"hello")?; - let path = txt.to_string_lossy().into_owned(); - let files = collect_python_files(&[path], &test_excludes())?; - assert!(files.is_empty(), "non-.py file must be skipped"); - let _ = std::fs::remove_file(&txt); - Ok(()) -} - -#[test] -fn collect_python_files_includes_py_file() -> Result<(), Box> { - let dir = std::env::temp_dir(); - let py = dir.join("basilisk_test_include.py"); - std::fs::write(&py, b"x = 1")?; - let path = py.to_string_lossy().into_owned(); - let files = collect_python_files(&[path], &test_excludes())?; - let _ = std::fs::remove_file(&py); - assert_eq!(files.len(), 1, ".py file must be included"); - Ok(()) -} - -#[test] -fn collect_python_files_walks_directory() -> Result<(), Box> { - let base = std::env::temp_dir().join("basilisk_test_walk_dir"); - let _ = std::fs::remove_dir_all(&base); - std::fs::create_dir_all(&base)?; - std::fs::write(base.join("a.py"), b"x = 1")?; - std::fs::write(base.join("b.txt"), b"ignored")?; - let path = base.to_string_lossy().into_owned(); - let files = collect_python_files(&[path], &test_excludes())?; - let _ = std::fs::remove_dir_all(&base); - assert_eq!( - files.len(), - 1, - "directory walk must find exactly one .py file" - ); - Ok(()) -} - -/// `collect_python_files` — `MatchArmGuard → true` mutant: the `NotFound` -/// guard distinguishes "not found" from other I/O errors. The `NotFound` -/// path specifically returns Err (not Ok with an empty list). -#[test] -fn collect_python_files_not_found_returns_err() { - let result = collect_python_files( - &["/absolutely/does/not/exist/file.py".to_owned()], - &test_excludes(), - ); - assert!(result.is_err(), "NotFound path must return Err, not Ok"); -} - -/// Complement: a path that exists but is not .py returns Ok with empty list. -/// This kills the `true` guard mutant: if all errors → Err, this would fail. -#[test] -fn collect_python_files_non_py_existing_file_returns_ok_empty( -) -> Result<(), Box> { - let dir = std::env::temp_dir(); - let txt = dir.join("basilisk_test_guard_complement.txt"); - std::fs::write(&txt, b"hello")?; - let path = txt.to_string_lossy().into_owned(); - let result = collect_python_files(&[path], &test_excludes()); - let _ = std::fs::remove_file(&txt); - assert!(result.is_ok(), "existing non-py file must return Ok"); - assert!(result?.is_empty(), "non-py file must produce empty list"); - Ok(()) -} - -#[test] -fn collect_python_files_skips_excluded_directories() -> Result<(), Box> { - let base = std::env::temp_dir().join("basilisk_test_exclude_dirs"); - let _ = std::fs::remove_dir_all(&base); - std::fs::create_dir_all(&base)?; - - // File in root — should be found. - std::fs::write(base.join("app.py"), b"x = 1")?; - - // Files in default-excluded directories — should be skipped. - for excluded in &["__pycache__", "venv", "site-packages", "node_modules"] { - let sub = base.join(excluded); - std::fs::create_dir_all(&sub)?; - std::fs::write(sub.join("hidden.py"), b"x = 1")?; - } - - // File in a hidden directory — should be skipped. - let hidden = base.join(".hidden"); - std::fs::create_dir_all(&hidden)?; - std::fs::write(hidden.join("secret.py"), b"x = 1")?; - - let path = base.to_string_lossy().into_owned(); - let files = collect_python_files(&[path], &test_excludes())?; - let _ = std::fs::remove_dir_all(&base); - - assert_eq!( - files.len(), - 1, - "only root app.py should be found, got: {files:?}" - ); - Ok(()) -} - -#[test] -fn collect_python_files_respects_custom_excludes() -> Result<(), Box> { - let base = std::env::temp_dir().join("basilisk_test_custom_exclude"); - let _ = std::fs::remove_dir_all(&base); - std::fs::create_dir_all(&base)?; - - std::fs::write(base.join("app.py"), b"x = 1")?; - let sub = base.join("vendor"); - std::fs::create_dir_all(&sub)?; - std::fs::write(sub.join("lib.py"), b"x = 1")?; - - // Custom exclude: only "vendor", not the defaults. - let custom: HashSet<&str> = ["vendor"].into_iter().collect(); - let path = base.to_string_lossy().into_owned(); - let files = collect_python_files(&[path], &custom)?; - let _ = std::fs::remove_dir_all(&base); - - assert_eq!( - files.len(), - 1, - "vendor should be excluded, only app.py found" - ); - Ok(()) -} - -/// Regression: the `basilisk check` CLI ignored user **glob** excludes. -/// `collect_python_files` must honour gitignore-style globs via -/// `basilisk_config::path_matches_pattern`, agreeing with the LSP scan. -/// Implements [CHKARCH-CONFIG-EXCLUDE]. -#[test] -fn collect_python_files_honors_user_glob_excludes() -> Result<(), Box> { - let base = std::env::temp_dir().join(format!( - "basilisk_test_cli_glob_exclude_{}", - std::process::id() - )); - let _ = std::fs::remove_dir_all(&base); - let gen = base.join("src").join("generated"); - std::fs::create_dir_all(&gen)?; - std::fs::write(base.join("app.py"), b"x = 1")?; // real code — must survive - std::fs::write(gen.join("models.py"), b"y = 2")?; // excluded by **/generated/** - std::fs::write(base.join("schema.pb.py"), b"z = 3")?; // excluded by *.pb.py - - let excludes: HashSet<&str> = ["**/generated/**", "*.pb.py"].into_iter().collect(); - let path = base.to_string_lossy().into_owned(); - let files = collect_python_files(&[path], &excludes)?; - let _ = std::fs::remove_dir_all(&base); - - let names: Vec = files.iter().map(|f| f.replace('\\', "/")).collect(); - assert_eq!( - files.len(), - 1, - "only app.py should survive the glob excludes, got: {names:?}" - ); - assert!( - names.iter().any(|f| f.ends_with("/app.py")), - "app.py must still be collected: {names:?}" - ); - assert!( - !names.iter().any(|f| f.contains("generated")), - "**/generated/** must exclude the nested directory: {names:?}" - ); - assert!( - !names.iter().any(|f| f.contains("schema.pb.py")), - "*.pb.py glob must exclude the file: {names:?}" - ); - Ok(()) -} - -/// Regression: `basilisk check .` found zero files because the root -/// entry `.` starts with `.` and was rejected by the hidden-dir filter. -#[test] -fn collect_python_files_hidden_root_dir_still_walked() -> Result<(), Box> { - let base = std::env::temp_dir().join("basilisk_test_hidden_root"); - let _ = std::fs::remove_dir_all(&base); - - let hidden = base.join(".myproject"); - std::fs::create_dir_all(&hidden)?; - std::fs::write(hidden.join("app.py"), b"x = 1")?; - let sub = hidden.join("pkg"); - std::fs::create_dir_all(&sub)?; - std::fs::write(sub.join("mod.py"), b"y = 2")?; - - let path = hidden.to_string_lossy().into_owned(); - let files = collect_python_files(&[path], &test_excludes())?; - let _ = std::fs::remove_dir_all(&base); - - assert_eq!( - files.len(), - 2, - "user-supplied root starting with '.' must still be walked, got: {files:?}" - ); - Ok(()) -} - -// ── collect_and_check ───────────────────────────────────────────────────── - -#[test] -#[cfg(unix)] -fn collect_and_check_handles_unreadable_py_file() -> Result<(), Box> { - use std::os::unix::fs::PermissionsExt; - - let dir = std::env::temp_dir(); - let py = dir.join("basilisk_test_locked.py"); - std::fs::write(&py, b"def foo(): pass")?; - std::fs::set_permissions(&py, std::fs::Permissions::from_mode(0o000))?; - - let path = py.to_string_lossy().into_owned(); - let result = collect_uncached(&[path], DiagnosticScope::Union); - std::fs::set_permissions(&py, std::fs::Permissions::from_mode(0o644))?; - let _ = std::fs::remove_file(&py); - - let outcome = result.map_err(|err| err.to_string())?; - assert!( - outcome.diagnostics.is_empty(), - "unreadable file produces no diagnostics, got: {:#?}", - outcome.diagnostics - ); - assert_eq!( - outcome.failures.len(), - 1, - "unreadable file must be a failure" - ); - Ok(()) -} - -/// [CHKARCH-COMMANDS]: `def foo(x)` violates the annotation house rules -/// (BSK-0001/BSK-0002), which are analyze-scope opt-ins. An opted-in project -/// sees them under the analyze scope — and never under the check scope. -#[test] -fn collect_and_check_scopes_house_rules_to_analyze() -> Result<(), Box> { - let dir = unique_project_dir("basilisk_test_bad_code"); - std::fs::create_dir_all(&dir)?; - std::fs::write( - dir.join("pyproject.toml"), - b"[tool.basilisk.rules]\n\"BSK-0001\" = \"error\"\n\"BSK-0002\" = \"error\"\n", - )?; - let py = dir.join("bad.py"); - std::fs::write(&py, b"def foo(x):\n pass\n")?; - let path = py.to_string_lossy().into_owned(); - let analyze = collect_uncached(std::slice::from_ref(&path), DiagnosticScope::Analyze) - .map_err(|err| err.to_string())?; - let check = collect_uncached(&[path], DiagnosticScope::Check).map_err(|err| err.to_string())?; - let _ = std::fs::remove_dir_all(&dir); - assert!( - analyze - .diagnostics - .iter() - .any(|d| d.code.code == "BSK-0001"), - "analyze scope must fire the configured house rule" - ); - assert!( - check.diagnostics.iter().all(|d| d.code.code != "BSK-0001"), - "check scope must never emit a house rule, even when configured \ - ([CHKARCH-COMMANDS]); got: {:#?}", - check.diagnostics - ); - Ok(()) -} - -/// Regression: `basilisk analyze` must honor `[tool.basilisk.rules]` severity -/// grades from `pyproject.toml`. A project that escalates BSK-0050 to "error" -/// must see it surface as a hard error through the real pipeline. -/// [CHKARCH-CONFIG-MODEL] -#[test] -fn collect_and_check_applies_project_rule_severity_override( -) -> Result<(), Box> { - let dir = unique_project_dir("basilisk_cli_cfg_promote"); - std::fs::create_dir_all(&dir)?; - // An explicit non-disabled severity both selects and grades an - // off-by-default rule. See [CHKARCH-CONFIGURATION-ONLY]. - std::fs::write( - dir.join("pyproject.toml"), - b"[project]\nname = \"x\"\nversion = \"0.1.0\"\n\n\ - [tool.basilisk.rules]\n\"BSK-0050\" = \"error\"\n", - )?; - let py = dir.join("m.py"); - std::fs::write(&py, b"x: int = 42\n")?; - - let path = py.to_string_lossy().into_owned(); - let outcome = - collect_uncached(&[path], DiagnosticScope::Analyze).map_err(|err| err.to_string())?; - let _ = std::fs::remove_dir_all(&dir); - - let w0050: Vec<_> = outcome - .diagnostics - .iter() - .filter(|d| d.code.code == "BSK-0050") - .collect(); - assert!(!w0050.is_empty(), "BSK-0050 must fire under analyze"); - assert!( - w0050 - .iter() - .all(|d| d.severity == basilisk_checker::Severity::Error), - "project config `BSK-0050 = \"error\"` must promote BSK-0050 to error; got {:?}", - w0050.iter().map(|d| d.severity).collect::>() - ); - Ok(()) -} - -// ── pep-disable violations ([CHKARCH-CONFIG-MODEL]) ─────────────────────── - -/// [CHKARCH-CONFIG-MODEL]: a config that resolves a `pep` rule to `disabled` -/// is invalid — the pipeline fails with a configuration error before checking. -#[test] -fn pep_disable_config_fails_the_run() -> Result<(), Box> { - let dir = unique_project_dir("basilisk_cli_pep_disable"); - std::fs::create_dir_all(&dir)?; - std::fs::write( - dir.join("pyproject.toml"), - b"[tool.basilisk.rules]\n\"imports_unresolved\" = \"disabled\"\n", - )?; - let py = dir.join("m.py"); - std::fs::write(&py, b"x: int = 1\n")?; - - let path = py.to_string_lossy().into_owned(); - let check = collect_uncached(std::slice::from_ref(&path), DiagnosticScope::Check); - let analyze = collect_uncached(&[path], DiagnosticScope::Analyze); - let _ = std::fs::remove_dir_all(&dir); - - for (command, result) in [("check", check), ("analyze", analyze)] { - let message = match result { - Err(PipelineError::Config(message)) => message, - Err(PipelineError::Internal(message)) => { - return Err(format!( - "`{command}` must fail with a Config error, got Internal: {message}" - ) - .into()) - } - Err(PipelineError::NoSource(message)) => { - return Err(format!( - "`{command}` must fail with a Config error, got NoSource: {message}" - ) - .into()) - } - Ok(outcome) => { - return Err(format!( - "`{command}` must fail with a Config error, got Ok with {} diagnostics", - outcome.diagnostics.len() - ) - .into()) - } - }; - assert!( - message.contains("imports_unresolved"), - "`{command}` config error must name the offending code, got: {message}" - ); - } - Ok(()) -} - -/// Grading (not disabling) a pep rule remains valid configuration. -/// [CHKARCH-CONFIG-MODEL] -#[test] -fn pep_grade_config_is_valid() -> Result<(), Box> { - let dir = unique_project_dir("basilisk_cli_pep_grade"); - std::fs::create_dir_all(&dir)?; - std::fs::write( - dir.join("pyproject.toml"), - b"[tool.basilisk.rules]\n\"imports_unresolved\" = \"warning\"\n", - )?; - let py = dir.join("m.py"); - std::fs::write(&py, b"x: int = 1\n")?; - - let path = py.to_string_lossy().into_owned(); - let outcome = collect_uncached(&[path], DiagnosticScope::Check); - let _ = std::fs::remove_dir_all(&dir); - assert!( - outcome.is_ok(), - "grading a pep rule to warning must be valid config" - ); - Ok(()) -} - -// ── config discovery (GitHub #311, [CHKARCH-CONFIG-DISCOVERY]) ──────────── - -/// Source that violates the annotation house rules (BSK-0001 on the -/// parameter, BSK-0002 on the return) once those opt-in rules are enabled. -// `x` has no default to infer from (BSK-0001) and `return x` is not inferable -// (BSK-0002) — a `pass` body would infer `-> None` and only fire BSK-0001 -// ([TYPEINF-FUNC-RETURN]). -const UNANNOTATED_FN: &[u8] = b"def foo(x):\n return x\n"; - -/// A `[tool.basilisk.rules]` table enabling the opt-in annotation rules. -const ANNOTATION_RULES_TOML: &[u8] = - b"[tool.basilisk.rules]\n\"BSK-0001\" = \"error\"\n\"BSK-0002\" = \"error\"\n"; - -/// GitHub #311 (headline): `basilisk analyze path/to/file.py` must discover -/// rule config from ancestor directories, so a repo-root `pyproject.toml` -/// governs a file checked by path. -#[test] -fn analyze_file_arg_discovers_config_from_ancestor_directories( -) -> Result<(), Box> { - let root = unique_project_dir("basilisk_cli_cfg_ancestor"); - let child = root.join("child"); - std::fs::create_dir_all(&child)?; - std::fs::write(root.join("pyproject.toml"), ANNOTATION_RULES_TOML)?; - let py = child.join("bad.py"); - std::fs::write(&py, UNANNOTATED_FN)?; - - let outcome = collect_uncached( - &[py.to_string_lossy().into_owned()], - DiagnosticScope::Analyze, - ); - let _ = std::fs::remove_dir_all(&root); - let outcome = outcome.map_err(|err| err.to_string())?; - - let codes: Vec<&str> = outcome.diagnostics.iter().map(|d| d.code.code).collect(); - assert!( - codes.contains(&"BSK-0001"), - "checking child/bad.py by file path must apply the root pyproject.toml \ - (ancestor walk, GitHub #311); got codes: {codes:?}" - ); - Ok(()) -} - -/// GitHub #311 (consequence 3): results must not depend on argument order. -/// With rules in `p/pyproject.toml` and none in `q`, both `analyze p q` and -/// `analyze q p` must flag `p/bad.py` — and never flag `q/bad.py`. -#[test] -fn results_are_independent_of_argument_order() -> Result<(), Box> { - let base = unique_project_dir("basilisk_cli_cfg_order"); - let p = base.join("p"); - let q = base.join("q"); - std::fs::create_dir_all(&p)?; - std::fs::create_dir_all(&q)?; - std::fs::write(p.join("pyproject.toml"), ANNOTATION_RULES_TOML)?; - std::fs::write(p.join("bad.py"), UNANNOTATED_FN)?; - std::fs::write(q.join("bad.py"), UNANNOTATED_FN)?; - - let p_arg = p.to_string_lossy().into_owned(); - let q_arg = q.to_string_lossy().into_owned(); - let p_first = collect_uncached(&[p_arg.clone(), q_arg.clone()], DiagnosticScope::Analyze); - let q_first = collect_uncached(&[q_arg, p_arg], DiagnosticScope::Analyze); - let _ = std::fs::remove_dir_all(&base); - - for (order, outcome) in [ - ("analyze p q", p_first.map_err(|err| err.to_string())?), - ("analyze q p", q_first.map_err(|err| err.to_string())?), - ] { - let e0001_paths: Vec<&str> = outcome - .diagnostics - .iter() - .filter(|d| d.code.code == "BSK-0001") - .map(|d| d.path.as_str()) - .collect(); - assert!( - e0001_paths - .iter() - .any(|path| std::path::Path::new(path).starts_with(&p)), - "`{order}` must apply p's own config to p/bad.py regardless of \ - argument order (GitHub #311); BSK-0001 paths: {e0001_paths:?}" - ); - assert!( - e0001_paths - .iter() - .all(|path| !std::path::Path::new(path).starts_with(&q)), - "`{order}` must NOT leak p's config onto q/bad.py, which has no \ - config anywhere above it (GitHub #311); BSK-0001 paths: {e0001_paths:?}" - ); - } - Ok(()) -} - -/// [CHKARCH-CONFIG-MODEL]: the nearest table that decides a rule wins, per -/// rule. The root enables both annotation rules; the child only disables -/// BSK-0001, so BSK-0002 is still decided by the root and must fire. -#[test] -fn nearest_deciding_table_wins_per_rule() -> Result<(), Box> { - let root = unique_project_dir("basilisk_cli_cfg_nearest"); - let child = root.join("child"); - std::fs::create_dir_all(&child)?; - std::fs::write(root.join("pyproject.toml"), ANNOTATION_RULES_TOML)?; - std::fs::write( - child.join("pyproject.toml"), - b"[tool.basilisk.rules]\n\"BSK-0001\" = \"disabled\"\n", - )?; - let py = child.join("bad.py"); - std::fs::write(&py, UNANNOTATED_FN)?; - - let outcome = collect_uncached( - &[py.to_string_lossy().into_owned()], - DiagnosticScope::Analyze, - ); - let _ = std::fs::remove_dir_all(&root); - let outcome = outcome.map_err(|err| err.to_string())?; - - let codes: Vec<&str> = outcome.diagnostics.iter().map(|d| d.code.code).collect(); - assert!( - codes.contains(&"BSK-0002"), - "the root's rule opt-ins must survive a child table that only decides \ - one rule (nearest-deciding-table, [CHKARCH-CONFIG-MODEL]); got: {codes:?}" - ); - assert!( - !codes.contains(&"BSK-0001"), - "the child table's `BSK-0001 = disabled` must be honored; got: {codes:?}" - ); - Ok(()) -} - -#[test] -fn collect_and_check_returns_no_diagnostics_for_clean_code( -) -> Result<(), Box> { - let dir = std::env::temp_dir(); - let py = dir.join("basilisk_test_clean_code.py"); - std::fs::write(&py, b"def greet(name: str) -> str:\n return name\n")?; - let path = py.to_string_lossy().into_owned(); - let outcome = - collect_uncached(&[path], DiagnosticScope::Union).map_err(|err| err.to_string())?; - let _ = std::fs::remove_file(&py); - assert!( - outcome.diagnostics.is_empty(), - "fully annotated code must produce no diagnostics" - ); - Ok(()) -} - -// ── Self-named external bases (issues #278/#299 family) ────────────────── - -/// `class EnvironBuilder(werkzeug.test.EnvironBuilder)` — flask 3.1.1 -/// `src/flask/testing.py`, hit by the [VSIX-REALWORLD-JOURNEY] flask corpus — -/// records its unresolved external base under the class's own terminal name. -/// On the FULL CLI pipeline (which, unlike the bare parse→resolve→check test -/// harness, first runs `resolve_module_imports`) the base walk must not -/// recurse through the self-referential class-map entry: before the fix this -/// stack-overflowed and aborted the whole `basilisk check` process (and -/// crash-looped the LSP on the same code path). -#[test] -fn self_named_attribute_base_check_does_not_overflow() -> Result<(), Box> { - let dir = unique_project_dir("bsk_selfnamed_attr_base"); - std::fs::create_dir_all(&dir)?; - let py = dir.join("repro.py"); - std::fs::write( - &py, - b"import werkzeug.test\n\n\nclass EnvironBuilder(werkzeug.test.EnvironBuilder):\n pass\n", - )?; - let path = py.to_string_lossy().into_owned(); - let outcome = - collect_uncached(&[path], DiagnosticScope::Check).map_err(|err| err.to_string())?; - let _ = std::fs::remove_dir_all(&dir); - assert!( - outcome.failures.is_empty(), - "self-named external base must analyse cleanly, got failures: {:?}", - outcome.failures - ); - assert_eq!( - outcome.sources.len(), - 1, - "exactly the repro file is checked" - ); - Ok(()) -} - -// ── pluralise ───────────────────────────────────────────────────────────── - -#[test] -fn pluralise_zero_returns_s() { - assert_eq!(pluralise(0), "s"); -} - -#[test] -fn pluralise_one_returns_empty() { - assert_eq!(pluralise(1), ""); -} - -#[test] -fn pluralise_many_returns_s() { - assert_eq!(pluralise(5), "s"); -} - -#[test] -fn pipeline_errors_preserve_the_exit_code_category_in_display() { - assert_eq!( - PipelineError::Config("bad target".to_owned()).to_string(), - "invalid configuration: bad target" - ); - assert_eq!( - PipelineError::Internal("read failed".to_owned()).to_string(), - "read failed" - ); - assert_eq!( - PipelineError::NoSource("NO SOURCE — missing".to_owned()).to_string(), - "NO SOURCE — missing" - ); -} - -#[test] -fn analysis_roots_adds_a_distinct_checked_directory() -> Result<(), Box> { - let project = tempfile::tempdir()?; - let checked = tempfile::tempdir()?; - let source = checked.path().join("module.py"); - std::fs::write(&source, "value: int = 1\n")?; - - let roots = analysis_roots(&[source.to_string_lossy().into_owned()], project.path()); - - assert_eq!(roots.len(), 2); - assert!(roots.contains(&std::fs::canonicalize(checked.path())?)); - Ok(()) -} - -#[test] -fn non_not_found_metadata_errors_are_skipped_without_aborting_other_roots() { - let invalid = "path-with-nul\0.py".to_owned(); - let files = collect_python_files(&[invalid], &test_excludes()); - - assert!(matches!(files, Ok(found) if found.is_empty())); -} diff --git a/crates/basilisk-cli/src/pipeline/typeshed.rs b/crates/basilisk-cli/src/pipeline/typeshed.rs deleted file mode 100644 index 442769364..000000000 --- a/crates/basilisk-cli/src/pipeline/typeshed.rs +++ /dev/null @@ -1,542 +0,0 @@ -//! CLI activation and reporting for [STUBRES-TYPESHED-WARN]. -//! See docs/specs/CHECKER-STUB-RESOLUTION-SPEC.md#STUBRES-TYPESHED-WARN. - -use basilisk_config::{BasiliskConfig, RuleSeverity}; -use tracing::{debug, info, warn}; - -use super::PipelineError; - -/// Load import/typeshed configuration and preserve CLI-only target evidence. -/// -/// The rule configuration loader also consults `.python-version`; the shared -/// LSP import configuration does not. Copying that detected value before -/// activation keeps stdlib filtering aligned with the checker target while an -/// explicit analysis configuration still wins. [STUBRES-TYPESHED-VERSION] -pub(super) fn load_cli_workspace_config( - project_root: &std::path::Path, - detected_python_version: Option<&str>, -) -> basilisk_lsp::config::WorkspaceConfig { - let mut config = basilisk_lsp::config::load_analysis_config(project_root); - if config.python_version.is_none() { - config.python_version = detected_python_version.map(str::to_owned); - } - if config.python_platform.is_none() { - config.python_platform = - basilisk_lsp::debug::python_platform_evidence(config.python_interpreter.as_deref()); - } - // [STUBRES-TYPESHED-PYPI] (issue #312): when no typeshed source is - // configured, auto-resolve a `PyPI` typeshed distribution pin from - // `uv.lock` so a uv-pinned project is reproducible without an explicit - // `typeshed-package` key. No-op for non-uv projects. - basilisk_lsp::config::apply_uv_typeshed_override(&mut config, project_root); - config -} - -/// Build the shared CLI/LSP import search path model for a project. -pub(crate) fn build_import_search_paths( - roots: Vec, - project_root: &std::path::Path, -) -> basilisk_lsp::import_resolver::ImportSearchPaths { - let config = basilisk_lsp::config::load_analysis_config(project_root); - build_import_search_paths_with_config(roots, &config) -} - -pub(super) fn build_import_search_paths_with_config( - roots: Vec, - config: &basilisk_lsp::config::WorkspaceConfig, -) -> basilisk_lsp::import_resolver::ImportSearchPaths { - let registry = build_uv_registry(&roots); - let mut search_paths = - basilisk_lsp::import_resolver::search_paths_from_config(&roots, config, registry); - search_paths.roots = roots; - info!( - site_packages = ?search_paths.site_packages, - has_registry = search_paths.registry.is_some(), - "built import search paths" - ); - search_paths -} - -/// Resolve the configured typeshed source — a local read, never a download -/// ([STUBRES-TYPESHED-OFFLINE]). A pin that is not on this machine is the -/// terminal `NO SOURCE` failure: analysis does not run, and the error itself -/// says how to materialise the pin (`basilisk typeshed download`). -pub(super) fn activate_production_typeshed( - search_paths: &mut basilisk_lsp::import_resolver::ImportSearchPaths, - config: &basilisk_lsp::config::WorkspaceConfig, - rule_config: &BasiliskConfig, -) -> Result<(), PipelineError> { - let request = basilisk_lsp::config::typeshed_request(config).map_err(PipelineError::Config)?; - let target = basilisk_lsp::import_resolver::stub_target_from_config(config); - if let Some(active) = deferred_bundled_activation(&request, target.clone(), rule_config) { - search_paths.typeshed_snapshot = Some(active); - return Ok(()); - } - let manager = basilisk_stubs::typeshed::runtime::production_manager(request); - let snapshot = manager.snapshot().map_err(|error| match error { - // A terminal source failure (missing/corrupt pin, missing/corrupt - // `PyPI` package) is a user-actionable `NO SOURCE`, not an internal - // bug — the message carries the recovery command - // ([STUBRES-TYPESHED-OFFLINE]). - basilisk_stubs::typeshed::selector::SelectionError::NoSource { .. } - | basilisk_stubs::typeshed::selector::SelectionError::PyPIPackage { .. } - | basilisk_stubs::typeshed::selector::SelectionError::Custom(_) => { - PipelineError::NoSource(error.to_string()) - } - // A backend handing back a source it was not asked for is a Basilisk - // bug, not something the user can fix — the only genuinely internal - // selection failure. - inconsistent @ basilisk_stubs::typeshed::selector::SelectionError::InconsistentIdentity => { - PipelineError::Internal(inconsistent.to_string()) - } - })?; - report_typeshed_status(&snapshot.status, rule_config, &mut std::io::stderr().lock()); - search_paths.typeshed_snapshot = Some(basilisk_checker::imports::ActiveTypeshed::new( - snapshot, target, - )); - Ok(()) -} - -/// Activate a pin of the BUNDLED commit without blocking on the archive: the -/// identity and status are manifest metadata (`bundled_pinned_status`, pinned -/// equal to the selector's status under test), so the banner prints -/// immediately while a background thread decodes the snapshot and prewarms -/// the builtins index. The pipeline lead-in (file collection, config -/// discovery, source parsing) runs concurrently, and a fully cache-hit run -/// never waits for the archive at all. Every other selection — custom trees, -/// non-bundled pins — resolves eagerly, so its verification and error -/// surfacing are unchanged. A deferred load failure is surfaced loudly at the -/// end of the run via `ActiveTypeshed::deferred_error`. -fn deferred_bundled_activation( - request: &basilisk_stubs::typeshed::source::TypeshedRequest, - target: Option, - rule_config: &BasiliskConfig, -) -> Option { - let basilisk_stubs::typeshed::source::SourceSelection::Pinned { commit, explicit } = - &request.selection - else { - return None; - }; - if commit.to_hex() != basilisk_stubs::typeshed::bundle::bundled_commit_sha() { - return None; - } - // Must match `SourceIdentity::Bundled.uri_component()` so fingerprints - // and equality agree with the eager path. - let identity = format!("bundled-{}", commit.to_hex()); - let status = basilisk_stubs::typeshed::bundle::bundled_pinned_status(*explicit).ok()?; - report_typeshed_status(&status, rule_config, &mut std::io::stderr().lock()); - let thread_request = request.clone(); - let thread_target = target.clone(); - let loader = std::thread::spawn(move || { - let manager = basilisk_stubs::typeshed::runtime::production_manager(thread_request); - let snapshot = manager.snapshot().map_err(|error| error.to_string())?; - basilisk_checker::imports::prewarm_builtin_classes(&snapshot, thread_target.as_ref()); - Ok(snapshot) - }); - Some(basilisk_checker::imports::ActiveTypeshed::deferred( - identity, - target, - move || { - loader - .join() - .unwrap_or_else(|_panic| Err("typeshed loader thread panicked".to_owned())) - }, - )) -} - -/// Report the resolved typeshed source status. -/// -/// Two distinct surfaces, never conflated ([STUBRES-TYPESHED-WARN]): -/// * structured **telemetry** at `debug` for the log file / Output Channel — -/// the machine `active_source`/identity fields, never a human banner; -/// * a rustc-style **human banner** on `banner` (stderr in production) — one -/// `[]: ` block per advisory with a `= see:` link to -/// its `/errors/` page, rendered at the severity the project's -/// `[tool.basilisk]` tables resolve for that code and skipped entirely when a -/// table grades it `disabled` ([STUBRES-TYPESHED-CONFIG]). -/// -/// Neither surface is stdout, so these advisories can never enter the JSON -/// diagnostics a conformance run scores ([STUBRES-TYPESHED-WARN]). -fn report_typeshed_status( - status: &basilisk_stubs::typeshed::source::TypeshedStatus, - rule_config: &BasiliskConfig, - banner: &mut impl std::io::Write, -) { - let commit_identity = status - .commit - .map_or_else(|| "not supplied".to_owned(), |identity| identity.to_hex()); - let tree_identity = status - .tree - .map_or_else(|| "not supplied".to_owned(), |identity| identity.to_hex()); - let license_reference = status - .license_reference - .as_deref() - .unwrap_or("not supplied"); - debug!( - active_source = status.active_source.as_str(), - commit_identity, - tree_identity, - license_status = ?status.license_status, - license_reference, - "typeshed source status" - ); - for warning in &status.warnings { - let severity = basilisk_lsp::config::resolve_status_severity( - rule_config, - &warning.code, - warning.severity, - ); - if severity == RuleSeverity::Disabled { - debug!( - warning_code = warning.code, - "typeshed source advisory silenced by config" - ); - continue; - } - let _ = writeln!( - banner, - "{}[{}]: {}", - severity.as_str(), - warning.code, - warning.message - ); - let _ = writeln!(banner, " = see: {}", warning.docs_url); - } -} - -/// Build a uv package registry from workspace roots, if this is a uv project. -fn build_uv_registry( - roots: &[std::path::PathBuf], -) -> Option> { - let uv_info = basilisk_uv::detect_uv_project(roots)?; - - if !uv_info.has_lockfile { - info!( - root = %uv_info.root.display(), - "uv project detected but no uv.lock — skipping registry" - ); - return None; - } - - let lock_path = uv_info.root.join("uv.lock"); - let lock_file = match basilisk_uv::parse_lock_file(&lock_path) { - Ok(lock) => lock, - Err(err) => { - warn!( - path = %lock_path.display(), - %err, - "failed to parse uv.lock — package registry unavailable" - ); - return None; - } - }; - - let deps = basilisk_uv::extract_pyproject_deps(&uv_info.root); - let registry = basilisk_uv::PackageRegistry::from_lock_file(&lock_file, &deps); - - let pkg_count = registry.all_packages().count(); - info!( - root = %uv_info.root.display(), - packages = pkg_count, - direct_deps = deps.len(), - "built uv package registry" - ); - - Some(std::sync::Arc::new(registry)) -} - -#[cfg(test)] -mod tests { - use std::sync::{Arc, Mutex}; - - use super::{build_uv_registry, load_cli_workspace_config, report_typeshed_status}; - - #[derive(Clone, Default)] - struct Capture(Arc>>); - - struct CaptureWriter(Arc>>); - - impl std::io::Write for CaptureWriter { - fn write(&mut self, bytes: &[u8]) -> std::io::Result { - let mut output = self - .0 - .lock() - .map_err(|error| std::io::Error::other(error.to_string()))?; - output.extend_from_slice(bytes); - Ok(bytes.len()) - } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } - } - - impl<'writer> tracing_subscriber::fmt::MakeWriter<'writer> for Capture { - type Writer = CaptureWriter; - - fn make_writer(&'writer self) -> Self::Writer { - CaptureWriter(Arc::clone(&self.0)) - } - } - - impl Capture { - fn text(&self) -> Result> { - let bytes = self - .0 - .lock() - .map_err(|error| std::io::Error::other(error.to_string()))? - .clone(); - Ok(String::from_utf8(bytes)?) - } - } - - #[test] - fn cli_detected_python_version_fills_missing_analysis_target( - ) -> Result<(), Box> { - let project = tempfile::tempdir()?; - std::fs::write(project.path().join(".python-version"), "3.12\n")?; - let detected = basilisk_uv::python_version::resolve_target_python_version(project.path()); - let config = load_cli_workspace_config(project.path(), detected.as_deref()); - assert_eq!(config.python_version.as_deref(), Some("3.12")); - Ok(()) - } - - #[test] - fn explicit_analysis_target_wins_over_cli_detected_version( - ) -> Result<(), Box> { - let project = tempfile::tempdir()?; - std::fs::write( - project.path().join("pyproject.toml"), - "[tool.basilisk]\npython-version = \"3.10\"\n", - )?; - let config = load_cli_workspace_config(project.path(), Some("3.12")); - assert_eq!(config.python_version.as_deref(), Some("3.10")); - Ok(()) - } - - #[cfg(unix)] - #[test] - fn selected_interpreter_supplies_platform_target_evidence( - ) -> Result<(), Box> { - use std::os::unix::fs::PermissionsExt; - - let project = tempfile::tempdir()?; - let interpreter = project.path().join("python"); - std::fs::write(&interpreter, "#!/bin/sh\nprintf 'fixture-platform\\n'\n")?; - std::fs::set_permissions(&interpreter, std::fs::Permissions::from_mode(0o755))?; - std::fs::write( - project.path().join("pyproject.toml"), - format!("[tool.basilisk]\npython = '{}'\n", interpreter.display()), - )?; - - let config = load_cli_workspace_config(project.path(), None); - - assert_eq!( - config.python_platform.as_deref(), - Some("fixture-platform"), - "an explicitly selected interpreter is real target evidence for sys.platform" - ); - Ok(()) - } - - #[cfg(unix)] - #[test] - fn explicit_all_platform_keeps_cross_platform_target() -> Result<(), Box> - { - use std::os::unix::fs::PermissionsExt; - - let project = tempfile::tempdir()?; - let interpreter = project.path().join("python"); - std::fs::write(&interpreter, "#!/bin/sh\nprintf 'fixture-platform\\n'\n")?; - std::fs::set_permissions(&interpreter, std::fs::Permissions::from_mode(0o755))?; - std::fs::write( - project.path().join("pyproject.toml"), - format!( - "[tool.basilisk]\npython = '{}'\npython-platform = 'All'\n", - interpreter.display() - ), - )?; - - let config = load_cli_workspace_config(project.path(), None); - - assert_eq!(config.python_platform.as_deref(), Some("All")); - Ok(()) - } - - #[test] - fn uv_detection_without_a_lockfile_does_not_build_a_registry( - ) -> Result<(), Box> { - let project = tempfile::tempdir()?; - std::fs::write(project.path().join(".python-version"), "3.13\n")?; - - assert!(build_uv_registry(&[project.path().to_path_buf()]).is_none()); - Ok(()) - } - - #[test] - fn malformed_uv_lockfile_does_not_build_a_partial_registry( - ) -> Result<(), Box> { - let project = tempfile::tempdir()?; - std::fs::write(project.path().join("uv.lock"), "not valid TOML = [")?; - - assert!(build_uv_registry(&[project.path().to_path_buf()]).is_none()); - Ok(()) - } - - #[test] - fn valid_uv_lockfile_builds_the_registry() -> Result<(), Box> { - let project = tempfile::tempdir()?; - std::fs::write( - project.path().join("uv.lock"), - "version = 1\n\n[[package]]\nname = 'example'\nversion = '1.0.0'\n", - )?; - - let registry = build_uv_registry(&[project.path().to_path_buf()]) - .ok_or("valid uv.lock should build a registry")?; - assert_eq!(registry.all_packages().count(), 1); - Ok(()) - } - - fn composed_status( - ) -> Result> { - use basilisk_stubs::typeshed::source::StatusWarning; - use basilisk_stubs::typeshed::warning::{TypeshedWarning, UnpinnedKind}; - - let mut status = basilisk_stubs::typeshed::bundle::bundled_snapshot()?.status; - status.warnings = StatusWarning::list(&[ - TypeshedWarning::LicenseChanged, - TypeshedWarning::UserManaged, - TypeshedWarning::Unpinned(UnpinnedKind::CustomFolder), - ]); - Ok(status) - } - - /// [STUBRES-TYPESHED-WARN]: the human banner reads like every other Basilisk - /// diagnostic — `[]: ` plus a `= see:` - /// deep link — in canonical status-table order, NOT `key="VALUE"` telemetry. - #[test] - fn composed_status_warnings_render_as_ordered_banner_diagnostics( - ) -> Result<(), Box> { - let status = composed_status()?; - let config = basilisk_config::BasiliskConfig::default(); - let mut banner = Vec::new(); - report_typeshed_status(&status, &config, &mut banner); - let banner = String::from_utf8(banner)?; - - // Advisory conditions default to `warning`; the elevated license change - // keeps its intrinsic `error` default ([STUBRES-TYPESHED-CONFIG]). - for header in [ - "warning[typeshed_source_unpinned]:", - "warning[typeshed_source_user_managed]:", - "error[typeshed_source_license_changed]:", - ] { - assert!( - banner.contains(header), - "missing banner header `{header}`: {banner}" - ); - } - // Every advisory deep-links to its own /errors/ page. - assert!( - banner - .matches("= see: https://www.basilisk-python.dev/errors/typeshed_source_") - .count() - == 3, - "each advisory must carry its own = see: link: {banner}" - ); - // Canonical status-table order is preserved on the banner. - let unpinned = banner.find("typeshed_source_unpinned"); - let user_managed = banner.find("typeshed_source_user_managed"); - let license = banner.find("typeshed_source_license_changed"); - assert!( - unpinned - .zip(user_managed) - .zip(license) - .is_some_and(|((first, second), third)| first < second && second < third), - "status warnings must stay in canonical order: {banner}" - ); - // The old `key="VALUE"` telemetry spelling must never resurface on the - // human banner. - assert!( - !banner.contains("warning_code=") && !banner.contains("warning_message="), - "banner must not read like CLI-arg telemetry: {banner}" - ); - Ok(()) - } - - /// [STUBRES-TYPESHED-CONFIG]: these advisories are configured exactly like - /// any Basilisk rule — a `[tool.basilisk.rules]` entry raises the severity - /// the banner prints, and grading a code `off` silences it entirely. - #[test] - fn config_tables_set_banner_severity_and_can_silence_advisories( - ) -> Result<(), Box> { - let status = composed_status()?; - let project = tempfile::tempdir()?; - std::fs::write( - project.path().join("pyproject.toml"), - "[tool.basilisk.rules]\n\ - \"typeshed_source_unpinned\" = \"error\"\n\ - \"typeshed_source_user_managed\" = \"off\"\n", - )?; - let config = basilisk_config::load_basilisk_config(project.path()); - - let mut banner = Vec::new(); - report_typeshed_status(&status, &config, &mut banner); - let banner = String::from_utf8(banner)?; - - assert!( - banner.contains("error[typeshed_source_unpinned]:"), - "a `[tool.basilisk.rules]` entry must raise the banner severity: {banner}" - ); - assert!( - !banner.contains("typeshed_source_user_managed"), - "grading a code `off` must silence its advisory: {banner}" - ); - // A code with no entry keeps its intrinsic default (license drift = error). - assert!( - banner.contains("error[typeshed_source_license_changed]:"), - "an elevated advisory keeps its `error` default: {banner}" - ); - Ok(()) - } - - /// [STUBRES-TYPESHED-WARN]: the machine identity fields stay on the - /// structured `debug` telemetry surface (log file / Output Channel), never - /// on the human banner. - #[test] - fn status_reporting_emits_structured_debug_telemetry() -> Result<(), Box> - { - let mut status = basilisk_stubs::typeshed::bundle::bundled_snapshot()?.status; - status.commit = None; - status.tree = None; - status.license_reference = None; - status.warnings.clear(); - let config = basilisk_config::BasiliskConfig::default(); - let capture = Capture::default(); - let subscriber = tracing_subscriber::fmt() - .with_ansi(false) - .without_time() - .with_target(false) - .with_max_level(tracing::Level::DEBUG) - .with_writer(capture.clone()) - .finish(); - - let mut banner = Vec::new(); - tracing::subscriber::with_default(subscriber, || { - report_typeshed_status(&status, &config, &mut banner); - }); - - let telemetry = capture.text()?; - assert!(telemetry.contains("typeshed source status"), "{telemetry}"); - for field in [ - "commit_identity=\"not supplied\"", - "tree_identity=\"not supplied\"", - "license_reference=\"not supplied\"", - ] { - assert!( - telemetry.contains(field), - "missing `{field}` in: {telemetry}" - ); - } - Ok(()) - } -} diff --git a/crates/basilisk-cli/src/stubs.rs b/crates/basilisk-cli/src/stubs.rs deleted file mode 100644 index 4375c84b6..000000000 --- a/crates/basilisk-cli/src/stubs.rs +++ /dev/null @@ -1,426 +0,0 @@ -//! Stub-management CLI implementation for [STUBRES-AUTOGEN]. - -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; - -use basilisk_resolver::{ImportInfo, ImportResolution}; -use basilisk_stubs::generate::{self, GeneratedStub, StubGenError, StubGenMode}; -use clap::{Args, Subcommand}; -use colored::Colorize as _; -use tracing::info; - -/// Stub management subcommands. -#[derive(Subcommand)] -pub(super) enum StubAction { - /// Generate best-effort `.pyi` stubs for untyped packages. - Generate { - /// Package names to generate stubs for. - packages: Vec, - /// Generate stubs for every untyped import in the project. - #[arg(long, conflicts_with = "packages")] - all: bool, - /// Generation mode: runtime, ast, or hybrid (default). - #[arg(long, default_value = "hybrid")] - mode: StubGenModeArg, - /// Path to the Python interpreter. - #[arg(long, default_value = "python3")] - python: String, - }, - /// Show stub coverage status for the project. - Status, -} - -/// Arguments accepted by the Pyright-compatible `--createstub` alias. -#[derive(Args)] -pub(super) struct CreateStubArgs { - /// Package name to generate a stub for. - package: String, - /// Generation mode: runtime, ast, or hybrid (default). - #[arg(long, default_value = "hybrid")] - mode: StubGenModeArg, - /// Path to the Python interpreter. - #[arg(long, default_value = "python3")] - python: String, -} - -/// CLI-friendly stub generation mode. -#[derive(Clone, Copy, Debug, clap::ValueEnum)] -pub(super) enum StubGenModeArg { - /// Generate through runtime introspection. - Runtime, - /// Generate by parsing package source. - Ast, - /// Prefer runtime introspection and fall back to source parsing. - Hybrid, -} - -impl From for StubGenMode { - fn from(mode: StubGenModeArg) -> Self { - match mode { - StubGenModeArg::Runtime => Self::Runtime, - StubGenModeArg::Ast => Self::Ast, - StubGenModeArg::Hybrid => Self::Hybrid, - } - } -} - -struct GenerationTarget { - module: String, - source_path: Option, -} - -/// Run a nested `stubs` command. -pub(super) fn run(action: StubAction) -> u8 { - match action { - StubAction::Generate { - packages, - all, - mode, - python, - } => run_generate(&packages, all, mode, &python), - StubAction::Status => run_status(), - } -} - -/// Map Pyright's top-level spelling to the named-package generation workflow. -// Implements [STUBRES-AUTOGEN]: `basilisk --createstub X` and -// `basilisk stubs generate X` share one backend and output contract. -pub(super) fn run_create_stub(args: CreateStubArgs) -> u8 { - run_generate(&[args.package], false, args.mode, &args.python) -} - -fn run_generate(packages: &[String], all: bool, mode: StubGenModeArg, python: &str) -> u8 { - let project_root = crate::pipeline::find_project_root(Path::new(".")); - let python_path = Path::new(python); - let targets = match generation_targets(packages, all, python_path, &project_root) { - Ok(targets) => targets, - Err(message) => { - eprintln!("{}: {message}", "error".red()); - return 1; - } - }; - if targets.is_empty() { - println!("No untyped imports found"); - return 0; - } - let cache_dir = project_root.join(generate::cache::DEFAULT_CACHE_DIR); - let mut failed = false; - for target in &targets { - failed |= !generate_target(target, mode.into(), python_path, &cache_dir); - } - u8::from(failed) -} - -fn generation_targets( - packages: &[String], - all: bool, - python_path: &Path, - project_root: &Path, -) -> Result, String> { - if all && !packages.is_empty() { - return Err("--all cannot be combined with package names".to_owned()); - } - if all { - return discover_untyped_imports(project_root); - } - if packages.is_empty() { - return Err("specify package names or use --all".to_owned()); - } - Ok(packages - .iter() - .map(|module| GenerationTarget { - module: module.clone(), - source_path: find_package_source(module, python_path), - }) - .collect()) -} - -// Implements [STUBRES-AUTOGEN]: scan the configured project inputs with the -// same parser, resolver, exclusions, and import search paths as `check`, then -// generate only imports that resolve to untyped site-packages source. -fn discover_untyped_imports(project_root: &Path) -> Result, String> { - let config = basilisk_config::load_basilisk_config(project_root); - let excluded = crate::pipeline::excluded_dirs_and_log(&config, project_root); - let paths = crate::pipeline::effective_check_paths(&[], &config, project_root); - let files = crate::pipeline::collect_python_files(&paths, &excluded)?; - let roots = crate::pipeline::analysis_roots(&paths, project_root); - let search_paths = crate::pipeline::build_import_search_paths(roots, project_root); - let Some(site_packages) = search_paths.site_packages.as_deref() else { - return Ok(Vec::new()); - }; - let mut targets = BTreeMap::new(); - for file in files { - collect_file_targets(&file, &search_paths, site_packages, &mut targets)?; - } - Ok(targets - .into_iter() - .map(|(module, source_path)| GenerationTarget { - module, - source_path: Some(source_path), - }) - .collect()) -} - -fn collect_file_targets( - file: &str, - search_paths: &basilisk_lsp::import_resolver::ImportSearchPaths, - site_packages: &Path, - targets: &mut BTreeMap, -) -> Result<(), String> { - let (resolved, _) = crate::pipeline::resolve_file_imports(file, search_paths, None)?; - resolved - .imports - .iter() - .filter(|import| is_untyped_third_party_import(import, site_packages)) - .filter_map(|import| { - import - .resolved_path - .as_ref() - .map(|path| (import.module.clone(), path.clone())) - }) - .for_each(|(module, path)| { - let _ = targets.entry(module).or_insert(path); - }); - Ok(()) -} - -fn is_untyped_third_party_import(import: &ImportInfo, site_packages: &Path) -> bool { - import.resolution == ImportResolution::SourcePy - && import.resolved_path.as_ref().is_some_and(|path| { - path.starts_with(site_packages) && !basilisk_stubs::has_py_typed_marker(path) - }) -} - -fn generate_target( - target: &GenerationTarget, - mode: StubGenMode, - python_path: &Path, - cache_dir: &Path, -) -> bool { - let result = match target.source_path.as_deref() { - Some(source) => { - info!(module = %target.module, source = %source.display(), "generating stubs"); - generate::generate_stubs(&target.module, source, python_path, mode) - } - None if mode == StubGenMode::Ast => { - eprintln!( - "{} Cannot find source for `{}` — AST mode requires source files", - "✗".red(), - target.module - ); - return false; - } - None => generate::runtime::generate_runtime_stubs(&target.module, python_path), - }; - cache_generation_result(cache_dir, &target.module, result) -} - -fn cache_generation_result( - cache_dir: &Path, - package: &str, - result: Result, -) -> bool { - match result { - Ok(stub) => cache_stub(cache_dir, package, &stub), - Err(error) => { - eprintln!( - "{} Failed to generate stub for `{package}`: {error}", - "✗".red() - ); - false - } - } -} - -/// Cache a generated stub and print the result. -pub(super) fn cache_stub(cache_dir: &Path, package: &str, stub: &GeneratedStub) -> bool { - // A declaration-free stub carries no type information. Writing it would - // report a false "✓ Generated" success AND let the empty `.pyi` satisfy - // BSK-0152 as though the module were typed (GitHub #336). Surface it as a - // warning and write nothing — there is no stub worth caching. - if !stub.has_declarations() { - println!( - "{} `{package}` exposed no introspectable public API — no stub written", - "⚠".yellow() - ); - return true; - } - - let source_hash = generate::cache::hash_source(&stub.pyi_content); - match generate::cache::write_cache(cache_dir, package, &stub.pyi_content, source_hash) { - Ok(path) => { - println!( - "{} Generated stub for `{package}` → {}", - "✓".green(), - path.display() - ); - true - } - Err(error) => { - eprintln!( - "{} Failed to write stub for `{package}`: {error}", - "✗".red() - ); - false - } - } -} - -/// Import a module named by `sys.argv[1]` and print its source path. -const FIND_PACKAGE_SOURCE_SCRIPT: &str = r#" -import importlib -import sys - -module = importlib.import_module(sys.argv[1]) -source = getattr(module, "__file__", None) -if source is None: - raise SystemExit(1) -print(source) -"#; - -fn is_valid_module_name(name: &str) -> bool { - name.split('.').all(|component| { - let mut chars = component.chars(); - chars - .next() - .is_some_and(|first| first == '_' || first.is_ascii_alphabetic()) - && chars.all(|character| character == '_' || character.is_ascii_alphanumeric()) - }) -} - -/// Find the source path for an installed package by querying Python. -pub(super) fn find_package_source(package: &str, python_path: &Path) -> Option { - if !is_valid_module_name(package) { - return None; - } - let output = std::process::Command::new(python_path) - .args(["-c", FIND_PACKAGE_SOURCE_SCRIPT, package]) - .output() - .ok()?; - if !output.status.success() { - return None; - } - let source = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim()); - source.is_file().then_some(source).filter(|path| { - path.extension() - .is_some_and(|extension| extension.eq_ignore_ascii_case("py")) - }) -} - -fn run_status() -> u8 { - let project_root = crate::pipeline::find_project_root(Path::new(".")); - print_status(&project_root.join(generate::cache::DEFAULT_CACHE_DIR)) -} - -fn print_status(cache_dir: &Path) -> u8 { - if !cache_dir.exists() { - println!("No generated stubs found ({})", cache_dir.display()); - return 0; - } - let modules: Vec = walkdir::WalkDir::new(cache_dir) - .into_iter() - .filter_map(Result::ok) - .filter(|entry| entry.file_type().is_file()) - .filter_map(|entry| stub_module_name(entry.path(), cache_dir)) - .collect(); - for module in &modules { - println!(" {} {module}", "✓".green()); - } - if modules.is_empty() { - println!("No generated stubs found"); - } else { - println!( - "\n{} generated stub(s) in {}", - modules.len(), - cache_dir.display() - ); - } - 0 -} - -fn stub_module_name(path: &Path, cache_dir: &Path) -> Option { - (path.extension()? == "pyi").then(|| { - path.strip_prefix(cache_dir) - .unwrap_or(path) - .with_extension("") - .components() - .map(|component| component.as_os_str().to_string_lossy()) - .collect::>() - .join(".") - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// [STUBRES-AUTOGEN]: `stubs status` succeeds whether or not anything was - /// ever generated — an empty project is a clean report, not an error. - #[test] - fn status_reports_cleanly_with_and_without_generated_stubs( - ) -> Result<(), Box> { - let project = tempfile::tempdir()?; - let cache_dir = project.path().join(generate::cache::DEFAULT_CACHE_DIR); - assert_eq!(print_status(&cache_dir), 0, "missing cache dir is clean"); - - std::fs::create_dir_all(cache_dir.join("pkg"))?; - assert_eq!(print_status(&cache_dir), 0, "empty cache dir is clean"); - - std::fs::write(cache_dir.join("pkg").join("mod.pyi"), "x: int\n")?; - std::fs::write(cache_dir.join("notes.txt"), "not a stub\n")?; - assert_eq!(print_status(&cache_dir), 0, "generated stubs list cleanly"); - Ok(()) - } - - /// Stub paths render as dotted module names relative to the cache root; - /// non-`.pyi` files are not stubs. - #[test] - fn stub_module_names_are_dotted_and_cache_relative() { - let cache = Path::new("/cache"); - assert_eq!( - stub_module_name(Path::new("/cache/pkg/mod.pyi"), cache), - Some("pkg.mod".to_owned()) - ); - assert_eq!( - stub_module_name(Path::new("elsewhere/solo.pyi"), cache), - Some("elsewhere.solo".to_owned()) - ); - assert_eq!(stub_module_name(Path::new("/cache/notes.txt"), cache), None); - assert_eq!( - stub_module_name(Path::new("/cache/no_extension"), cache), - None - ); - } - /// The three CLI mode spellings map one-to-one onto generator modes. - #[test] - fn generation_mode_arguments_map_to_generator_modes() { - assert!(matches!( - StubGenMode::from(StubGenModeArg::Runtime), - StubGenMode::Runtime - )); - assert!(matches!( - StubGenMode::from(StubGenModeArg::Ast), - StubGenMode::Ast - )); - assert!(matches!( - StubGenMode::from(StubGenModeArg::Hybrid), - StubGenMode::Hybrid - )); - } - - /// [STUBRES-AUTOGEN]: contradictory or empty selections are rejected - /// before any interpreter or filesystem work happens. - #[test] - fn generation_target_selection_rejects_contradictory_requests() { - let root = Path::new("/nonexistent-project-root"); - let python = Path::new("python3"); - assert!( - generation_targets(&["requests".to_owned()], true, python, root).is_err(), - "--all plus explicit packages must be rejected" - ); - assert!( - generation_targets(&[], false, python, root).is_err(), - "no packages and no --all must be rejected" - ); - } -} diff --git a/crates/basilisk-cli/src/typeshed_cli.rs b/crates/basilisk-cli/src/typeshed_cli.rs deleted file mode 100644 index ffc71a6f5..000000000 --- a/crates/basilisk-cli/src/typeshed_cli.rs +++ /dev/null @@ -1,550 +0,0 @@ -//! Implements the [STUBRES-TYPESHED-DOWNLOAD] CLI surface: -//! `basilisk typeshed download [--commit | --package >]`. -//! -//! This command — like the editor's Download buttons — is the ONLY way -//! typeshed bytes arrive on a machine ([TYPESHEDRT-SEGREGATION]). `check` and -//! `analyze` never download: a pin that is not in the store tanks hard with -//! `NO SOURCE`, and this command is what that error tells the user to run. - -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; - -use basilisk_typeshed_fetch::{ - DownloadPhase, GithubApi, GithubClient, PackageDownloadPhase, PypiApi, PypiClient, -}; -use colored::Colorize as _; -use tracing::error; - -/// The `basilisk typeshed` action surface. -#[derive(Debug, clap::Subcommand)] -pub(crate) enum TypeshedAction { - /// Download and verify one typeshed source into the content-addressed - /// store. With no flag this resolves the latest `python/typeshed@main` and - /// writes the resolved SHA as the workspace's `typeshed-commit` pin; - /// `--commit` materialises that exact, already-configured pin and writes - /// no configuration; `--package` acquires a `PyPI` typeshed distribution - /// wheel pinned by SHA-256 and writes no configuration. - Download { - /// Exact full 40-hex commit SHA to download (defaults to latest). - #[arg(long, value_name = "SHA", conflicts_with = "package")] - commit: Option, - /// A `PyPI` typeshed distribution pin `name@sha256:` to download - /// and verify into the store. Mutually exclusive with `--commit`. - #[arg(long, value_name = "SPEC", conflicts_with = "commit")] - package: Option, - /// Workspace whose configuration supplies the store location and, for - /// a latest download, receives the pin. - #[arg(long, default_value = ".", value_name = "DIR")] - workspace: PathBuf, - }, -} - -/// Dispatch a `basilisk typeshed` action. Returns the process exit code -/// ([CHKARCH-CLI-EXITCODES]: `0` ok, `2` invalid configuration, `3` failure). -pub(crate) fn run(action: TypeshedAction) -> u8 { - match action { - TypeshedAction::Download { - commit, - package, - workspace, - } => { - if let Some(spec) = package { - run_download_package(&spec, &workspace) - } else { - run_download(commit, &workspace) - } - } - } -} - -fn run_download(commit: Option, workspace: &Path) -> u8 { - let client = GithubClient::new(); - download_action(commit, workspace, &client) -} - -fn run_download_package(spec: &str, workspace: &Path) -> u8 { - let client = PypiClient::new(); - download_package_action(spec, workspace, &client) -} - -/// The download action with its transport injected, so tests drive the whole -/// surface — config discovery, store resolution, progress — offline. -fn download_action(commit: Option, workspace: &Path, api: &dyn GithubApi) -> u8 { - let config = basilisk_lsp::config::load_analysis_config(workspace); - let store = config.typeshed_store_path; - let progress = |phase: DownloadPhase| println!(" {}", phase_label(phase).dimmed()); - match commit { - Some(sha) => download_exact(&sha, store, api, &progress), - None => download_latest_and_pin(workspace, store, api, &progress), - } -} - -/// The `--package` download with its transport injected, so tests drive the -/// whole surface — config discovery, store resolution, progress — offline. -/// Writes no configuration: the pin (`typeshed-package`) is the caller's -/// contract, exactly like `--commit` ([STUBRES-TYPESHED-DOWNLOAD]). -fn download_package_action(spec: &str, workspace: &Path, api: &dyn PypiApi) -> u8 { - let (name, sha256) = match basilisk_config::parse_typeshed_package(spec) { - Ok(parsed) => parsed, - Err(message) => { - error!(spec, reason = %message, "--package must be name@sha256:<64-hex>"); - return 2; - } - }; - let config = basilisk_lsp::config::load_analysis_config(workspace); - let store = config.typeshed_store_path; - let progress = - |phase: PackageDownloadPhase| println!(" {}", package_phase_label(phase).dimmed()); - println!("Downloading {name}@sha256:{sha256} into the verified store…"); - match basilisk_typeshed_fetch::download_package(&name, &sha256, store, api, &progress) { - Ok(()) => { - println!( - "{} {}@sha256:{sha256} is now available offline", - "ok:".green().bold(), - name - ); - 0 - } - Err(download_error) => { - error!(%download_error, "typeshed download failed; nothing was written"); - 3 - } - } -} - -fn download_exact( - sha: &str, - store: Option, - api: &dyn GithubApi, - progress: &dyn Fn(DownloadPhase), -) -> u8 { - let Ok(commit) = basilisk_stubs::typeshed::gittree::Oid::from_hex(sha) else { - error!( - len = sha.len(), - "--commit must be a full 40-character hex SHA" - ); - return 2; - }; - println!("Downloading typeshed {commit} into the verified store…"); - match basilisk_typeshed_fetch::download_commit(commit, store, api, progress) { - Ok(outcome) => { - println!( - "{} {} (tree {}) is now available offline", - "ok:".green().bold(), - outcome.commit, - outcome.tree - ); - 0 - } - Err(download_error) => { - error!(%download_error, "typeshed download failed; nothing was written"); - 3 - } - } -} - -fn download_latest_and_pin( - workspace: &Path, - store: Option, - api: &dyn GithubApi, - progress: &dyn Fn(DownloadPhase), -) -> u8 { - println!("Downloading the latest python/typeshed commit into the verified store…"); - let outcome = match basilisk_typeshed_fetch::download_latest(store, api, progress) { - Ok(outcome) => outcome, - Err(download_error) => { - error!(%download_error, "typeshed download failed; nothing was written"); - return 3; - } - }; - match write_pin(workspace, &outcome.commit.to_hex()) { - Ok(()) => { - println!( - "{} pinned typeshed-commit = {}", - "ok:".green().bold(), - outcome.commit - ); - 0 - } - Err(config_error) => { - // The store entry is verified and kept — only the pin write - // failed, so the command is re-runnable without a re-download. - error!(%config_error, commit = %outcome.commit, "downloaded but could not write the pin"); - 2 - } - } -} - -/// Write `typeshed-commit` through the same validated, structure-preserving -/// editor transaction the LSP configuration editor uses ([LSPCFGED-TYPESHED]). -/// -/// The pin and a custom folder are the two mutually exclusive step-3 sources -/// ([STUBRES-TYPESHED]), so the same transaction retires `typeshed-path` — -/// byte for byte the update the LSP's Download latest button writes -/// (`pin_update` in `crates/basilisk-lsp/src/typeshed_download.rs`). Without -/// the retirement the patch would name both sources and validation would -/// reject the whole write, leaving a downloaded commit unpinned. -fn write_pin(workspace: &Path, sha: &str) -> Result<(), basilisk_config::ConfigDocumentError> { - let document = basilisk_config::discover_config_document(workspace)?; - let update = basilisk_config::ConfigurationUpdate { - rules: basilisk_config::RuleConfigUpdate::default(), - typeshed: basilisk_config::TypeshedConfigUpdate { - entries: BTreeMap::from([ - ( - basilisk_config::TypeshedConfigKey::TypeshedCommit, - Some(sha.to_owned()), - ), - (basilisk_config::TypeshedConfigKey::TypeshedPath, None), - ]), - }, - cache: basilisk_config::CacheConfigUpdate::default(), - }; - let patch = basilisk_config::build_configuration_patch(&document, &update)?; - basilisk_config::apply_config_patch(&patch) -} - -const fn phase_label(phase: DownloadPhase) -> &'static str { - match phase { - DownloadPhase::Resolving => "resolving commit metadata", - DownloadPhase::FetchingTree => "fetching the trusted file tree", - DownloadPhase::FetchingArchive => "downloading the archive", - DownloadPhase::Verifying => "verifying against the commit identity", - DownloadPhase::Writing => "writing the store entry", - } -} - -const fn package_phase_label(phase: PackageDownloadPhase) -> &'static str { - match phase { - PackageDownloadPhase::Resolving => "resolving the package index", - PackageDownloadPhase::Verifying => "verifying the wheel against the pin", - PackageDownloadPhase::Writing => "writing the store entry", - } -} - -#[cfg(test)] -mod tests { - use basilisk_typeshed_fetch::testing::{ - fake_repo, fake_wheel, FakeApi, FakePypiApi, Faults, PypiFaults, - }; - - use super::*; - - /// [STUBRES-TYPESHED-DOWNLOAD]: the pin write is the same validated - /// editor transaction the configuration editor uses — structure - /// preserved, full SHA required. - #[test] - fn write_pin_round_trips_through_the_validated_editor() -> Result<(), Box> - { - let dir = tempfile::tempdir()?; - std::fs::write( - dir.path().join("pyproject.toml"), - "# keep\n[project]\nname = \"demo\"\n\n[tool.basilisk]\n", - )?; - write_pin(dir.path(), "83c2518a9e6abbda0c44592c3483de459198f887")?; - let written = std::fs::read_to_string(dir.path().join("pyproject.toml"))?; - assert!(written.contains("# keep")); - assert!( - written.contains("typeshed-commit = \"83c2518a9e6abbda0c44592c3483de459198f887\""), - "pin must be written: {written}" - ); - - assert!( - write_pin(dir.path(), "not-a-sha").is_err(), - "a malformed SHA must be rejected by validation, never written" - ); - Ok(()) - } - - /// [STUBRES-TYPESHED-DOWNLOAD]: pinning retires a custom folder in the - /// same transaction, exactly like the LSP's Download latest action. The - /// two step-3 sources are mutually exclusive, so a write that kept both - /// would be rejected outright and the download would end up unpinned. - #[test] - fn write_pin_retires_a_custom_typeshed_path() -> Result<(), Box> { - let dir = tempfile::tempdir()?; - std::fs::write( - dir.path().join("pyproject.toml"), - "[tool.basilisk]\ntypeshed-path = \"vendor/typeshed\"\ntypeshed-store-path = \"store\"\n", - )?; - write_pin(dir.path(), "83c2518a9e6abbda0c44592c3483de459198f887")?; - let written = std::fs::read_to_string(dir.path().join("pyproject.toml"))?; - assert!( - written.contains("typeshed-commit = \"83c2518a9e6abbda0c44592c3483de459198f887\""), - "the resolved pin must be written: {written}" - ); - assert!( - !written.contains("typeshed-path"), - "the custom folder must be retired by the same write: {written}" - ); - assert!( - written.contains("typeshed-store-path = \"store\""), - "unrelated typeshed settings must survive untouched: {written}" - ); - Ok(()) - } - - #[test] - fn a_malformed_commit_argument_is_a_configuration_error() { - let api = FakeApi::new(fake_repo()); - // No transport is touched: the SHA fails validation before any request. - assert_eq!(download_exact("short", None, &api, &|_phase| {}), 2); - } - - /// The `run` dispatch reaches the same validation: a malformed pin exits - /// `2` before any transport work. - #[test] - fn run_rejects_a_malformed_sha_through_the_dispatch() -> Result<(), Box> - { - let dir = tempfile::tempdir()?; - let action = TypeshedAction::Download { - commit: Some("short".to_owned()), - package: None, - workspace: dir.path().to_path_buf(), - }; - assert_eq!(run(action), 2); - Ok(()) - } - - /// `download --commit ` materialises the exact pin into the store and - /// writes no configuration ([STUBRES-TYPESHED-DOWNLOAD]). - #[test] - fn download_exact_materialises_the_pin_into_the_store() -> Result<(), Box> - { - let store = tempfile::tempdir()?; - let api = FakeApi::new(fake_repo()); - let sha = api.repo.commit.to_hex(); - assert_eq!( - download_exact(&sha, Some(store.path().to_path_buf()), &api, &|_phase| {}), - 0 - ); - assert_eq!( - std::fs::read_dir(store.path())?.count(), - 1, - "exactly one verified store entry must exist" - ); - Ok(()) - } - - /// A transport failure is exit `3` and writes nothing. - #[test] - fn a_transport_failure_downloading_an_exact_pin_is_exit_3( - ) -> Result<(), Box> { - let store = tempfile::tempdir()?; - let mut api = FakeApi::new(fake_repo()); - api.faults = Faults { - resolve_fails: true, - ..Faults::default() - }; - let sha = api.repo.commit.to_hex(); - assert_eq!( - download_exact(&sha, Some(store.path().to_path_buf()), &api, &|_phase| {}), - 3 - ); - assert_eq!(std::fs::read_dir(store.path())?.count(), 0); - Ok(()) - } - - /// `download` with no `--commit` resolves latest, stores it, and pegs the - /// resolved SHA as the workspace's `typeshed-commit` pin. - #[test] - fn download_latest_pins_the_resolved_sha() -> Result<(), Box> { - let workspace = tempfile::tempdir()?; - let store = tempfile::tempdir()?; - std::fs::write(workspace.path().join("pyproject.toml"), "[tool.basilisk]\n")?; - let api = FakeApi::new(fake_repo()); - assert_eq!( - download_latest_and_pin( - workspace.path(), - Some(store.path().to_path_buf()), - &api, - &|_phase| {} - ), - 0 - ); - let written = std::fs::read_to_string(workspace.path().join("pyproject.toml"))?; - assert!( - written.contains(&format!("typeshed-commit = \"{}\"", api.repo.commit)), - "the resolved SHA must be pegged as the pin: {written}" - ); - Ok(()) - } - - /// A failed latest download is exit `3` and leaves the configuration - /// untouched — no pin without verified bytes. - #[test] - fn a_failed_latest_download_writes_no_pin() -> Result<(), Box> { - let workspace = tempfile::tempdir()?; - let store = tempfile::tempdir()?; - std::fs::write(workspace.path().join("pyproject.toml"), "[tool.basilisk]\n")?; - let mut api = FakeApi::new(fake_repo()); - api.faults = Faults { - archive_fails: true, - ..Faults::default() - }; - assert_eq!( - download_latest_and_pin( - workspace.path(), - Some(store.path().to_path_buf()), - &api, - &|_phase| {} - ), - 3 - ); - let written = std::fs::read_to_string(workspace.path().join("pyproject.toml"))?; - assert!( - !written.contains("typeshed-commit"), - "no pin may be written for a failed download: {written}" - ); - Ok(()) - } - - /// A pin-write failure after a successful download is exit `2`; the store - /// entry is kept so re-running needs no re-download. - #[cfg(unix)] - #[test] - fn a_pin_write_failure_after_download_is_a_configuration_error( - ) -> Result<(), Box> { - use std::os::unix::fs::PermissionsExt as _; - let workspace = tempfile::tempdir()?; - let store = tempfile::tempdir()?; - std::fs::write(workspace.path().join("pyproject.toml"), "[tool.basilisk]\n")?; - let api = FakeApi::new(fake_repo()); - std::fs::set_permissions(workspace.path(), std::fs::Permissions::from_mode(0o555))?; - let exit = download_latest_and_pin( - workspace.path(), - Some(store.path().to_path_buf()), - &api, - &|_phase| {}, - ); - std::fs::set_permissions(workspace.path(), std::fs::Permissions::from_mode(0o755))?; - assert_eq!(exit, 2); - assert_eq!( - std::fs::read_dir(store.path())?.count(), - 1, - "the verified store entry must survive the failed pin write" - ); - Ok(()) - } - - /// The full action surface offline: config discovery resolves the - /// workspace-relative store, the latest commit lands there, and the pin is - /// pegged — the exact flow `basilisk typeshed download` runs. - #[test] - fn download_action_resolves_the_store_from_workspace_config( - ) -> Result<(), Box> { - let workspace = tempfile::tempdir()?; - std::fs::write( - workspace.path().join("pyproject.toml"), - "[tool.basilisk]\ntypeshed-store-path = \"store\"\n", - )?; - let api = FakeApi::new(fake_repo()); - assert_eq!(download_action(None, workspace.path(), &api), 0); - assert_eq!( - std::fs::read_dir(workspace.path().join("store"))?.count(), - 1, - "the store entry must land in the config-resolved location" - ); - Ok(()) - } - - /// Every phase renders a distinct, human-readable progress label. - #[test] - fn every_download_phase_has_a_distinct_label() { - let labels = [ - phase_label(DownloadPhase::Resolving), - phase_label(DownloadPhase::FetchingTree), - phase_label(DownloadPhase::FetchingArchive), - phase_label(DownloadPhase::Verifying), - phase_label(DownloadPhase::Writing), - ]; - let unique: std::collections::BTreeSet<&str> = labels.iter().copied().collect(); - assert_eq!(unique.len(), labels.len()); - assert!(labels.iter().all(|label| !label.is_empty())); - } - - /// [STUBRES-TYPESHED-PYPI]: `basilisk typeshed download --package - /// >` acquires the wheel, verifies it, and writes no - /// configuration (the pin is the caller's contract). - #[test] - fn download_package_materialises_the_wheel_into_the_store( - ) -> Result<(), Box> { - let workspace = tempfile::tempdir()?; - let store = tempfile::tempdir()?; - std::fs::write( - workspace.path().join("pyproject.toml"), - format!( - "[tool.basilisk]\ntypeshed-store-path = \"{}\"\n", - store.path().display() - ), - )?; - let api = FakePypiApi::new(fake_wheel()); - let spec = format!("micropython-stdlib-stubs@sha256:{}", api.sha256); - assert_eq!(download_package_action(&spec, workspace.path(), &api), 0); - assert_eq!( - std::fs::read_dir(store.path())?.count(), - 1, - "exactly one verified store entry must exist" - ); - // No configuration is written for a package download. - let written = std::fs::read_to_string(workspace.path().join("pyproject.toml"))?; - assert!( - !written.contains("typeshed-package"), - "a package download must not write a pin: {written}" - ); - Ok(()) - } - - /// A malformed `--package` spec is a configuration error (exit `2`) before - /// any transport work — the parser the config surface shares validates it. - #[test] - fn a_malformed_package_spec_is_a_configuration_error() -> Result<(), Box> - { - let api = FakePypiApi::new(fake_wheel()); - let workspace = tempfile::tempdir()?; - assert_eq!( - download_package_action("not-a-spec", workspace.path(), &api), - 2 - ); - Ok(()) - } - - /// A download failure is exit `3` and writes nothing. - #[test] - fn a_package_download_failure_writes_nothing() -> Result<(), Box> { - let workspace = tempfile::tempdir()?; - let store = tempfile::tempdir()?; - std::fs::write( - workspace.path().join("pyproject.toml"), - format!( - "[tool.basilisk]\ntypeshed-store-path = \"{}\"\n", - store.path().display() - ), - )?; - let mut api = FakePypiApi::new(fake_wheel()); - api.faults = PypiFaults { - download_fails: true, - ..PypiFaults::default() - }; - let spec = format!("micropython-stdlib-stubs@sha256:{}", api.sha256); - assert_eq!(download_package_action(&spec, workspace.path(), &api), 3); - assert_eq!( - std::fs::read_dir(store.path())?.count(), - 0, - "nothing may be written on failure" - ); - Ok(()) - } - - /// Every `PyPI`-package phase renders a distinct, human-readable label. - #[test] - fn every_package_download_phase_has_a_distinct_label() { - let labels = [ - package_phase_label(PackageDownloadPhase::Resolving), - package_phase_label(PackageDownloadPhase::Verifying), - package_phase_label(PackageDownloadPhase::Writing), - ]; - let unique: std::collections::BTreeSet<&str> = labels.iter().copied().collect(); - assert_eq!(unique.len(), labels.len()); - assert!(labels.iter().all(|label| !label.is_empty())); - } -} diff --git a/docs/INDEX.md b/docs/INDEX.md index 4261597f5..22c7490d5 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -13,6 +13,7 @@ linked to an active plan. | File | Purpose | |---|---| +| [Withdrawal messaging](specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md) | **Single source of truth for everything Basilisk says publicly** — the message, the approved copy, what is never said, the unlisting scope, and the inert CLI contract. Every README, listing, and website page copies from it. | | [Checker architecture](specs/CHECKER-ARCHITECTURE-SPEC.md) | Configuration, rules, diagnostics, analysis pipeline, CLI, and quality gates — including [CHKARCH-TEXT-MATCHED-LOGIC](specs/CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-TEXT-MATCHED-LOGIC), the failing-test → delete → report rule that governs any code deciding from source text. | | [Type inference](specs/CHECKER-TYPE-INFERENCE-SPEC.md) | The bidirectional/constraint inference engine — the checker's single type oracle — its narrowing contracts, research grounding, and the condemned legacy mechanisms under demolition. | | [Stub resolution](specs/CHECKER-STUB-RESOLUTION-SPEC.md) | Pinned typing-spec import order, custom typeshed, offline pin verification against the store, a PyPI-package wheel pin, the segregated download component, bundled stdlib ZIP, and generation. | @@ -37,9 +38,7 @@ linked to an active plan. | [Zed extension](specs/ZED-SPEC.md) | Zed WASM client behavior. | | [WASM](specs/WASM-SPEC.md) | The checker compiled for the browser: one-shot in-memory checking with no filesystem, network, or threads. | | [Editor screenshots](specs/VSIX-EDITOR-SCREENSHOTS-SPEC.md) | Automated real VS Code screenshots. | -| [Website E2E](specs/WEBSITE-E2E-SPEC.md) | Navigation and responsive smoke tests. | -| [Website screenshots](specs/WEBSITE-SCREENSHOTS-SPEC.md) | Verified CLI screenshot generation. | -| [Website error pages](specs/WEBSITE-ERROR-PAGES-SPEC.md) | Generated per-diagnostic documentation. | +| [Website E2E](specs/WEBSITE-E2E-SPEC.md) | The withdrawal-contract tests: approved copy on the statement page, every retired URL still resolving, and nothing forbidden anywhere in the build. | | [READMEs](specs/DOCS-README-SPEC.md) | One authored README per language, generated to GitHub, the VSIX (Marketplace + Open VSX), and PyPI. | | [Repository standards](specs/REPO-STANDARDS-SPEC.md) | Root/`.github` gates: duplication budget, coverage thresholds, committed editor directories, Dependabot, CodeQL, and dependency review. | | [Release manual verification](specs/RELEASE-MANUAL-VERIFICATION-SPEC.md) | The manual passes a release person runs before publishing and again after, against the installed artifact: where `/ci-prep` fits, the artifact-provenance gate, the responsiveness smoke test, and the full hands-on test surface. | @@ -73,4 +72,4 @@ Plans contain only unfinished work. Delete a plan when its acceptance gate passe | File | Contents | |---|---| -| [Conformance integrity audit](CONFORMANCE-INTEGRITY-AUDIT.md#CHKARCH-CONFORMANCE-INTEGRITY-AUDIT) | Phase 1: the fitted alias predicates, measured impact, wider checker scan, remediation status, and process changes found by the 2026-08 audit. Linked from the site's [conformance correction](../website/src/docs/conformance.md). | +| [Conformance integrity audit](CONFORMANCE-INTEGRITY-AUDIT.md#CHKARCH-CONFORMANCE-INTEGRITY-AUDIT) | Phase 1: the fitted alias predicates, measured impact, wider checker scan, remediation status, and process changes found by the 2026-08 audit. The public site no longer carries a conformance page; this audit is the internal record ([WITHDRAWAL-SURFACES](specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md#WITHDRAWAL-SURFACES)). | diff --git a/docs/readme/README.src.md b/docs/readme/README.src.md index 6dc1516e4..c988e875b 100644 --- a/docs/readme/README.src.md +++ b/docs/readme/README.src.md @@ -2,218 +2,41 @@ THIS IS THE ONLY AUTHORED README. Every published README — GitHub, the VS Code Marketplace / Open VSX, PyPI — is generated from this file by scripts/gen_readmes.py. Do not edit the generated copies. + The statement itself is NOT authored here: `{{withdrawal:…}}` is substituted + from docs/specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md ([WITHDRAWAL-COPY]). + Change the message there, never here. Exactly ONE paragraph may vary per target: the identity line below ([README-IDENTITY]). A second variant block is a review failure. --> -

- Basilisk -

- -

Basilisk

- -

English · 简体中文

- -

- An open-source Python type checker and language server, built in Rust.
- One extension for the whole workflow — diagnostics, autocomplete, refactoring, formatting, debugging, and profiling — driven by a single bundled binary. -

+# {{withdrawal:title}} > **You are reading the Basilisk source repository** — the checker, language server, editor extensions, and website all live here. -> **You are reading the Basilisk extension listing** for VS Code, Cursor, Windsurf, and every VS Code fork — the same extension is published to the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=Nimblesite.basilisk) and [Open VSX](https://open-vsx.org/extension/Nimblesite/basilisk). +> **You are reading the Basilisk extension listing** for VS Code, Cursor, Windsurf, and every VS Code fork. -> **You are reading the `basilisk-python` wheel listing** — the Basilisk CLI packaged for `pip`/`uv`. The distribution is named `basilisk-python` because `basilisk` was taken on PyPI; the installed command is still `basilisk`. +> **You are reading the `basilisk-python` wheel listing** — the Basilisk CLI packaged for `pip`/`uv`. + +> **You are reading the Basilisk Zed extension listing.** + + +> **You are reading the `basilisk.nvim` plugin listing.** + -

- Website  •  - Install  •  - Quick Start  •  - Rules  •  - Refactoring  •  - GitHub -

- -

- Basilisk in action — type checking, diagnostics, and refactoring in the editor -

- -> ## ⚠️ Do not use Basilisk's type checker in your pipeline -> -> **The type checker still contains code that isn't doing real type checking, and -> it is not yet trustworthy.** Some rules decide from the way code is *spelled* -> rather than what it means, so they can be wrong in both directions — a false -> error on correct code, or silence where there is a genuine bug. Until the audit -> below is finished, don't gate CI on `basilisk check`, don't block a merge with -> it, and don't read a clean run as a clean codebase. -> -> The rest of Basilisk — language server, refactoring, formatting, debugging, -> profiling — does not depend on those rules and is unaffected. - -## Restoring trust: audit, delete, and lean on a checker that works - -We withdrew our former conformance claim and our benchmark figures, and asked to be -[removed from the official `python/typing` results](https://github.com/python/typing/blob/main/conformance/results/results.html). -The cause was checker logic fitted to the contents of conformance test files -instead of implementing the typing specification generally: rules that matched -the *spelling* of code rather than its meaning. Rename an import or reformat a -file and the answer changed. A score produced that way is not evidence. - -**This was a mistake and a failure to verify.** Our process treated the score as -the goal, matching text raises a score faster than real analysis does, and we -published without ever asking whether a rule still held when the same program was -spelled differently. Basilisk's author has published a -[personal account and apology](https://www.christianfindlay.com/blog/basilisk-conformance-apology). - -**So we are auditing every rule and deleting the ones that don't do real type -checking.** Not rewriting them, not patching them, not marking them TODO — -deleting them, with a failing test left behind so the gap is visible instead of -hidden. A rule stays only if it decides from the resolved syntax tree and gives -the same answer when the code is spelled differently. - -**Where a rule can't be made reliable in a straightforward way, we will depend on -a different, established type checker rather than ship our own unreliable version -of it.** An answer from an engine that has earned trust is worth more to you than -a Basilisk-branded one that hasn't. No replacement figure gets published until it -survives off-suite and mutation testing. - -That means Basilisk gets **smaller** before it gets better. Expect fewer rules, -fewer diagnostics, and a lower conformance number. We will report each drop -rather than avoid it. What is left will be code that is honest about what it -does — nothing else. - -### Basilisk is much more than a type checker - -Type checking is one part of it. The rest is a complete Python workflow in a -single Rust binary — language server, refactoring, formatting, integrated -debugging, profiling, and the editor extensions — and none of it rests on the -rules under audit. That is what we are sharpening while the audit runs: make the -parts that are genuinely useful solid, and remove anything that could hand you a -misleading result. The point of getting smaller is to end up with a tool you can -believe. - -[Read the full correction →](https://www.basilisk-python.dev/docs/conformance/)  •  -[Integrity audit →](docs/CONFORMANCE-INTEGRITY-AUDIT.md) - -## What you get - -One extension covers the whole Python workflow. A single bundled Rust binary -drives it — no Node.js, no npm, no `pip install`: - -- **Diagnostics as you type** — incremental analysis powered by [Salsa](https://github.com/salsa-rs/salsa) -- **Autocomplete, hover, go-to-definition, find references, rename** -- **Refactoring code actions** — extract, inline, move symbol, organize imports -- **Integrated debugging** — F5 to debug via bundled [debugpy](https://github.com/microsoft/debugpy); no separate extension -- **Integrated profiling** — CPU heat map, flame graph, and a memory dashboard with leak detection -- **Activity panel** — module tree with per-module type-health coverage, plus feature toggles -- **Inlay hints** and **Ruff** formatting/import-organization, built in -- **Standard-library types from [typeshed](https://github.com/python/typeshed)** — a complete `stdlib/` snapshot is compiled into the binary, so hover and diagnostics work offline with no configuration - -Strictness is configured **per rule**, never by a mode: the unconfigured default -enables the typing-spec rule set, and each rule can be graded down to -`warning`/`info` so a codebase can adopt type safety incrementally. Every -diagnostic carries a `help`, a `note`, and a link to a per-rule explainer, so a -red squiggle tells you *why*. - -## Install +{{withdrawal:full}} -**Editor extension** — install *Basilisk* from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=Nimblesite.basilisk) or [Open VSX](https://open-vsx.org/extension/Nimblesite/basilisk) (Cursor, Windsurf, and other forks read Open VSX). The Basilisk binary is bundled for macOS (Apple Silicon), Linux (x86_64, aarch64), and Windows (x86_64, aarch64) — nothing else to install. Zed and Neovim 0.10+ extensions are available too. +## What to do now -**CLI** — on [PyPI as `basilisk-python`](https://pypi.org/project/basilisk-python/); the installed command is `basilisk`: - -```sh -uv tool install basilisk-python # or: pipx install basilisk-python, pip install basilisk-python -``` - -Also via Homebrew (`brew install Nimblesite/tap/basilisk`), Scoop (`scoop bucket add nimblesite https://github.com/Nimblesite/scoop-bucket && scoop install basilisk`), and [GitHub Releases](https://github.com/Nimblesite/Basilisk/releases). Every channel ships the same single Rust CLI, built from this repository at the same version, with no runtime dependencies. Point `basilisk.executablePath` at your own build to have the extension use it. Full options: [install guide](https://www.basilisk-python.dev/docs/installation/). - -## Try it - -The [`examples/`](examples/) folder has ready-to-go Python files: - -```sh -basilisk check examples/bad.py # 8 typing-spec errors — always on, no config needed -basilisk analyze examples/bad.py # the opt-in strictness warnings on the same file -basilisk analyze examples/good.py # clean, even at full strictness -basilisk check examples/mixed.py # one real type error -basilisk check examples/ # the whole folder at once -``` - -Machine-readable output for CI and tooling: - -```sh -basilisk check path/to/your_code.py --output json --color never -``` - -The two commands read one rule universe split by provenance ([`CHKARCH-COMMANDS`](docs/specs/CHECKER-ARCHITECTURE-SPEC.md)): `check` reports -the `pep`-tagged typing-spec rules and nothing else — that set is always on, and -while a config table may grade one of them down to `warning`/`info`, none may -switch it off. `analyze` reports the non-`pep` house rules, which stay silent -until a table selects them. Only `analyze` emits `BSK-` diagnostics. - -## Standard-library types, always offline - -Basilisk resolves the standard library from [typeshed](https://github.com/python/typeshed), -and checking **never downloads anything**. Out of the box it uses the complete -typeshed `stdlib/` snapshot compiled into the binary, reporting the source as -unpinned — so stdlib types work on a plane, behind a firewall, or in an -air-gapped CI runner, with no configuration. - -Pin an exact commit with `typeshed-commit = "<40-char sha>"` under -`[tool.basilisk]`. A pin does exactly one thing: it verifies, offline, that the -typeshed tree in the local store hashes to that commit. If the commit is not on -this machine the run fails hard with `NO SOURCE` rather than substituting -another source — bring it down first with `basilisk typeshed download` (with no -`--commit` it downloads the latest and writes the pin for you), or use the -editor's **Download latest** button. Alternatively, point `typeshed-path` at -your own typeshed tree. Full options: -[configuration guide](https://www.basilisk-python.dev/docs/configuration/). - -## Development - -```sh -cargo build # build all crates -cargo test # run all tests -cargo clippy # lint (zero warnings policy) -cargo fmt # format -``` - -Rust 1.87+ required. - -## Contributing - -Basilisk is built by a human + AI partnership, with the work split on purpose. See -[CONTRIBUTING.md](CONTRIBUTING.md) — **For Humans** (testing, code-quality review, -conformance/security audits, IDE feature parity, sharpening the AI instructions) and -**For AI** (the technical execution, under the standing rules in [CLAUDE.md](CLAUDE.md)). +{{withdrawal:action}} ## Acknowledgments -Basilisk builds on the open-source community — with thanks to: - -- **[Astral](https://astral.sh/)** — [Ruff](https://github.com/astral-sh/ruff), whose parser, AST, and formatter crates Basilisk embeds (MIT). The foundation we rely on most. -- **[typeshed](https://github.com/python/typeshed)** — standard-library type stubs (Apache-2.0, with MIT-licensed parts). -- **[Salsa](https://github.com/salsa-rs/salsa)** — incremental query engine. -- **[Rayon](https://github.com/rayon-rs/rayon)** — data parallelism. -- **[tower-lsp](https://github.com/ebkalderon/tower-lsp)** — LSP scaffolding. -- **[debugpy](https://github.com/microsoft/debugpy)** — debug adapter (bundled in the VS Code extension). -- The [`python/typing`](https://github.com/python/typing) conformance suite. - -Full component list, selected licenses, and required notices: [NOTICES](NOTICES) -and [RUST-DEPENDENCY-LICENSES](RUST-DEPENDENCY-LICENSES). Each published -artifact carries its own copies: the VSIX ships Rust notices in -`RUST-DEPENDENCY-LICENSES`, npm notices in `VSCODE-DEPENDENCY-LICENSES`, and -debugpy's license and `ThirdPartyNotices.txt` inside `bundled/debugpy`; the -wheel carries the complete locked notices in its `.dist-info/licenses/` -directory. - ---- +Basilisk is built on [Ruff](https://github.com/astral-sh/ruff) by [Astral](https://astral.sh/), whose parser, AST, and formatter crates it embeds (MIT), and on standard-library type stubs from [typeshed](https://github.com/python/typeshed) (Apache-2.0, with MIT-licensed parts). Neither project is responsible for how Basilisk used them. Full component list and required notices: [NOTICES](NOTICES) and [RUST-DEPENDENCY-LICENSES](RUST-DEPENDENCY-LICENSES). ## License -Basilisk source code is MIT licensed. Binary distributions also contain -third-party components under the licenses shipped beside each artifact. +Basilisk source code is MIT licensed. Binary distributions also contain third-party components under the licenses shipped beside each artifact. Built by [NIMBLESITE PTY LTD](https://www.nimblesite.co). diff --git a/docs/readme/README.zh.src.md b/docs/readme/README.zh.src.md deleted file mode 100644 index 1f634fe18..000000000 --- a/docs/readme/README.zh.src.md +++ /dev/null @@ -1,200 +0,0 @@ - -

- Basilisk -

- -

Basilisk

- -

English · 简体中文

- -

- 用 Rust 打造的开源 Python 类型检查器与语言服务器。
- 一个扩展覆盖整套工作流 —— 诊断、自动补全、重构、格式化、调试与性能分析 —— 全部由单一捆绑的二进制文件驱动。 -

- - -> **你正在阅读 Basilisk 的源码仓库** —— 检查器、语言服务器、编辑器扩展与网站都在这里。 - - -> **你正在阅读 Basilisk 的扩展页面**,适用于 VS Code、Cursor、Windsurf 以及所有 VS Code 分支 —— 同一个扩展同时发布到 [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=Nimblesite.basilisk) 与 [Open VSX](https://open-vsx.org/extension/Nimblesite/basilisk)。 - - -

- 网站  •  - 安装  •  - 快速上手  •  - 规则  •  - 重构  •  - GitHub -

- -

- Basilisk 实战 —— 编辑器中的类型检查、诊断与重构 -

- -> ## ⚠️ 请勿在流水线中使用 Basilisk 的类型检查器 -> -> **类型检查器中仍然存在没有做真正类型检查的代码,它目前还不值得信任。** 有些规则 -> 依据的是代码的**写法**而不是含义,因此两个方向上都可能出错 —— 既可能对正确的代码 -> 报出虚假错误,也可能对真实的缺陷保持沉默。在下文所述的审计完成之前,请不要用 -> `basilisk check` 作为 CI 的门禁,不要用它拦截合并,也不要把一次干净的运行结果当作 -> 代码库是干净的。 -> -> Basilisk 的其余部分 —— 语言服务器、重构、格式化、调试、性能分析 —— 并不依赖这些 -> 规则,因此不受影响。 - -## 重建信任:审计、删除,并倚重真正可靠的检查器 - -我们撤回了此前的一致性宣称与基准测试数字,并主动请求 -[从官方 `python/typing` 结果中移除](https://github.com/python/typing/blob/main/conformance/results/results.html)。 -原因是检查器中存在针对一致性测试文件内容而写的逻辑,而不是对类型规范的通用实现: -那些规则匹配的是代码的**写法**,而不是代码的含义。改一个导入别名或重新格式化文件, -结论就会变。这样得出的分数并不能作为证据。 - -**这是一个错误、一次验证上的失职。** 我们的流程把分数当成了目标,而匹配文本比真正做 -分析更快地提高分数;我们在发布之前,始终没有问过这样一个问题 —— 同一个程序换一种 -写法时,这条规则是否依然成立。Basilisk 作者已发表 -[个人说明与致歉](https://www.christianfindlay.com/blog/basilisk-conformance-apology)。 - -**因此,我们正在逐条审计规则,并删除那些没有做真正类型检查的规则。** 不是重写,不是 -打补丁,也不是标一个 TODO —— 是删除,并留下一个失败的测试,让这个缺口可见而不是被 -掩盖。一条规则只有在依据已解析的语法树做判断、并且在代码换一种写法时给出相同结论的 -情况下,才会保留。 - -**如果一条规则无法以直截了当的方式做到可靠,我们会转而依赖另一个成熟的类型检查器, -而不是端出我们自己那份不可靠的实现。** 一个已经赢得信任的引擎给出的答案,对你而言 -比一个挂着 Basilisk 名号却没有赢得信任的答案更有价值。在通过套件之外的用例与变异 -测试之前,我们不会发布任何替代数字。 - -这意味着 Basilisk 会**先变小,再变好**。规则会更少,诊断会更少,一致性数字也会更低。 -每一次下降我们都会如实报告,而不是设法回避。留下来的,将是对自己所做之事诚实的代码 -—— 仅此而已。 - -### Basilisk 远不只是一个类型检查器 - -类型检查只是其中一部分。其余部分是装在单个 Rust 二进制文件里的完整 Python 工作流 -—— 语言服务器、重构、格式化、集成调试、性能分析,以及各个编辑器扩展 —— 它们都不 -建立在正在接受审计的规则之上。这正是我们在审计期间着力打磨的地方:把真正有用的部分 -做扎实,并移除任何可能给出误导性结果的东西。变小的意义,是最终得到一个你可以信赖的 -工具。 - -[阅读完整更正 →](https://www.basilisk-python.dev/zh/docs/conformance/)  •  -[完整性审计 →](docs/CONFORMANCE-INTEGRITY-AUDIT.md) - -## 你能得到什么 - -一个扩展即可覆盖整套 Python 工作流。一切由单一捆绑的 Rust 二进制文件驱动 —— -无需 Node.js、无需 npm、无需 `pip install`: - -- **随输入实时诊断** —— 由 [Salsa](https://github.com/salsa-rs/salsa) 提供增量分析 -- **自动补全、悬停信息、跳转到定义、查找引用、重命名** -- **重构代码操作** —— 提取、内联、移动符号、整理导入 -- **集成调试** —— 按 F5 即可通过捆绑的 [debugpy](https://github.com/microsoft/debugpy) 调试;无需额外扩展 -- **集成性能分析** —— CPU 热力图、火焰图,以及带泄漏检测的内存面板 -- **活动面板** —— 模块树与逐模块的类型健康度覆盖率,并可切换功能开关 -- 内置 **Inlay hints** 与 **Ruff** 格式化/导入整理 -- **来自 [typeshed](https://github.com/python/typeshed) 的标准库类型** —— 完整的 `stdlib/` 快照已编译进二进制文件,因此悬停与诊断在离线且零配置的情况下依然可用 - -严格程度按**规则**配置,而不是靠模式切换:未配置的默认值即启用类型规范规则集, -每条规则都可以降级为 `warning`/`info`,让代码库能够渐进地采用类型安全。每条诊断 -都附带 `help`、`note` 以及指向每条规则详解页的链接,因此一条红色波浪线总能告诉你 -*为什么*。 - -## 安装 - -**编辑器扩展** —— 从 [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=Nimblesite.basilisk) 或 [Open VSX](https://open-vsx.org/extension/Nimblesite/basilisk) 安装 *Basilisk*(Cursor、Windsurf 等分支读取 Open VSX)。Basilisk 二进制文件已为 macOS(Apple Silicon)、Linux(x86_64、aarch64)与 Windows(x86_64、aarch64)捆绑 —— 无需再安装其他东西。Zed 与 Neovim 0.10+ 的扩展同样可用。 - -**命令行工具** —— 在 [PyPI 上名为 `basilisk-python`](https://pypi.org/project/basilisk-python/);安装后的命令是 `basilisk`: - -```sh -uv tool install basilisk-python # 或:pipx install basilisk-python、pip install basilisk-python -``` - -也可通过 Homebrew(`brew install Nimblesite/tap/basilisk`)、Scoop(`scoop bucket add nimblesite https://github.com/Nimblesite/scoop-bucket && scoop install basilisk`)与 [GitHub Releases](https://github.com/Nimblesite/Basilisk/releases) 获取。每个渠道都发布同一个 Rust 命令行工具,由本仓库在同一版本构建,且没有运行时依赖。把 `basilisk.executablePath` 指向你自己的构建,扩展就会使用它。完整选项:[安装指南](https://www.basilisk-python.dev/zh/docs/installation/)。 - -## 试一试 - -[`examples/`](examples/) 目录中有可直接运行的 Python 文件: - -```sh -basilisk check examples/bad.py # 8 处类型规范错误 —— 始终启用,无需配置 -basilisk analyze examples/bad.py # 同一文件上可选的严格性警告 -basilisk analyze examples/good.py # 即使在完全严格下也是干净的 -basilisk check examples/mixed.py # 一处真实的类型错误 -basilisk check examples/ # 一次检查整个目录 -``` - -供 CI 与工具使用的机器可读输出: - -```sh -basilisk check path/to/your_code.py --output json --color never -``` - -这两条命令读取的是按来源划分的同一套规则宇宙([`CHKARCH-COMMANDS`](docs/specs/CHECKER-ARCHITECTURE-SPEC.md)):`check` -只报告带 `pep` 标签的类型规范规则 —— 该集合始终启用,配置表虽可将其中某条 -降级为 `warning`/`info`,但都不能将其关闭。`analyze` 报告非 `pep` 的自有规则, -它们在被配置表选用之前始终保持沉默。只有 `analyze` 会输出 `BSK-` 诊断。 - -## 标准库类型:始终离线 - -Basilisk 从 [typeshed](https://github.com/python/typeshed) 解析标准库类型, -而且检查**从不下载任何东西**。开箱即用时它使用编译进二进制文件的完整 typeshed -`stdlib/` 快照,并将来源报告为未固定(unpinned)—— 因此在飞机上、防火墙后或 -隔离网络的 CI 中,标准库类型都无需配置即可使用。 - -在 `[tool.basilisk]` 中使用 `typeshed-commit = "<40 位 sha>"` 固定到某个确切提交。 -固定只做一件事:离线校验本地存储库中的 typeshed 树是否哈希为该提交。若该提交 -不在本机上,运行会以 `NO SOURCE` 硬失败,而不会替换为其他来源 —— 请先用 -`basilisk typeshed download` 取回(不带 `--commit` 时会下载最新提交并替你写入 -固定项),或使用编辑器中的 **Download latest** 按钮。或者,把 `typeshed-path` -指向你自己的 typeshed 目录树。完整选项参见[配置指南](https://www.basilisk-python.dev/zh/docs/configuration/)。 - -## 开发 - -```sh -cargo build # build all crates -cargo test # run all tests -cargo clippy # lint (zero warnings policy) -cargo fmt # format -``` - -需要 Rust 1.87+。 - -## 贡献 - -Basilisk 由人类与 AI 的协作打造,并有意地划分了各自的工作。请参阅 -[CONTRIBUTING.md](CONTRIBUTING.md) —— **For Humans**(测试、代码质量审查、 -一致性/安全审计、IDE 功能对等、打磨 AI 指令)以及 -**For AI**(在 [CLAUDE.md](CLAUDE.md) 既定规则下的技术执行)。 - -## 致谢 - -Basilisk 建立在开源社区之上 —— 特别感谢: - -- **[Astral](https://astral.sh/)** —— [Ruff](https://github.com/astral-sh/ruff),Basilisk 嵌入了其解析器、AST 与格式化器 crate(MIT)。我们最倚重的基础。 -- **[typeshed](https://github.com/python/typeshed)** —— 标准库类型存根(Apache-2.0,部分内容采用 MIT 许可证)。 -- **[Salsa](https://github.com/salsa-rs/salsa)** —— 增量查询引擎。 -- **[Rayon](https://github.com/rayon-rs/rayon)** —— 数据并行。 -- **[tower-lsp](https://github.com/ebkalderon/tower-lsp)** —— LSP 脚手架。 -- **[debugpy](https://github.com/microsoft/debugpy)** —— 调试适配器(捆绑于 VS Code 扩展)。 -- [`python/typing`](https://github.com/python/typing) 一致性测试套件。 - -完整的组件、所选许可证与必要声明见 [NOTICES](NOTICES) 和 -[RUST-DEPENDENCY-LICENSES](RUST-DEPENDENCY-LICENSES)。每个发布的产物也各自 -携带副本:VSIX 在 `RUST-DEPENDENCY-LICENSES` 中提供 Rust 声明,在 -`VSCODE-DEPENDENCY-LICENSES` 中提供 npm 声明,并在 `bundled/debugpy` 内保留 -debugpy 自身的许可证与 `ThirdPartyNotices.txt`;wheel 则在 `.dist-info/licenses/` -目录中携带完整的锁定声明。 - ---- - -## 许可证 - -Basilisk 源代码采用 MIT 许可证。二进制发行物还包含第三方组件;其许可证 -随每个发行物一并提供。 - -由 [NIMBLESITE PTY LTD](https://www.nimblesite.co) 构建。 diff --git a/docs/specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md b/docs/specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md new file mode 100644 index 000000000..ff18da360 --- /dev/null +++ b/docs/specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md @@ -0,0 +1,112 @@ +# Basilisk withdrawal — canonical messaging {#WITHDRAWAL} + +Single source of truth for everything Basilisk says publicly. Every README, listing, website page, and the CLI's own output copies from here. No surface writes its own version. Where old copy conflicts, this wins. + +## The message {#WITHDRAWAL-CLAIMS} + +1. **Basilisk's type checker was producing incorrect results.** +2. **We asked for it to be removed from the `python/typing` results, and it was** — [python/typing#2330](https://github.com/python/typing/pull/2330). +3. **The code responsible is not isolated. We cannot say how many rules are affected.** +4. **A code-quality tool that does not produce correct results is worse than useless.** +5. **Remove Basilisk from your pipeline.** The type checker is being made inert, and every distribution channel is being unlisted. +6. **We are unlisting first, then rebuilding from the ground up as a new product** — not fixing, auditing, or salvaging this code. +7. **Nothing is relisted until it has been rebuilt from components we can vouch for.** It will ship only what can be trusted — most likely not type checking. If type checking returns, it will go through **external auditing** before being released. + +Nothing else is asserted. One vocabulary, in this order: *incorrect results → removed from `python/typing` → unlisted from every channel → the CLI is inert → rebuilt from the ground up as a new product*. "Unlisted" throughout — never delisted, withdrawn, deprecated, or discontinued as synonyms. + +## Copy {#WITHDRAWAL-COPY} + +Verbatim. Doesn't fit? Use the shorter one. + +**One line** {#WITHDRAWAL-COPY-LINE} — repo description, package summaries, store descriptions, OG tags: + +> Basilisk's type checker produced incorrect results. Basilisk is unlisted and is being rebuilt from the ground up as a new product. + +**Short** {#WITHDRAWAL-COPY-SHORT} — READMEs, PyPI, extension listings: + +> **Basilisk's type checker was producing incorrect results. Basilisk is unlisted everywhere.** +> +> We asked for Basilisk to be removed from the `python/typing` conformance results, and it has been removed ([python/typing#2330](https://github.com/python/typing/pull/2330)). The code responsible is not isolated to a known set of rules, so we cannot say how many rules are affected. A code-quality tool that does not produce correct results is worse than useless. +> +> **Remove Basilisk from your pipeline.** Every distribution channel is being unlisted, and the type checker is inert — it checks nothing and exits non-zero, so a build that still calls it fails loudly instead of reporting a clean run. +> +> What comes next is a new product, rebuilt from the ground up, shipping only what can be trusted. That most likely will not include type checking. Nothing is relisted until it has been rebuilt from components we can vouch for. +> +> Basilisk's author has published a full public account: [an apology](https://www.christianfindlay.com/blog/basilisk-conformance-apology). + +**What to do now** {#WITHDRAWAL-COPY-ACTION} — every README and store listing carries this under the statement. It is the only part of the message that asks the reader to do something, so it never gets cut for length: + +> **Remove Basilisk from your pipeline, your pre-commit hooks, and your editor.** Uninstall the CLI and the extension. +> +> The type checker is inert: it checks nothing, and every invocation fails. It prints this statement and exits non-zero, so a build that still calls it fails loudly rather than reporting a clean run. Do not treat that failure as a finding about your code. +> +> **Treat every result Basilisk gave you as unverified.** A clean run was never evidence that your code was clean, and an error it reported may never have been real. +> +> Every distribution channel is being unlisted. Nothing will be relisted until it has been rebuilt from components we can vouch for. + +**Full** {#WITHDRAWAL-COPY-FULL} — website home and README body. There is no longer form: + +> # Basilisk is unlisted +> +> **Basilisk's type checker was producing incorrect results.** Rules decided from the way code was *spelled* rather than what it meant, so they could be wrong in both directions — a false error on correct code, or silence on a real bug. +> +> **We asked for Basilisk to be removed from the `python/typing` conformance results, and it has been removed** ([python/typing#2330](https://github.com/python/typing/pull/2330)). That score did not demonstrate correctness. +> +> **We cannot tell you how much of the checker this affects.** The code responsible is not isolated to a known set of rules. We will not estimate. That uncertainty is the reason for everything below. +> +> **A code-quality tool that does not produce correct results is worse than useless.** Basilisk is being unlisted everywhere it was published — the VS Code Marketplace, Open VSX, the Zed registry, PyPI, the Homebrew tap, and the Scoop bucket — and the type checker is inert. Remove it from your pipeline; it checks nothing, and every invocation fails rather than reporting a clean run. +> +> **We are not fixing Basilisk's type checker code. We are rebuilding from the ground up as a new product.** It will ship only what can be trusted. That most likely will not include type checking. Nothing is relisted until it has been rebuilt from components we can vouch for. If type checking ever returns, it will be externally audited before release. +> +> Basilisk's author has published a full public account: [an apology](https://www.christianfindlay.com/blog/basilisk-conformance-apology). + +## Never {#WITHDRAWAL-PROHIBITED} + +- **Never quote the apology** — link it, neutrally, nowhere else. It speaks for itself in its author's words. +- **No conformance or benchmark figure**, in any tense, caveated or archived. +- **No feature marketing, rule counts, or per-rule docs** — including for parts that never touched the checker. +- **No scoping reassurance** — never "only a few rules", "the language server is fine, keep using it". Claim 3 forbids it. +- **No blame outside the project.** No timeline. No install instructions. + +Tone: plain declaratives, active voice, worst part first. One statement of fault, then facts. No hedging, no repeated apology. Under a minute to read. + +## Unlisting {#WITHDRAWAL-UNLIST} + +Marketplace, Open VSX, Zed registry, PyPI, Homebrew tap, Scoop bucket — all unlisted. The repo stays public with the [full copy](#WITHDRAWAL-COPY-FULL) as its README — taking it down would erase what happened. Installed copies aren't force-removed; they go inert. + +**One last release, then no more.** Unlisting hides the listing; it does not touch the copy already installed on a developer's machine. So exactly one final version ships to every channel first — carrying this statement and the [inert CLI](#WITHDRAWAL-INERT) — and the channel is unlisted immediately after. That is the only way an existing install learns what happened. Existing GitHub Releases stay (deleting them destroys the record); after the final one, no new releases. Order per channel, no exceptions: **publish the final version → verify it is live → unlist.** The runbook and its scripts: [`delist/`](../../delist/). + +Sources to edit, never the generated artefact: READMEs come from [`docs/readme/*.src.md`](../readme/); the website collapses to one notice page, with every retired URL — `/docs/*`, `/blog/*`, `/errors/BSK-XXXX/` — **redirecting** to it so links from installed binaries and search results land on the explanation rather than a 404 or a second copy of the statement. Internal specs, plans, and the [integrity audit](../CONFORMANCE-INTEGRITY-AUDIT.md) are not marketing surfaces — they are the record. Keep them, marked superseded. + +## Surfaces {#WITHDRAWAL-SURFACES} + +Every surface below carries a block from [Copy](#WITHDRAWAL-COPY) and nothing else. None writes its own version; each is generated or copied from this file. + +| Surface | Block | Generated by | +|---|---|---| +| Website home | [full](#WITHDRAWAL-COPY-FULL) | `scripts/gen_withdrawal_copy.py` → `website/src/_data/withdrawal.json` | +| Every retired website URL | redirect to `/` | `website/src/notice.njk` | +| GitHub / VSIX / PyPI / Zed / Neovim READMEs | [full](#WITHDRAWAL-COPY-FULL) + [action](#WITHDRAWAL-COPY-ACTION) | `scripts/gen_readmes.py` from `docs/readme/README.src.md` | +| Package + store description fields | [one line](#WITHDRAWAL-COPY-LINE) | copied by hand, asserted by `scripts/test_published_readmes.py` | +| CLI, every invocation | [notice](#WITHDRAWAL-INERT-TEXT) | `crates/basilisk-cli/src/main.rs` | +| VS Code extension | [notice](#WITHDRAWAL-INERT-TEXT) | `vscode-extension/src/extension.ts` | + +The extension ships **no checker binary and no type-checking UI** — no diagnostics, commands, views, settings, debugger, or profiler. It activates, states this, and links the website. + +## Inert Type Checker CLI {#WITHDRAWAL-INERT} + +**Every invocation fails.** Bare `basilisk`, every subcommand, every flag, `--help`, a bad argument: print the notice to **stderr** and exit `4` (*unlisted*, added to [CHKARCH-CLI-EXITCODES](CHECKER-ARCHITECTURE-SPEC.md#CHKARCH-CLI-EXITCODES)). No parsing, no analysis, no file touched, no server. Stdout emits nothing ever, so `--output json > report.json` yields an empty file, not prose a consumer might parse. Never exit `0` — a pipeline that still calls Basilisk must break, loudly, rather than read a clean run into it — and never `1`, because "errors found" would be one more incorrect result. `--version` is the sole exception, exit `0`: package managers and installed extensions verify against it and would otherwise hang instead of showing the notice. + +Exact text {#WITHDRAWAL-INERT-TEXT}, no colour or emoji: + +```text +Basilisk is unlisted. Its type checker is inert and checks nothing. + +Basilisk's type checker was producing incorrect results. The code responsible is not isolated to a known set of rules, so we cannot say how many rules are affected. We asked for Basilisk to be removed from the python/typing conformance results, and it has been removed: https://github.com/python/typing/pull/2330 + +A code-quality tool that does not produce correct results is worse than useless. Remove Basilisk from your pipeline, your pre-commit hooks, and your editor. This command failed on purpose. It is not a finding about your code. + +We are not fixing this code. We are rebuilding from the ground up as a new product, shipping only what can be trusted. If type checking ever returns, it will be externally audited before release. + +A full public account: https://www.christianfindlay.com/blog/basilisk-conformance-apology +``` diff --git a/docs/specs/WEBSITE-E2E-SPEC.md b/docs/specs/WEBSITE-E2E-SPEC.md index 043178b93..59de03e4a 100644 --- a/docs/specs/WEBSITE-E2E-SPEC.md +++ b/docs/specs/WEBSITE-E2E-SPEC.md @@ -1,62 +1,20 @@ -# Website: Navigation & End-to-End Smoke Tests {#WEBSITE-E2E} +# Website: withdrawal-contract end-to-end tests {#WEBSITE-E2E} ## Purpose {#WEBSITE-E2E-PURPOSE} -Browser smoke tests for the Eleventy site (`website/`), run against the -**production build** of `_site/` on a desktop and a phone viewport, enforcing in -CI that a visitor can navigate the site. +Browser tests for the Eleventy site (`website/`), run against the **production build** of `_site/` on a desktop and a phone viewport. The site publishes one thing — the withdrawal statement ([WITHDRAWAL-COPY-FULL](DOCS-WITHDRAWAL-MESSAGING-SPEC.md#WITHDRAWAL-COPY-FULL)) — so these tests enforce that contract rather than navigation: the published words are the approved words, no retired URL 404s, and nothing forbidden survives anywhere in the build. -## Smoke Coverage {#WEBSITE-E2E-SMOKE} +## Coverage {#WEBSITE-E2E-WITHDRAWAL} -`website/tests/e2e/navigation.spec.ts` and `website/tests/e2e/homepage.spec.ts`, driven by -`website/playwright.config.ts` (two projects: `desktop` = Desktop Chrome, -`mobile` = iPhone SE 3rd generation emulated in Chromium, 375 × 667), served by -`website/tests/static-server.js`. Run with -`npm run test:e2e` (`test:e2e:ui` locally). Asserts per viewport: +`website/tests/e2e/withdrawal.spec.ts`, driven by `website/playwright.config.ts` (two projects: `desktop` = Desktop Chrome, `mobile` = iPhone SE 3rd generation emulated in Chromium, 375 × 667), served by `website/tests/static-server.js`. Run with `npm run test:e2e` (`test:e2e:ui` locally). -- **Top navigation resolves** — the home page links to Docs, Rules, Blog, - Discord and GitHub (matched by `href`, so the check holds even where the nav - is collapsed behind the hamburger on a phone). -- **Docs landing page loads** — `/docs/` renders with the docs sidebar present. -- **Desktop sidebar** — the docs sidebar is permanently visible and navigates - between sections without any toggle. -- **Mobile docs submenu** — see [WEBSITE-MOBILE-DOCS-NAV]. -- **Mobile top nav** — the hamburger reveals the collapsed top nav. -- **Homepage positioning** — the title, H1 and opening answer identify Basilisk - neutrally as a Python type checker and language server, without an unverified - speed or conformance claim. -- **Integrity disclosure is unavoidable** — the hero states that the former - conformance and benchmark figures are withdrawn, the current conformance - percentage is temporarily unknown, Basilisk was removed from the official - results at its request, and clean reimplementation plus robustness/mutation - verification must finish before new figures are published. Both notices link - to their detailed correction pages. -- **Social image matches its declared size** — the `og:image` URL resolves and - the PNG's own IHDR dimensions equal the advertised `og:image:width`/`height`, - so a re-exported image cannot silently desync from its metadata. -- **The Chinese homepage is a translation, not a second pitch** — `/zh/` and `/` - are asserted to produce an identical structural skeleton (section, stat-card, - bullet and button class lists, in order). The zh page repeats both withdrawal - notices and the temporarily unknown status, so one locale cannot retain a - claim the other has retracted. -- **Homepage mobile usability** — no horizontal overflow and visible calls to - action retain a minimum 48 px touch target on the iPhone SE viewport. +- **The statement is the approved copy** — the home page renders every paragraph of `withdrawal.full`, in order, from `website/src/_data/withdrawal.json`. That file is generated from the messaging spec by `scripts/gen_withdrawal_copy.py`, so a test failure means the page drifted from the spec, and a `--check` failure means the data did. +- **The four load-bearing facts appear** — incorrect results, removal from the `python/typing` results, the damage not being scoped to a known set of rules, and a wrong tool being worse than useless. Asserted on visible text, so deleting a paragraph fails even if the copy file still contains it. +- **Every retired URL resolves** — each entry in `website/src/_data/retiredUrls.json` has a built page. A representative URL per family (`/docs/`, `/docs/rules/`, `/errors/BSK-XXXX/`, `/blog/`, `/playground/`, `/zh/docs/…`) is fetched over HTTP and asserted to return 200, carry the short copy, and link home. `/errors/` matters most: shipped binaries print those links, and a 404 there strands a user with a diagnostic and no explanation. +- **Only the statement is indexable** — every built page except `/` carries `noindex`, and the sitemap lists `/` alone. 297 byte-identical notices must not be offered to search engines as 297 pages. +- **Nothing forbidden survives** — the whole build is scanned for anything [WITHDRAWAL-PROHIBITED](DOCS-WITHDRAWAL-MESSAGING-SPEC.md#WITHDRAWAL-PROHIBITED) bars: a percentage figure, install instructions for any channel, a marketplace or PyPI link, a competitor name, a benchmark claim, a `BSK-` rule code. This is the test that catches a page nobody remembered to delete. +- **The apology is linked, never quoted** — every page links it; no page reproduces its wording. ### CI constraint {#WEBSITE-E2E-NO-ARTIFACTS} -Per `[GITHUB-NO-ARTIFACTS]`, CI emits only the stdout `list` reporter — no -Playwright HTML report, trace, video or screenshot. Those (HTML report + on-retry -trace) are local-only and git-ignored (`website/.gitignore`). The website CI job -(`.github/workflows/ci.yml`) installs only Chromium, since both presets run on it. - -## Mobile Docs Submenu Reachability {#WEBSITE-MOBILE-DOCS-NAV} - -On phones (`max-width: 768px`) the docs section sidebar collapses. It **must** -remain reachable: the hamburger toggle (`mobile-menu.js`, which adds `.open` to -`.sidebar`) reveals it via `.sidebar.open { display: block; }` in -`website/src/assets/css/styles.css`, mirroring the `.nav-links.open` rule for the -top nav. Without that reveal rule the toggle has no effect and the per-section -submenu is unreachable on a phone (regression issue #186). Guard test -`"docs section submenu is reachable via the hamburger"` in -`website/tests/e2e/navigation.spec.ts` asserts the submenu is hidden by default, -becomes visible after the hamburger is tapped, and navigates to the section. +Per `[GITHUB-NO-ARTIFACTS]`, CI emits only the stdout `list` reporter — no Playwright HTML report, trace, video or screenshot. Those (HTML report + on-retry trace) are local-only and git-ignored (`website/.gitignore`). The website CI job (`.github/workflows/ci.yml`) installs only Chromium, since both presets run on it. diff --git a/docs/specs/WEBSITE-ERROR-PAGES-SPEC.md b/docs/specs/WEBSITE-ERROR-PAGES-SPEC.md deleted file mode 100644 index 262031695..000000000 --- a/docs/specs/WEBSITE-ERROR-PAGES-SPEC.md +++ /dev/null @@ -1,83 +0,0 @@ -# Website: Per-Diagnostic Error Pages {#WEBSITE-ERROR-PAGES} - -## Purpose {#WEBSITE-ERROR-PAGES-PURPOSE} - -Every diagnostic Basilisk reports ends with a deep link, e.g. -`= see: https://www.basilisk-python.dev/errors/BSK-0001`. That URL is baked into -each rule (`docs_url` on the `ErrorCode` in -`crates/basilisk-checker/src/rules/*.rs`). This spec generates a landing page for -**every** diagnostic code at `/errors/BSK-XXXX/`, built from the checker source so -pages never drift from the diagnostics the binary emits. - -## 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", "references": [{label, url}] } -``` - -`docsUrl` is read from the rule's own `docs_url` literal, so the page URL and CLI -link are identical. The same data drives the `/docs/rules/` reference table and -the headline counts (`_data/ruleStats.js`, `_data/ruleGroups.js`) — prose, table, -and pages share one source. - -### Canonical references {#WEBSITE-ERROR-PAGES-REFERENCES} - -Every record carries `references` — the canonical upstream documentation for -the diagnostic, in order: the rule's chapter of the maintained -[typing spec](https://typing.python.org/en/latest/spec/index.html) (chapter -titles and filenames mirror that index verbatim), then the PEP(s) that chapter -incorporates merged with every `PEP NNN` the rule's own doc comment cites -(linked to `https://peps.python.org/pep-NNNN/`), then any language-reference -link for rules governed by Python semantics rather than the typing spec (the -`names_*` unbound/undefined checks link the execution model's Naming and -binding section). The prefix→chapter and prefix→PEP tables live in -`scripts/gen_rules_reference.py` (`SPEC_CHAPTER_BY_PREFIX`, `PEPS_BY_PREFIX`); -PEP links are labelled `PEP NNN` only, so no title can drift from -peps.python.org. `inline_html` additionally links every `PEP NNN` mention in -rendered summary/body prose. Opt-in house rules (`BSK-` codes) map through -`REFERENCE_PREFIX_BY_BSK_CODE` to the chapter/PEPs documenting the mechanism -they police (annotation rules → Type annotations; BSK-0152 → PEP 561; -BSK-0011/0012 → PEP 621; BSK-0013 → uv's lockfile docs), plus any PEP their -own docs cite (BSK-0025 → PEP 698). The suppression rules (BSK-0060..0063) -police Basilisk's own directives and deliberately list nothing. `error.njk` -renders the list as a "Canonical documentation" section on every -`/errors//` page. - -### Drift guard {#WEBSITE-ERROR-PAGES-DRIFT} - -The CI website job regenerates the data and `diff`s it against the committed -`rules.json`, failing on any difference; the change-classifier treats edits under -`crates/basilisk-checker/src/rules/` as website changes, so adding/renaming a rule -re-runs the guard. A new rule 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, the canonical-documentation links -([WEBSITE-ERROR-PAGES-REFERENCES]), 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 its demonstrating screenshot by reading the -manifest's `expect` field ([WEBSITE-SCREENSHOTS-MANIFEST]), so the mapping is -correct even where image stem and code differ (e.g. `e0011.png` demonstrates -`BSK-0014`). Adding a verified shot to `screenshots/shots.mjs` and regenerating -gives a code's page a real-output example. - -## Verification {#WEBSITE-ERROR-PAGES-VERIFY} - -`website/tests/e2e/errors.spec.ts` (same Playwright config as [WEBSITE-E2E-SMOKE]) -asserts: every `rules.json` code 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, this closes the loop checker source → data → -page → render. diff --git a/docs/specs/WEBSITE-SCREENSHOTS-SPEC.md b/docs/specs/WEBSITE-SCREENSHOTS-SPEC.md deleted file mode 100644 index 9f4148faf..000000000 --- a/docs/specs/WEBSITE-SCREENSHOTS-SPEC.md +++ /dev/null @@ -1,81 +0,0 @@ -# Website: Automated CLI Screenshots {#WEBSITE-SCREENSHOTS} - -## Purpose {#WEBSITE-SCREENSHOTS-PURPOSE} - -The 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`). - -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, with a guard that every snippet still triggers the diagnostic it -documents. The bytes shown are exactly what `basilisk check --color always` -prints; per project rule, these are never hand-typed code fences or synthetic -renders. - -## 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 dir - (`basilisk-demo-*`) so diagnostic paths read `e0001.py:1:13` — relative and - PII-free. -2. Runs `basilisk check --color always ` there. A non-zero exit is expected; - stdout is read off the thrown error. -3. Asserts the documented diagnostic code is present - ([WEBSITE-SCREENSHOTS-MANIFEST]). If absent the image is **not** written and - generation fails loudly, so a checker change can never silently ship a - misleading screenshot. -4. Renders the output in a Terminal window ([WEBSITE-SCREENSHOTS-CHROME]) via - Playwright (Chromium, `deviceScaleFactor: 2`) 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 — PNGs are committed and regenerated locally when -CLI output changes; CI only verifies they render ([WEBSITE-SCREENSHOTS-VERIFY]), -per `[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** docs snippet with the code it must -produce (`expect`). Snippets isolate one rule — e.g. `e0001` keeps the `-> str` -return annotation so only `BSK-0001` fires, not `BSK-0002`; `e0011` documents -the explicit-`Any` check and asserts `BSK-0014`; the home shots assert the -summary line (`Found 6 diagnostics`, `No issues found`). The assertion lives in -code and runs on every regeneration. - -## Terminal chrome {#WEBSITE-SCREENSHOTS-CHROME} - -### ANSI conversion {#WEBSITE-SCREENSHOTS-ANSI} - -`website/screenshots/terminal.mjs` builds the window HTML; `ansi.mjs` converts the -binary's ANSI escapes to themed HTML. The binary emits a fixed SGR set — reset, -bold, and bold foreground red (errors), yellow (warnings), blue (gutters), cyan -(labels) — modelled exactly. The window is a 120-column macOS Terminal -("basilisk-demo — -zsh") on the default dark profile (`rgb(30, 30, 30)`). - -## 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), -importing the manifest so it cannot drift from the generated set. Asserts: - -- The rule docs (`/docs/rules/missing-annotations/`, `/docs/rules/type-safety/`) - embed **every** `e00*` screenshot, each decoding to non-zero pixels. -- The homepage demo embeds both `cli-demo.png` and `cli-clean.png`; `cli-demo` - (visible) renders immediately, `cli-clean` after its tab panel is revealed (it - is lazy-loaded, initially hidden). -- No `/assets/images/*.png` request returns a non-200 status. - -A missing, zero-byte, or unreferenced screenshot fails CI. As with -[WEBSITE-E2E-NO-ARTIFACTS], CI emits only the stdout `list` reporter — no report, -trace, video or capture. diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 74c767a6c..000000000 --- a/examples/README.md +++ /dev/null @@ -1,124 +0,0 @@ -

English · 简体中文

- -# Basilisk Examples - -Realistic Python scripts that demonstrate what Basilisk catches — and what -clean, fully-typed code looks like. - -## Running the examples - -```bash -# Typing-spec errors in a single file -basilisk check examples/bad.py - -# The opt-in house rules on that same file -basilisk analyze examples/bad.py - -# Every example at once -basilisk check examples/ - -# JSON output (for editors / CI) -basilisk check examples/bad.py --output json -``` - -`check` and `analyze` read one rule universe partitioned by provenance -([CHKARCH-COMMANDS]): `check` reports the `pep`-tagged typing-spec rules and -nothing else, while `analyze` reports the non-`pep` house rules a table -selected. Both honour the severities the tables below describe. The `BSK-` codes -tabulated later therefore appear under `analyze` — never under `check`. - -## Spec rules vs house rules - -Every **PEP** diagnostic below is a genuine violation of the -[Python typing spec](https://typing.python.org/en/latest/spec/index.html). -Those rules are on out of the box — in your own project, with no -configuration, you get exactly them, at `error`. A config file can grade one -of them down to `warning` or `info`, but no table may switch it off. - -The rest are Basilisk's opt-in house rules (annotations required everywhere, -`@override` required, and so on). They stay silent until a `[tool.basilisk]` -table selects them. Basilisk resolves configuration per checked file by -walking up from the file's own folder, and the nearest table that decides a -rule wins outright. Two tables decide things here: the root `pyproject.toml` -selects the house rules for the repository, and `examples/pyproject.toml` — the -nearer one for everything under `examples/` — grades `BSK-0001`–`BSK-0005` and -`BSK-0025` down to `warning`. That is the incremental-adoption setup the docs -teach: warnings mean "this type-checks, but strictness isn't at full yet". -Rules the examples' table says nothing about, such as `BSK-0014` and -`BSK-0050`, are still decided by the root table. - -That is the whole scoping mechanism. To run a rule at a different severity in -one part of a tree, put a `pyproject.toml` carrying its own `[tool.basilisk]` -table in that folder; there are no glob path patterns, no per-module tables, -and no presets or modes. - -## Files - -### Violation showcases (many diagnostics) - -| File | Domain | PEP rules (always on, errors here) | Basilisk house rules (opt-in) | -|---|---|---|---| -| [bad.py](bad.py) | Minimal tour | `calls_argument_type`, `returns_compatibility`, `assignment_compatibility`, `calls_argument_count`, `classes_override`, `names_unbound`, `match_exhaustiveness` | BSK-0001, BSK-0002, BSK-0004 | -| [mixed.py](mixed.py) | Mixed typed / untyped | `calls_argument_type` | BSK-0001, BSK-0002 | -| [api_server.py](api_server.py) | REST API handler | `assignment_compatibility`, `overloads_consistency`, `names_unbound`, `dict_key_hashable`, `classes_override_2` | BSK-0001–BSK-0003, BSK-0025, BSK-0014, BSK-0050 | -| [data_pipeline.py](data_pipeline.py) | ETL pipeline | `assignment_compatibility`, `overloads_consistency`, `names_unbound`, `dict_key_hashable`, `classes_override_2` | BSK-0001–BSK-0003, BSK-0025, BSK-0014 | -| [ml_trainer.py](ml_trainer.py) | ML training loop | `assignment_compatibility`, `overloads_consistency`, `match_exhaustiveness`, `dict_key_hashable`, `classes_override_2` | BSK-0001–BSK-0003, BSK-0025, BSK-0014, BSK-0050 | -| [finance.py](finance.py) | Financial calculations | `assignment_compatibility`, `classes_override_2`, `overloads_consistency`, `names_unbound`, `match_exhaustiveness`, `dict_key_hashable` | BSK-0001–BSK-0003, BSK-0025, BSK-0014, BSK-0050 | -| [cli_tool.py](cli_tool.py) | CLI application | `assignment_compatibility`, `classes_override_2`, `overloads_consistency`, `names_unbound`, `match_exhaustiveness`, `dict_key_hashable` | BSK-0001–BSK-0003, BSK-0025, BSK-0014, BSK-0050 | -| [weird_violations.py](weird_violations.py) | Subtle edge cases | `overloads_consistency`, `names_unbound`, `classes_override_2`, `assignment_compatibility`, `match_exhaustiveness`, `dict_key_hashable` | BSK-0001–BSK-0003, BSK-0014, BSK-0050 | - -### Clean counterparts (zero diagnostics) - -| File | Counterpart | -|---|---| -| [good.py](good.py) | `bad.py` fixed — passes at full strictness | -| [api_server_clean.py](api_server_clean.py) | `api_server.py` fixed | - -### Debugger & profiler demos (launch with F5) - -These are clean, fully-typed scripts meant to be *run* under the Basilisk -debugger rather than statically checked. Open one and press F5. - -| File | Demonstrates | How to use | -|---|---|---| -| [debug_demo.py](debug_demo.py) | Breakpoints, Watch panel, Locals, Debug Console | Set a breakpoint and step through | -| [profile_demo.py](profile_demo.py) | CPU profiling — a few seconds of CPU-bound work with a clear hot spot, so the flame chart and hot-line heat map fill in | One click: **Run & Profile CPU (Current File)** | -| [cpu_demo.py](cpu_demo.py) | CPU sampling — hot/warm/cold flame chart, hot-line hints | Attach the CPU profiler to the live session | -| [memory_demo.py](memory_demo.py) | Memory — sustained leak, transient spike, reference cycle; the run captures a final snapshot at exit, so it ends in a viewable heat map / `.heapprofile` | One click: **Run & Track Memory (Current File)** | -| [heap_demo.py](heap_demo.py) | Memory — a chunky ~70 MB warm cache across ~40 distinct allocation sites, so the `.heapprofile` flame chart and Self-Size table fill with varied, real slices | One click: **Run & Track Memory (Current File)** | - -## Rule reference - -Every diagnostic ends with a `see:` link to its documentation page. The full -catalog lives at [basilisk-python.dev/docs/rules](https://www.basilisk-python.dev/docs/rules/). - -### PEP typing-spec rules shown here (always on, errors here) - -| Code | Meaning | -|---|---| -| `calls_argument_type` | Argument incompatible with the parameter's declared type | -| `calls_argument_count` | Wrong number of arguments in a call | -| `returns_compatibility` / `returns_compatibility_2` | Returned value not assignable to the declared return type | -| `assignment_compatibility` | Assigned value not assignable to the annotation | -| `classes_override` | `@override` method incompatible with the base-class method | -| `classes_override_2` | Attribute override incompatible with the base class | -| `names_unbound` | Variable may be unbound on some execution paths | -| `match_exhaustiveness` | Non-exhaustive `match` — no wildcard `case _:` branch | -| `dict_key_hashable` | Unhashable type used as a dict key | -| `overloads_consistency` | Inconsistent or overlapping `@overload` group | - -### Basilisk house rules shown here (opt-in) - -The severity column is what the tables governing `examples/` select, not a -property of the code — a rule code carries no severity class. In an -unconfigured project none of these rules run at all. - -| Code | Meaning | Severity here | -|---|---|---| -| BSK-0001 | Missing parameter type annotation | warning | -| BSK-0002 | Missing return type annotation | warning | -| BSK-0003 | Cannot infer type of empty collection or `None` | warning | -| BSK-0004 | Missing `*args` / `**kwargs` type annotation | warning | -| BSK-0025 | Override missing `@override` decorator | warning | -| BSK-0014 | Explicit `Any` without justification | warning | -| BSK-0050 | Redundant type annotation | warning | diff --git a/examples/README.zh.md b/examples/README.zh.md deleted file mode 100644 index 29e9808b3..000000000 --- a/examples/README.zh.md +++ /dev/null @@ -1,118 +0,0 @@ -

English · 简体中文

- -> 📝 本文档由机器翻译生成,欢迎母语者校对改进。 - -# Basilisk 示例 - -真实的 Python 脚本,展示 Basilisk 能捕获哪些问题,以及无错误(干净)、完整类型注解的代码是什么样子。 - -## 运行示例 - -```bash -# 单个文件中的类型规范错误 -basilisk check examples/bad.py - -# 同一文件上可选启用的自定规则 -basilisk analyze examples/bad.py - -# 一次性检查所有示例 -basilisk check examples/ - -# JSON 输出(用于编辑器 / CI) -basilisk check examples/bad.py --output json -``` - -`check` 与 `analyze` 读取的是同一套规则,只是按来源做了划分([CHKARCH-COMMANDS]): -`check` 只报告带 `pep` 标签的类型规范规则,而 `analyze` 报告由配置表选用的、不带 -`pep` 标签的自定规则;两者都遵循下文表格所描述的严重级别。因此下文列出的 `BSK-` -代码只会出现在 `analyze` 中——`check` 永远不会输出它们。 - -## 规范规则 vs 自定规则 - -下面的每个 **PEP** 诊断都是对 -[Python 类型规范](https://typing.python.org/en/latest/spec/index.html)的真实违反。 -这些规则开箱即用——在您自己的项目中,无需任何配置,您得到的正是它们,级别为 -`error`。配置文件可以把其中某条降级为 `warning` 或 `info`,但任何表都无法把 -它关掉。 - -其余的都是 Basilisk 的可选自定规则(处处要求注解、要求 `@override` 等)。 -在某个 `[tool.basilisk]` 表选中它们之前,它们保持沉默。Basilisk 针对每个被 -检查的文件,从该文件所在目录逐级向上查找配置,最近的、对某条规则作出决定的表 -直接胜出。这里有两个表在起作用:根 `pyproject.toml` 为整个仓库选中这些自定 -规则,而 `examples/pyproject.toml`——对 `examples/` 下的一切来说更近的那个 -表——把 `BSK-0001`–`BSK-0005` 与 `BSK-0025` 降级为 `warning`。这正是文档教授 -的渐进式采纳方式:警告意味着"这段代码通过类型检查,但严格度还没有拉满"。 -examples 的表没有提到的规则(例如 `BSK-0014` 与 `BSK-0050`)仍由根表决定。 - -这就是全部的作用域机制。若要让某条规则在目录树的某一部分以不同严重级别运行, -请在该文件夹放一个带有自己的 `[tool.basilisk]` 表的 `pyproject.toml`;这里 -没有 glob 路径模式,没有按模块的表,也没有预设或模式。 - -## 文件 - -### 违规展示(包含大量诊断) - -| 文件 | 领域 | PEP 规则(始终启用,此处为错误) | Basilisk 自定规则(可选启用) | -|---|---|---|---| -| [bad.py](bad.py) | 最小化导览 | `calls_argument_type`, `returns_compatibility`, `assignment_compatibility`, `calls_argument_count`, `classes_override`, `names_unbound`, `match_exhaustiveness` | BSK-0001, BSK-0002, BSK-0004 | -| [mixed.py](mixed.py) | 混合:有类型 / 无类型 | `calls_argument_type` | BSK-0001, BSK-0002 | -| [api_server.py](api_server.py) | REST API 处理器 | `assignment_compatibility`, `overloads_consistency`, `names_unbound`, `dict_key_hashable`, `classes_override_2` | BSK-0001–BSK-0003, BSK-0025, BSK-0014, BSK-0050 | -| [data_pipeline.py](data_pipeline.py) | ETL 管道 | `assignment_compatibility`, `overloads_consistency`, `names_unbound`, `dict_key_hashable`, `classes_override_2` | BSK-0001–BSK-0003, BSK-0025, BSK-0014 | -| [ml_trainer.py](ml_trainer.py) | 机器学习训练循环 | `assignment_compatibility`, `overloads_consistency`, `match_exhaustiveness`, `dict_key_hashable`, `classes_override_2` | BSK-0001–BSK-0003, BSK-0025, BSK-0014, BSK-0050 | -| [finance.py](finance.py) | 财务计算 | `assignment_compatibility`, `classes_override_2`, `overloads_consistency`, `names_unbound`, `match_exhaustiveness`, `dict_key_hashable` | BSK-0001–BSK-0003, BSK-0025, BSK-0014, BSK-0050 | -| [cli_tool.py](cli_tool.py) | CLI 应用程序 | `assignment_compatibility`, `classes_override_2`, `overloads_consistency`, `names_unbound`, `match_exhaustiveness`, `dict_key_hashable` | BSK-0001–BSK-0003, BSK-0025, BSK-0014, BSK-0050 | -| [weird_violations.py](weird_violations.py) | 微妙的边界情况 | `overloads_consistency`, `names_unbound`, `classes_override_2`, `assignment_compatibility`, `match_exhaustiveness`, `dict_key_hashable` | BSK-0001–BSK-0003, BSK-0014, BSK-0050 | - -### 无错误对照版本(零诊断) - -| 文件 | 对照 | -|---|---| -| [good.py](good.py) | `bad.py` 的修复版——在完全严格模式下通过 | -| [api_server_clean.py](api_server_clean.py) | `api_server.py` 的修复版 | - -### 调试器与性能分析器演示(按 F5 启动) - -这些是无错误(干净)、完整类型注解的脚本,旨在 Basilisk 调试器下*运行*,而非进行静态检查。打开其中一个并按 F5。 - -| 文件 | 演示内容 | 使用方式 | -|---|---|---| -| [debug_demo.py](debug_demo.py) | 断点、Watch 面板、Locals、Debug Console | 设置一个断点并单步执行 | -| [profile_demo.py](profile_demo.py) | CPU 性能分析——几秒钟具有明显热点的 CPU 密集型工作,让火焰图和热点行热力图填充起来 | 一键操作:**Run & Profile CPU (Current File)** | -| [cpu_demo.py](cpu_demo.py) | CPU 采样——热/温/冷火焰图、热点行提示 | 将 CPU 性能分析器附加到正在运行的会话 | -| [memory_demo.py](memory_demo.py) | 内存——持续泄漏、瞬时峰值、引用循环;该运行会在退出时捕获最终快照,因此结束时会生成可查看的热力图 / `.heapprofile` | 一键操作:**Run & Track Memory (Current File)** | -| [heap_demo.py](heap_demo.py) | 内存——约 70 MB 的大块温缓存,分布在约 40 个不同的分配位置,使 `.heapprofile` 火焰图和 Self-Size 表格填满多样、真实的数据切片 | 一键操作:**Run & Track Memory (Current File)** | - -## 规则参考 - -每条诊断末尾都带有指向其文档页面的 `see:` 链接。完整目录见 -[basilisk-python.dev/docs/rules](https://www.basilisk-python.dev/docs/rules/)。 - -### 此处展示的 PEP 类型规范规则(始终启用,此处为错误) - -| 代码 | 含义 | -|---|---| -| `calls_argument_type` | 实参与形参声明的类型不兼容 | -| `calls_argument_count` | 调用时参数数量错误 | -| `returns_compatibility` / `returns_compatibility_2` | 返回值不能赋值给声明的返回类型 | -| `assignment_compatibility` | 赋的值不能赋值给注解类型 | -| `classes_override` | `@override` 方法与基类方法不兼容 | -| `classes_override_2` | 属性重写与基类不兼容 | -| `names_unbound` | 变量在某些执行路径上可能未绑定 | -| `match_exhaustiveness` | 非穷尽的 `match`——缺少通配 `case _:` 分支 | -| `dict_key_hashable` | 不可哈希的类型被用作字典键 | -| `overloads_consistency` | `@overload` 组不一致或相互重叠 | - -### 此处展示的 Basilisk 自定规则(可选启用) - -严重级别一列是管辖 `examples/` 的那些表所选中的值,而不是代码自身的属性—— -规则代码不携带任何严重级别类别。在未作配置的项目中,这些规则根本不会运行。 - -| 代码 | 含义 | 此处的严重级别 | -|---|---|---| -| BSK-0001 | 缺少参数类型注解 | warning | -| BSK-0002 | 缺少返回值类型注解 | warning | -| BSK-0003 | 无法推断空集合或 `None` 的类型 | warning | -| BSK-0004 | 缺少 `*args` / `**kwargs` 类型注解 | warning | -| BSK-0025 | 重写缺少 `@override` 装饰器 | warning | -| BSK-0014 | 使用显式 `Any` 但缺少说明 | warning | -| BSK-0050 | 冗余的类型注解 | warning | diff --git a/examples/api_server.py b/examples/api_server.py deleted file mode 100644 index 7635603ba..000000000 --- a/examples/api_server.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -REST API handler — realistic web service code with type violations. - -Run: basilisk check examples/api_server.py -""" - -from __future__ import annotations - -import json -from typing import Any, overload - - -# ── BSK-0003: can't infer type from empty dict literal ───────────────────── -_route_table = {} # BSK-0003: empty dict, no annotation -_middleware_stack = [] # BSK-0003: empty list, no annotation - - -# ── BSK-0001/0002: untyped handler signatures ────────────────────────────── -def handle_get(request, context): # BSK-0001: request, context untyped - user_id = request.get("user_id") - return {"user": user_id} # BSK-0002: no return type - - -def handle_post(request, body, auth): # BSK-0001: three untyped params - if not auth: - return None - return body # BSK-0002: no return type - - -# ── returns_compatibility: naked Any in public API signature ───────────────────────────── -def serialize(value: Any) -> Any: # returns_compatibility: Any in/out, no justification - return json.dumps(value) - - -# ── assignment_compatibility: int field assigned a string at module level ─────────────────── -MAX_RETRIES: int = "three" # assignment_compatibility: "three" is not int -TIMEOUT_MS: int = 30.5 # assignment_compatibility: float assigned to int - - -# ── classes_override_2: child route overrides attribute with incompatible type ───────── -class BaseRoute: - path: str - method: str - priority: int - - -class AdminRoute(BaseRoute): - priority: str = "high" # classes_override_2: str overrides int - - -# ── overloads_consistency: overload signatures identical (both take no-annotation param) ── -@overload -def parse_id(raw) -> int: ... # BSK-0001: raw untyped - - -@overload -def parse_id(raw) -> int: ... # BSK-0001 + overloads_consistency: duplicate overload - - -def parse_id(raw: str) -> int: - return int(raw) - - -# ── dict_key_hashable: unhashable list literal as dict key ──────────────────────────── -def default_routes() -> dict[ - list[str], str -]: # dict_key_hashable inside return annotation - return {["GET", "POST"]: "/"} # dict_key_hashable: list literal as key - - -# ── BSK-0025: override without @override decorator ───────────────────────── -class Router: - def resolve(self, path: str) -> str: - return path - - -class PrefixRouter(Router): - prefix: str = "/api" - - def resolve(self, path: str) -> str: # BSK-0025: missing @override - return self.p + path - - -# ── names_unbound: variable assigned inside if, returned outside ───────────────── -def extract_token(headers: dict[str, str]) -> str: - if "Authorization" in headers: - token = headers["Authorization"].split(" ")[-1] - return token # names_unbound: token may be unbound diff --git a/examples/api_server_clean.py b/examples/api_server_clean.py deleted file mode 100644 index 96445740a..000000000 --- a/examples/api_server_clean.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -REST API handler — fully typed, passes Basilisk with zero diagnostics. - -Run: basilisk check examples/api_server_clean.py -""" - -from __future__ import annotations - -import json -from typing import ClassVar, overload, override - - -# Properly annotated module-level state -_route_table: dict[str, str] = {} -_middleware_stack: list[str] = [] - - -# Typed handler signatures -def handle_get(request: dict[str, str], context: str) -> dict[str, str]: - user_id = request.get("user_id", "") - return {"user": user_id} - - -def handle_post( - request: dict[str, str], - body: dict[str, str], - auth: str, -) -> dict[str, str] | None: - if not auth: - return None - return body - - -# `object` instead of `Any` — JSON can be any valid JSON type, and -# `json.dumps` accepts `object`, so no suppression is needed -def serialize(value: object) -> str: - return json.dumps(value) - - -# Properly typed constants -MAX_RETRIES = 3 -TIMEOUT_MS = 30_000 - - -# Consistent attribute types in the hierarchy -class BaseRoute: - path: str - method: str - priority: int - - -class AdminRoute(BaseRoute): - priority = 100 # same type as the base class - - -# Non-overlapping overloads -@overload -def parse_id(raw: str) -> int: ... - - -@overload -def parse_id(raw: bytes) -> int: ... # different param type - - -def parse_id(raw: str | bytes) -> int: - return int(raw) - - -# Hashable key -def register_handler(path: str, method: str) -> None: - _route_table[path] = method # str is hashable - - -# @override present -class Router: - def resolve(self, path: str) -> str: - return path - - -class PrefixRouter(Router): - prefix: ClassVar[str] = "/api" - - @override - def resolve(self, path: str) -> str: - return self.prefix + path - - -# Variable always bound before use -def extract_token(headers: dict[str, str]) -> str: - return headers.get("Authorization", "").split(" ")[-1] diff --git a/examples/bad.py b/examples/bad.py deleted file mode 100644 index 192f44345..000000000 --- a/examples/bad.py +++ /dev/null @@ -1,71 +0,0 @@ -# Every diagnostic in the first section is a genuine PEP typing-spec -# violation. Basilisk reports all of them out of the box — no configuration, -# every one an error. -# -# Run: basilisk check examples/bad.py -# -# The final section violates only Basilisk's opt-in strictness rules. Those -# stay silent until a project enables them — per rule, at any severity — in -# `[tool.basilisk.rules]` or via "Basilisk: Open Configuration Editor" in -# VS Code. This repository enables them for `examples/**` as warnings in the -# root `pyproject.toml`: the incremental-adoption setup, where warnings mean -# "this type-checks, but strictness isn't at full yet". - -from typing import override - - -def greet(name: str) -> str: - return "Hello, " + name - - -greet(42) # error[calls_argument_type]: `name` expects `str`, got an `int` - - -def get_score() -> int: # error[returns_compatibility]: declared `int`, returns `str` - return "high" # error[returns_compatibility_2]: `str` is not assignable to `int` - - -count: int = "zero" # error[assignment_compatibility]: annotated `int`, assigned `str` - - -def add(x: int, y: int) -> int: - return x + y - - -add(1) # error[calls_argument_count]: missing required argument `y` - - -class Shape: - def area(self, scale: float) -> float: - return scale - - -class Circle(Shape): - @override - def area( - self, scale: str - ) -> float: # error[classes_override]: incompatible with `Shape.area` - return 1.0 - - -def describe(flag: bool) -> str: - if flag: - label = "on" - return label # error[names_unbound]: `label` is unbound when `flag` is false - - -def classify(value: int | str) -> str: - match value: # error[match_exhaustiveness]: no `case _:` branch - case int(): - return "number" - - -# ── Opt-in strictness rules — silent until enabled ────────────────────────── - - -def process(data): # BSK-0001: `data` untyped; BSK-0002: no return type - return data.upper() - - -def log_all(*args, **kwargs): # BSK-0004: `*args` / `**kwargs` untyped - pass diff --git a/examples/cli_tool.py b/examples/cli_tool.py deleted file mode 100644 index 45dc22731..000000000 --- a/examples/cli_tool.py +++ /dev/null @@ -1,131 +0,0 @@ -""" -CLI tool — realistic command-line application with type violations. - -This models the kind of ad-hoc scripting code that gradually grows -into a maintenance problem. Every violation has a plausible story. - -Run: basilisk check examples/cli_tool.py -""" - -from __future__ import annotations - -import sys -from typing import Any, overload - - -# ── BSK-0003: unannotated state at module scope ───────────────────────────── -_parsed_flags = {} # BSK-0003: empty dict -_positional_args = [] # BSK-0003: empty list -_subcommand_map = {} # BSK-0003: empty dict - - -# ── BSK-0001/0002: argument parsing functions without any types ───────────── -def parse_flag(argv, name, default): # BSK-0001: three untyped params - """Return the value of --name from argv, or default.""" - for i, arg in enumerate(argv): - if arg == f"--{name}" and i + 1 < len(argv): - return argv[i + 1] - return default # BSK-0002: no return type - - -def run_subcommand(name, args, env): # BSK-0001: three untyped params - handler = _subcommand_map.get(name) - if handler: - handler(args, env) - # BSK-0002: no return type - - -def format_error(code, message, context): # BSK-0001: three untyped params - return f"[E{code}] {message} ({context})" # BSK-0002: no return type - - -# ── returns_compatibility: Any in public-facing output function ────────────────────────── -def print_result( - value: Any, -) -> None: # returns_compatibility: Any param, no justification - print(value) - - -def load_config(path: str) -> Any: # returns_compatibility: Any return - return {} - - -# ── assignment_compatibility: exit code assigned a string, verbosity a float ──────────────── -EXIT_SUCCESS: int = "0" # assignment_compatibility: str assigned to int -EXIT_FAILURE: int = "1" # assignment_compatibility: str assigned to int -DEFAULT_VERBOSITY: int = 1.5 # assignment_compatibility: float assigned to int - - -# ── classes_override_2: subcommand narrows timeout type incompatibly ────────────────── -class Command: - name: str - timeout: int - retryable: bool - - -class NetworkCommand(Command): - timeout: float = 30.0 # classes_override_2: float overrides int - retryable: str = "yes" # classes_override_2: str overrides bool - - -# ── names_undefined: reference before module-level assignment ────────────────────── -def get_version_string() -> str: - return f"v{VERSION}" # names_undefined: VERSION not yet defined - - -VERSION: str = "1.0.0" - - -# ── names_unbound: output path only bound inside a branch ──────────────────────── -def resolve_output(flags: dict[str, str], default: bool) -> str: - if "output" in flags: - out_path = flags["output"] - elif default: - out_path = "/tmp/out.txt" - # no else — out_path unbound if neither condition holds - return out_path # names_unbound: out_path may be unbound - - -# ── overloads_consistency: unannotated params make overloads identical ─────────────────── -@overload -def coerce_value(raw, kind) -> int: ... # BSK-0001: raw, kind untyped - - -@overload -def coerce_value(raw, kind) -> int: ... # BSK-0001 + overloads_consistency: duplicate - - -def coerce_value(raw: str, kind: str) -> int: - return int(raw) - - -# ── dict_key_hashable: list literal as a dict key (command alias map) ───────────────── -def default_aliases() -> dict[list[str], str]: - return {["help", "h", "?"]: "help"} # dict_key_hashable: list literal as key - - -# ── match_exhaustiveness: non-exhaustive match on log level ───────────────────────────── -def emit_log(level: str, msg: str) -> None: - match level: - case "info": - print(f"[INFO] {msg}") - case "warn": - print(f"[WARN] {msg}", file=sys.stderr) - case "error": - print(f"[ERROR] {msg}", file=sys.stderr) - # match_exhaustiveness: no wildcard — "debug", "trace" etc. are silently dropped - - -# ── BSK-0025: override missing @override decorator ───────────────────────── -class BaseFormatter: - def format(self, record: dict[str, str]) -> str: - return str(record) - - -class JsonFormatter(BaseFormatter): - indent: int = 2 - - def format(self, record: dict[str, str]) -> str: # BSK-0025: no @override - import json - - return json.dumps(record, indent=self.indent) diff --git a/examples/cpu_demo.py b/examples/cpu_demo.py deleted file mode 100644 index 6343825a5..000000000 --- a/examples/cpu_demo.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Basilisk CPU Profiling Demo — open this file and start a profiling session. - -Launch it under the Basilisk debugger (F5), then attach the CPU profiler to -the live session. The workload below is deliberately lopsided so the flame -chart, bottom-up table, and inline hot-line hints all have something to show: - - hot_primes() -> dominates self-time (trial division, no sieve) - warm_strings() -> moderate self-time (quadratic string concat) - cold_io() -> almost all wall time in sleep, near-zero CPU - -Things to look for once the .cpuprofile opens: - * `is_prime` should be the heaviest leaf in the bottom-up view. - * `fib_recursive` shows a deep, self-similar flame (exponential recursion). - * `cold_io` barely appears — sampling profilers don't bill time spent asleep. -""" - -import time - - -def is_prime(candidate: int) -> bool: - """Deliberately naive primality test — the CPU hot spot of this demo.""" - if candidate < 2: - return False - divisor = 2 - while divisor * divisor <= candidate: # Hot line: most samples land here. - if candidate % divisor == 0: - return False - divisor += 1 - return True - - -def hot_primes(limit: int) -> list[int]: - """Collect primes below `limit` the slow way to burn CPU on one function.""" - return [n for n in range(limit) if is_prime(n)] - - -def fib_recursive(n: int) -> int: - """Exponential recursion — produces a tall, self-similar flame graph.""" - if n < 2: - return n - return fib_recursive(n - 1) + fib_recursive(n - 2) - - -def warm_strings(rows: int) -> str: - """Quadratic string building — moderate, steady self-time.""" - report = "" - for index in range(rows): - report += f"row {index}: {'#' * (index % 40)}\n" # Reallocates each pass. - return report - - -def cold_io(rounds: int) -> int: - """Mostly sleeping — shows how little CPU blocked I/O actually costs.""" - total = 0 - for _ in range(rounds): - time.sleep(0.05) # Wall time burns here, but the CPU profile stays flat. - total += 1 - return total - - -def main() -> None: - # Run a few rounds so the sampler accumulates a clear, stable picture. - for round_number in range(5): - primes = hot_primes(60_000) - digest = fib_recursive(30) - report = warm_strings(4_000) - idle = cold_io(4) - print( - f"round {round_number}: " - f"{len(primes)} primes, fib={digest}, " - f"{len(report)} report chars, {idle} idle ticks" - ) - - -if __name__ == "__main__": - main() diff --git a/examples/cpu_demo_loop.py b/examples/cpu_demo_loop.py deleted file mode 100644 index e194f2e0a..000000000 --- a/examples/cpu_demo_loop.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Basilisk CPU Profiling Demo (long-running) — start it, then attach the profiler. - -Same lopsided workload as `cpu_demo.py`, but it never stops on its own: it runs -the workload in an endless loop so you can launch it (F5 under the Basilisk -debugger), let it warm up, then attach the CPU profiler to the live session and -watch samples accumulate in real time. - - hot_primes() -> dominates self-time (trial division, no sieve) - warm_strings() -> moderate self-time (quadratic string concat) - cold_io() -> almost all wall time in sleep, near-zero CPU - -Stop it with Ctrl-C (or by detaching/stopping the debug session) when you're -done collecting samples. - -Things to look for once the .cpuprofile opens: - * `is_prime` should be the heaviest leaf in the bottom-up view. - * `fib_recursive` shows a deep, self-similar flame (exponential recursion). - * `cold_io` barely appears — sampling profilers don't bill time spent asleep. -""" - -import time - - -def is_prime(candidate: int) -> bool: - """Deliberately naive primality test — the CPU hot spot of this demo.""" - if candidate < 2: - return False - divisor = 2 - while divisor * divisor <= candidate: # Hot line: most samples land here. - if candidate % divisor == 0: - return False - divisor += 1 - return True - - -def hot_primes(limit: int) -> list[int]: - """Collect primes below `limit` the slow way to burn CPU on one function.""" - return [n for n in range(limit) if is_prime(n)] - - -def fib_recursive(n: int) -> int: - """Exponential recursion — produces a tall, self-similar flame graph.""" - if n < 2: - return n - return fib_recursive(n - 1) + fib_recursive(n - 2) - - -def warm_strings(rows: int) -> str: - """Quadratic string building — moderate, steady self-time.""" - report = "" - for index in range(rows): - report += f"row {index}: {'#' * (index % 40)}\n" # Reallocates each pass. - return report - - -def cold_io(rounds: int) -> int: - """Mostly sleeping — shows how little CPU blocked I/O actually costs.""" - total = 0 - for _ in range(rounds): - time.sleep(0.05) # Wall time burns here, but the CPU profile stays flat. - total += 1 - return total - - -def main() -> None: - # Loop forever so the sampler can be attached at any time and keep filling. - # Press Ctrl-C (or stop the debug session) to exit. - round_number = 0 - while True: - primes = hot_primes(60_000) - digest = fib_recursive(30) - report = warm_strings(4_000) - idle = cold_io(4) - print( - f"round {round_number}: " - f"{len(primes)} primes, fib={digest}, " - f"{len(report)} report chars, {idle} idle ticks" - ) - round_number += 1 - - -if __name__ == "__main__": - try: - main() - except KeyboardInterrupt: - print("\nstopped") diff --git a/examples/data_pipeline.py b/examples/data_pipeline.py deleted file mode 100644 index 45b47b22d..000000000 --- a/examples/data_pipeline.py +++ /dev/null @@ -1,103 +0,0 @@ -""" -ETL data pipeline — realistic data engineering code with type violations. - -Run: basilisk check examples/data_pipeline.py -""" - -from __future__ import annotations - -from typing import Any, overload - - -# ── BSK-0003: unannotated empty collections at module scope ───────────────── -_schema_cache = {} # BSK-0003: empty dict -_transform_registry = [] # BSK-0003: empty list - - -# ── BSK-0001/0002: untyped ETL stage functions ───────────────────────────── -def extract(source, options): # BSK-0001: source, options untyped - records = source.read_all() - return records # BSK-0002: no return type - - -def transform(records, schema, strict): # BSK-0001: three untyped params - result = [] - for row in records: - result.append(row) - return result # BSK-0002: no return type - - -def load(records, destination): # BSK-0001: records, destination untyped - destination.write(records) - # implicit return None — no annotation # BSK-0002: no return type - - -# ── returns_compatibility: Any annotation without justification ────────────────────────── -def coerce_field(value: Any, target_type: Any) -> Any: # returns_compatibility ×3 - return target_type(value) - - -# ── assignment_compatibility: type-incompatible constant assignments ──────────────────────── -BATCH_SIZE: int = "1000" # assignment_compatibility: str, not int -NULL_SENTINEL: float = "NaN" # assignment_compatibility: str, not float -MAX_ERRORS: int = 0.5 # assignment_compatibility: float, not int - - -# ── classes_override_2: subclass narrows column type incompatibly ───────────────────── -class Column: - name: str - dtype: str - nullable: bool - - -class PartitionKey(Column): - nullable: int = 0 # classes_override_2: int overrides bool - - -# ── names_undefined: name used before any assignment in the module ───────────────── -def validate_schema(name: str) -> bool: - return name in _known_types # names_undefined: _known_types undefined - - -_known_types: set[str] = {"int", "str", "float", "bool"} - - -# ── names_unbound: conditionally assigned variable returned unconditionally ─────── -def detect_encoding(raw_bytes: bytes) -> str: - if raw_bytes[:3] == b"\xef\xbb\xbf": - encoding = "utf-8-sig" - elif raw_bytes[:2] in (b"\xff\xfe", b"\xfe\xff"): - encoding = "utf-16" - # no else branch — encoding may be unbound if no BOM matches - return encoding # names_unbound: encoding may be unbound - - -# ── overloads_consistency: unannotated overload params produce a duplicate ─────────────── -@overload -def read_source(path) -> list[dict[str, str]]: ... # BSK-0001: path untyped - - -@overload -def read_source( - path, -) -> list[dict[str, str]]: ... # BSK-0001 + overloads_consistency: duplicate - - -def read_source(path: str) -> list[dict[str, str]]: - return [] - - -# ── dict_key_hashable: list literal used as a dict key ─────────────────────────────── -def empty_schema() -> dict[list[str], str]: # unhashable key type in annotation - return {["a", "b"]: "string"} # dict_key_hashable: list literal as key - - -# ── BSK-0025: override not decorated ─────────────────────────────────────── -class BaseWriter: - def flush(self, data: list[bytes]) -> int: - return len(data) - - -class ParquetWriter(BaseWriter): - def flush(self, data: list[bytes]) -> int: # BSK-0025: missing @override - return len(data) * 2 diff --git a/examples/debug_demo.py b/examples/debug_demo.py deleted file mode 100644 index 10dd6ce04..000000000 --- a/examples/debug_demo.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Basilisk Debug Demo — open this file and press F5 to try the debugger. - -Set breakpoints on any line, then step through to see variables in the -Watch panel, Locals scope, and Debug Console. -""" - - -def fibonacci(n: int) -> list[int]: - """Generate the first n Fibonacci numbers.""" - seq: list[int] = [] - a, b = 0, 1 - for _ in range(n): - seq.append(a) # Set a breakpoint here to watch the sequence grow - a, b = b, a + b - return seq - - -def classify_numbers(numbers: list[int]) -> dict[str, list[int]]: - """Split a list into evens and odds.""" - result: dict[str, list[int]] = {"even": [], "odd": []} - for num in numbers: - if num % 2 == 0: - result["even"].append(num) - else: - result["odd"].append(num) - return result - - -def main() -> None: - # Step through these lines and inspect variables in the Watch panel. - name = "Basilisk" - version = "0.1.0" - greeting = f"Welcome to {name} v{version} debugger demo!" - print(greeting) - - # Watch `fib` grow as you step through fibonacci(). - fib = fibonacci(10) - print(f"Fibonacci(10): {fib}") - - # Inspect the classified dict in the Variables pane. - classified = classify_numbers(fib) - print(f"Even: {classified['even']}") - print(f"Odd: {classified['odd']}") - - # Try evaluating these in the Debug Console (REPL): - # sum(fib) - # len(classified["even"]) - # [x ** 2 for x in fib] - total = sum(fib) - print(f"Sum: {total}") - - -if __name__ == "__main__": - main() diff --git a/examples/exception_demo.py b/examples/exception_demo.py deleted file mode 100644 index 9861375cc..000000000 --- a/examples/exception_demo.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Basilisk Exception Demo — set breakpoints inside except blocks to inspect exceptions. - -Press F5 with this file open. Put breakpoints on the `print(...)` lines -inside each `except` block, then check the Variables pane for `exc`. -""" - - -class ValidationError(Exception): - """Custom exception with extra attributes.""" - - def __init__(self, field: str, message: str) -> None: - super().__init__(f"{field}: {message}") - self.field = field - self.message = message - - -def main() -> None: - # 1. KeyError — inspect exc, exc.args - try: - data: dict[str, int] = {"a": 1, "b": 2} - _value = data["missing_key"] - except KeyError as exc: - print(f"Caught KeyError: {exc}") # breakpoint here - - # 2. ZeroDivisionError — inspect exc.args[0] - try: - _result = 100 / 0 - except ZeroDivisionError as exc: - print(f"Caught ZeroDivisionError: {exc}") # breakpoint here - - # 3. IndexError — inspect type(exc), exc.args - try: - numbers: list[int] = [10, 20, 30] - _bad = numbers[99] - except IndexError as exc: - print(f"Caught IndexError: {exc}") # breakpoint here - - # 4. ValueError with chained exception — inspect exc.__cause__ - try: - try: - int("not_a_number") - except ValueError as original: - raise ValueError("Failed to parse config") from original - except ValueError as exc: - print(f"Caught chained ValueError: {exc}") # breakpoint here - print(f" Original cause: {exc.__cause__}") - - # 5. Custom exception — inspect exc.field, exc.message - try: - raise ValidationError("age", "must be >= 0") - except ValidationError as exc: - print(f"Caught ValidationError: {exc}") # breakpoint here - print(f" field={exc.field}, message={exc.message}") - - print("\nAll exceptions handled successfully.") - - -if __name__ == "__main__": - main() diff --git a/examples/finance.py b/examples/finance.py deleted file mode 100644 index 6803a5b17..000000000 --- a/examples/finance.py +++ /dev/null @@ -1,126 +0,0 @@ -""" -Financial calculations — realistic fintech code with type violations. - -The violations here are subtle: wrong numeric types, shadowed names, -conditional assignments in risk functions that may never bind. - -Run: basilisk check examples/finance.py -""" - -from __future__ import annotations - -from typing import Any, overload - - -# ── BSK-0003: empty portfolio and ledger ──────────────────────────────────── -_open_positions = {} # BSK-0003: empty dict, no annotation -_trade_log = [] # BSK-0003: empty list, no annotation - - -# ── BSK-0001/0002: core pricing functions missing all annotations ─────────── -def black_scholes(S, K, T, r, sigma): # BSK-0001: five untyped params - """Call option price — classic formula.""" - import math - - d1 = (math.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * math.sqrt(T)) - _d2 = d1 - sigma * math.sqrt(T) - # omit N(d1)/N(d2) for brevity - return S - K * math.exp(-r * T) # BSK-0002: no return type - - -def present_value(cash_flows, discount_rate): # BSK-0001: untyped params - total = 0.0 - for i, cf in enumerate(cash_flows): - total += cf / (1 + discount_rate) ** i - return total # BSK-0002: no return type - - -def kelly_criterion(win_prob, win_amount, loss_amount): # BSK-0001 - edge = win_prob * win_amount - (1 - win_prob) * loss_amount - return edge / win_amount # BSK-0002: no return type - - -# ── returns_compatibility: Any type with no justification ───────────────────────────────── -def execute_order(order: Any) -> Any: # returns_compatibility ×2 - return order - - -# ── assignment_compatibility: currency constant assigned wrong type ───────────────────────── -BASE_CURRENCY: str = 42 # assignment_compatibility: int assigned to str -RISK_FREE_RATE: float = "0.05" # assignment_compatibility: str assigned to float -MAX_POSITION_SIZE: int = 1_000_000.0 # assignment_compatibility: float assigned to int - - -# ── classes_override_2: subclass changes field type in class hierarchy ──────────────── -class Instrument: - ticker: str - notional: float - is_derivative: bool - - -class Future(Instrument): - notional: int = 0 # classes_override_2: int overrides float - - -class Option(Instrument): - is_derivative: str = "yes" # classes_override_2: str overrides bool - - -# ── names_undefined: forward reference to name assigned later ────────────────────── -def get_benchmark() -> str: - return BENCHMARK_INDEX # names_undefined: referenced before assignment - - -BENCHMARK_INDEX: str = "SP500" - - -# ── names_unbound: VaR only assigned inside the risk branch ────────────────────── -def compute_portfolio_risk( - returns: list[float], confidence: float, stressed: bool -) -> float: - if stressed: - sorted_returns = sorted(returns) - cutoff = int(len(sorted_returns) * (1 - confidence)) - var = abs(sorted_returns[cutoff]) - return var # names_unbound: var may be unbound - - -# ── overloads_consistency: unannotated params make overloads identical ─────────────────── -@overload -def round_to_tick(price, tick) -> float: ... # BSK-0001: price, tick untyped - - -@overload -def round_to_tick( - price, tick -) -> float: ... # BSK-0001 + overloads_consistency: duplicate - - -def round_to_tick(price: float, tick: int) -> float: - return round(price / tick) * tick - - -# ── dict_key_hashable: list literal used as a dict key in a position record ─────────── -def empty_book() -> dict[list[str], float]: - return {["AAPL", "MSFT"]: 0.0} # dict_key_hashable: list literal as key - - -# ── match_exhaustiveness: non-exhaustive match on order side ──────────────────────────── -def apply_slippage(side: str, price: float, bps: float) -> float: - match side: - case "buy": - return price * (1 + bps / 10_000) - case "sell": - return price * (1 - bps / 10_000) - # match_exhaustiveness: no wildcard — "short", "cover", etc. fall through - - -# ── BSK-0025: settlement override missing @override ──────────────────────── -class BaseSettlement: - def settle(self, amount: float, currency: str) -> str: - return f"{amount} {currency}" - - -class T2Settlement(BaseSettlement): - def settle(self, amount: float, currency: str) -> str: # BSK-0025 - return f"T+2: {amount} {currency}" diff --git a/examples/good.py b/examples/good.py deleted file mode 100644 index 8cf702e74..000000000 --- a/examples/good.py +++ /dev/null @@ -1,59 +0,0 @@ -# The fixed counterpart of `bad.py` — every diagnostic addressed, including -# the opt-in strictness rules. Passes Basilisk cleanly at full strictness. -# Run: basilisk check examples/good.py - -from typing import override - - -def greet(name: str) -> str: - return "Hello, " + name - - -def get_score() -> int: - return 42 - - -def add(x: int, y: int) -> int: - return x + y - - -class Shape: - def area(self, scale: float) -> float: - return scale - - -class Circle(Shape): - @override - def area(self, scale: float) -> float: - return scale * 3.14 - - -def describe(flag: bool) -> str: - return "on" if flag else "off" - - -def classify(value: int | str) -> str: - match value: - case int(): - return "number" - case _: - return "text" - - -def process(data: str) -> str: - return data.upper() - - -def log_all(*args: str, **kwargs: int) -> None: - pass - - -def main() -> None: - print(greet("world")) - print(add(get_score(), 2)) - print(describe(flag=True)) - print(classify("basilisk")) - - -if __name__ == "__main__": - main() diff --git a/examples/heap_demo.py b/examples/heap_demo.py deleted file mode 100644 index 2c4aa520b..000000000 --- a/examples/heap_demo.py +++ /dev/null @@ -1,140 +0,0 @@ -"""Basilisk Heap-Profile Demo — open this file and click -"Run & Track Memory (Current File)" in the Python Processes panel. - -A deliberately *chunky* memory workload: it builds an in-memory analytics cache -of tens of megabytes across many distinct allocation sites, then keeps all of it -alive in the module-global WAREHOUSE until the program exits. The run needs no -breakpoint — Basilisk starts tracemalloc at the entry pause, runs to completion, -and captures a final snapshot as the program exits ([PROFILE-MEMORY-FINAL]). - -Why this makes a good `.heapprofile`: Basilisk filters the debugger's own -allocations out and keeps each allocation's full call stack, so the viewer shows -a real call tree of *your* code — `warm_cache` branching into each builder below, -down to the line that allocated. With big buffers, medium columns, and a long -tail of small structures, the flame chart and the Self-Size table fill with real, -varied entries instead of a single dominant bar: - - allocate_frame_buffers -> a few large contiguous bytearrays (the wide bars) - build_*_series -> medium lists of numbers (the mid bars) - build_inverted_index -> thousands of small strings + posting lists (tail) - build_session_records -> objects with per-record blobs - build_adjacency_graph -> nested dict/list structure - -What to look for once the snapshot opens: - * The 8 MiB / 16 MiB buffers dominate the flame chart's widest slices. - * Each `build_*` line shows up separately in the bottom-up (Self-Size) table. - * Peak vs current: only WAREHOUSE survives, so the final total is what's live. -""" - -from __future__ import annotations - -# Everything is retained here, so the at-exit snapshot attributes each megabyte -# to the line that allocated it. Nothing is ever evicted. -WAREHOUSE: dict[str, object] = {} - -# ── Large contiguous buffers (the wide flame-chart bars) ──────────────────── - - -def allocate_frame_buffers() -> dict[str, bytearray]: - """A render cache of differently-sized buffers — one big slice per line.""" - return { - "rgba_canvas": bytearray(8 * 1024 * 1024), # 8 MiB — the widest bar. - "depth_buffer": bytearray(4 * 1024 * 1024), # 4 MiB. - "shadow_map": bytearray(2 * 1024 * 1024), # 2 MiB. - "lightmap": bytearray(1 * 1024 * 1024), # 1 MiB. - } - - -def allocate_embedding_matrix(vectors: int, dimensions: int) -> bytearray: - """A flat float32 matrix as raw bytes — one large, clean allocation.""" - return bytearray(vectors * dimensions * 4) # 4 bytes per float32 cell. - - -# ── Medium columnar series (the mid bars) ─────────────────────────────────── - - -def build_price_series(rows: int) -> list[float]: - """Distinct float objects (not interned) — a real per-line allocation.""" - return [float(index) * 1.5 + 0.25 for index in range(rows)] - - -def build_volume_series(rows: int) -> list[int]: - """Big ints (above the small-int cache) — each one really allocated.""" - return [index * index + 9_999_999 for index in range(rows)] - - -def build_label_series(rows: int) -> list[str]: - """Many short, distinct strings — a fat slice of small allocations.""" - return [f"row-{index:07d}" for index in range(rows)] - - -# ── Long tail of small structures (fills the bottom-up table) ─────────────── - - -def synth_terms(doc_id: int, terms_per_doc: int) -> list[str]: - """Synthesize a document's tokens — distinct interned-busting strings.""" - return [ - f"t{(doc_id * 131 + position) % 4096:04x}" for position in range(terms_per_doc) - ] - - -def build_inverted_index(documents: int, terms_per_doc: int) -> dict[str, list[int]]: - """token -> posting list. Thousands of tiny strings and lists.""" - index: dict[str, list[int]] = {} - for doc_id in range(documents): - for term in synth_terms(doc_id, terms_per_doc): - index.setdefault(term, []).append(doc_id) - return index - - -class SessionRecord: - """A per-session object carrying its own payload — objects with __dict__.""" - - def __init__(self, session_id: int) -> None: - self.session_id = session_id - self.token = f"sess-{session_id:08x}" - self.payload = bytes(8 * 1024) # 8 KiB blob retained per record. - - -def build_session_records(count: int) -> list[SessionRecord]: - """A list of objects, each holding an 8 KiB blob — a chunky mid slice.""" - return [SessionRecord(session_id) for session_id in range(count)] - - -def build_adjacency_graph(nodes: int, fan_out: int) -> dict[int, list[int]]: - """node -> neighbours. A nested dict/list structure with many small lists.""" - return { - node: [(node * 2_654_435_761 + step) % nodes for step in range(fan_out)] - for node in range(nodes) - } - - -# ── Orchestration ─────────────────────────────────────────────────────────── - - -def warm_cache() -> None: - """Build every subsystem and retain it, so the heap fills up for real.""" - WAREHOUSE["frame_buffers"] = allocate_frame_buffers() - WAREHOUSE["embeddings"] = allocate_embedding_matrix(vectors=65_536, dimensions=64) - WAREHOUSE["price"] = build_price_series(120_000) - WAREHOUSE["volume"] = build_volume_series(120_000) - WAREHOUSE["labels"] = build_label_series(120_000) - WAREHOUSE["index"] = build_inverted_index(documents=4_000, terms_per_doc=48) - WAREHOUSE["sessions"] = build_session_records(2_000) - WAREHOUSE["graph"] = build_adjacency_graph(nodes=8_000, fan_out=6) - - -def describe() -> str: - """A one-line summary so the run prints something on completion.""" - parts = [f"{name}={type(value).__name__}" for name, value in WAREHOUSE.items()] - return "warehouse: " + ", ".join(parts) - - -def main() -> None: - warm_cache() - print(describe()) - print(f"Loaded {len(WAREHOUSE)} retained subsystems — heap is warm for profiling.") - - -if __name__ == "__main__": - main() diff --git a/examples/memory_demo.py b/examples/memory_demo.py deleted file mode 100644 index 0a97b4e85..000000000 --- a/examples/memory_demo.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Basilisk Memory Profiling Demo — open this file and click -"Run & Track Memory (Current File)" in the Python Processes panel. - -No breakpoint required: Basilisk starts tracemalloc at the entry pause, runs the -program to completion, and captures a final memory snapshot as it exits — so the -run ends in a viewable result (the V8 `.heapprofile` plus the purple allocation -heat map on the hot line), never a dead end. - -The workload exercises every signal the memory profiler reports: - - leak_cache -> retained forever in a module-global list (the leak) - transient_spike -> a big allocation that is freed before the program ends - make_cycle -> reference cycle with __del__, only the GC can reclaim - -What to look for once the final snapshot opens: - * `leak_cache`'s `bytes(512 * 1024)` line dominates — ~3 MiB still retained. - * `transient_spike` shows up in PEAK memory but not in the final total. - * The Node cycle survives until `gc.collect()` runs at the very end. - -Prefer the interactive flow? Set a breakpoint on the marked line inside `main`'s -loop, launch under the debugger (F5), and take a snapshot each pass — the diff -between snapshots is where leak confidence escalates. -""" - -import gc - - -# Module-level store that is never cleared — the classic accidental leak. -_LEAK: list[bytes] = [] - - -class Node: - """A graph node that points back at its owner, forming a reference cycle.""" - - def __init__(self, label: str) -> None: - self.label = label - self.peer: Node | None = None # Set to another Node to close the cycle. - self.payload = bytearray(256 * 1024) # Real bytes so the leak is visible. - - def __del__(self) -> None: - # __del__ on a cycle historically blocked collection — a leak smell. - print(f"collected node {self.label}") - - -def leak_cache(round_number: int) -> int: - """Append to a module-global list that nothing ever frees.""" - chunk = bytes(512 * 1024) # 512 KiB retained forever, one per round. - _LEAK.append(chunk) - return len(_LEAK) - - -def transient_spike() -> int: - """Allocate a large buffer and drop it — peak rises, baseline does not.""" - scratch = [bytearray(1024 * 1024) for _ in range(8)] # ~8 MiB, short-lived. - total = sum(len(buffer) for buffer in scratch) - return total # `scratch` dies here; the next snapshot won't see it. - - -def make_cycle(label: str) -> None: - """Build a two-node cycle that escapes reference counting.""" - left = Node(f"{label}-left") - right = Node(f"{label}-right") - left.peer = right - right.peer = left # Now neither node's refcount can ever reach zero. - # Both go out of scope here, but the cycle keeps them alive until gc runs. - - -def main() -> None: - for round_number in range(6): - retained = leak_cache(round_number) # Breakpoint here: snapshot each pass. - spike = transient_spike() - make_cycle(f"round{round_number}") - print( - f"round {round_number}: {retained} leaked chunks, {spike} transient bytes" - ) - - # Force a collection so the cycle's __del__ output appears at the end. - unreachable = gc.collect() - print(f"gc reclaimed {unreachable} objects") - - -if __name__ == "__main__": - main() diff --git a/examples/mixed.py b/examples/mixed.py deleted file mode 100644 index f109dd0c6..000000000 --- a/examples/mixed.py +++ /dev/null @@ -1,37 +0,0 @@ -# A realistic file with a mix of typed and untyped code. -# Run: basilisk check examples/mixed.py -# -# The genuine type error is an error out of the box. The untyped parts only -# surface once the opt-in strictness rules are enabled — this repository -# enables them for `examples/**` as warnings in the root `pyproject.toml`. - -from typing import Optional - - -def fetch_user(user_id: int) -> Optional[str]: - # pretend DB lookup - return None - - -def save_record(data): # BSK-0001: data untyped - pass # BSK-0002: no return type - - -class Config: - debug: bool - timeout: int - - def __init__(self, debug: bool, timeout: int) -> None: - self.debug = debug - self.timeout = timeout - - def reset(self): # BSK-0002: no return type - self.debug = False - self.timeout = 30 - - -def compute(x: int, y: int) -> int: - return x * y - - -compute(2, "three") # error[calls_argument_type]: `y` expects `int`, got a `str` diff --git a/examples/ml_trainer.py b/examples/ml_trainer.py deleted file mode 100644 index 12e2ee5d0..000000000 --- a/examples/ml_trainer.py +++ /dev/null @@ -1,118 +0,0 @@ -""" -Machine-learning training loop — realistic ML code with type violations. - -Run: basilisk check examples/ml_trainer.py -""" - -from __future__ import annotations - -from typing import Any, overload - - -# ── BSK-0003: unannotated empty collections ───────────────────────────────── -_metric_history = [] # BSK-0003: empty list, type unknown -_checkpoint_index = {} # BSK-0003: empty dict, type unknown - - -# ── BSK-0001/0002: untyped training functions ────────────────────────────── -def forward_pass(model, batch, device): # BSK-0001: three untyped params - inputs, labels = batch - logits = model(inputs.to(device)) - return logits # BSK-0002: no return type - - -def compute_loss(logits, labels, weights): # BSK-0001: three untyped params - loss = ((logits - labels) ** 2).mean() - return loss # BSK-0002: no return type - - -def backward_and_step(loss, optimizer): # BSK-0001: two untyped params - loss.backward() - optimizer.step() - optimizer.zero_grad() # BSK-0002: no return type - - -# ── returns_compatibility: Any used in public interfaces without justification ──────────── -def load_checkpoint(path: str) -> Any: # returns_compatibility: Any return, no comment - pass - - -def apply_augmentation(sample: Any, config: Any) -> Any: # returns_compatibility ×3 - return sample - - -# ── assignment_compatibility: float hyperparameter assigned a string ──────────────────────── -LEARNING_RATE: float = "1e-3" # assignment_compatibility: str assigned to float -NUM_EPOCHS: int = 10.0 # assignment_compatibility: float assigned to int -DROPOUT: float = "0.5" # assignment_compatibility: str assigned to float - - -# ── classes_override_2: subclass changes metric type ─────────────────────────────────── -class Metric: - name: str - value: float - higher_is_better: bool - - -class LossMetric(Metric): - higher_is_better: str = "no" # classes_override_2: str overrides bool - - -# ── names_undefined: reference to name not yet defined ───────────────────────────── -def get_default_optimizer() -> str: - return DEFAULT_OPTIMIZER # names_undefined: not yet assigned - - -DEFAULT_OPTIMIZER: str = "adam" - - -# ── names_unbound: epoch stats built conditionally, returned unconditionally ────── -def run_epoch(data: list[dict[str, float]], validate: bool) -> dict[str, float]: - if validate: - val_loss = sum(r["loss"] for r in data) / len(data) - return {"val_loss": val_loss} # names_unbound: val_loss may be unbound - - -# ── overloads_consistency: unannotated params make overloads identical ─────────────────── -@overload -def decode_predictions(raw) -> list[int]: ... # BSK-0001: raw untyped - - -@overload -def decode_predictions( - raw, -) -> list[int]: ... # BSK-0001 + overloads_consistency: duplicate - - -def decode_predictions(raw: list[float]) -> list[int]: - return [round(x) for x in raw] - - -# ── dict_key_hashable: list literal as dict key ────────────────────────────────────── -def make_layer_index() -> dict[list[str], int]: - return {["conv1", "conv2"]: 0} # dict_key_hashable: list literal as key - - -# ── match_exhaustiveness: non-exhaustive match on optimizer name ──────────────────────── -def build_optimizer(name: str, lr: float) -> str: - match name: - case "adam": - return f"Adam(lr={lr})" - case "sgd": - return f"SGD(lr={lr})" - # match_exhaustiveness: no wildcard branch — other values fall through silently - - -# ── BSK-0025: override without @override ──────────────────────────────────── -class BaseCallback: - def on_epoch_end(self, epoch: int, metrics: dict[str, float]) -> None: - pass - - -class EarlyStoppingCallback(BaseCallback): - patience: int = 5 - - def on_epoch_end( # BSK-0025: missing @override - self, epoch: int, metrics: dict[str, float] - ) -> None: - pass diff --git a/examples/profile_demo.py b/examples/profile_demo.py deleted file mode 100644 index f9f75def3..000000000 --- a/examples/profile_demo.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Basilisk CPU Profiling Demo — open this file and click -"Run & Profile CPU (Current File)" in the Python Processes panel. - -Unlike debug_demo.py — which finishes in about a millisecond, far too fast for a -sampling profiler to catch — this program runs a CPU-bound workload for a few -seconds. That gives the sampler enough snapshots to build a real flame chart, -bottom-up table, and inline hot-line heat map. - -The work is deliberately lopsided so the profile points straight at the -bottleneck: - - sum_of_squares() -> the hot spot: a tight arithmetic loop - count_primes() -> moderate: naive trial-division primality - format_round() -> light: a little string formatting - -What to look for once the profile opens: - * sum_of_squares dominates the bottom-up (self-time) table. - * The `total += index * index` line wears the brightest heat-map color. - * count_primes is a clear but smaller slice; format_round barely registers. -""" - -import time - -# Run the workload for about this many seconds so the sampler builds a stable -# picture regardless of machine speed. A fixed iteration count would finish too -# fast on a quick machine (too few samples) and drag under the debugger's line -# tracing — a wall-clock deadline keeps the demo's profile meaningful either way. -PROFILE_SECONDS = 3.0 - - -def sum_of_squares(count: int) -> int: - """The hot spot: a tight arithmetic loop where most samples should land.""" - total = 0 - for index in range(count): - total += index * index # Hot line — the profiler paints this brightest. - return total - - -def count_primes(limit: int) -> int: - """Moderate cost: naive primality by trial division (no sieve).""" - found = 0 - for candidate in range(2, limit): - divisor = 2 - is_prime = True - while divisor * divisor <= candidate: - if candidate % divisor == 0: - is_prime = False - break - divisor += 1 - if is_prime: - found += 1 - return found - - -def format_round(round_number: int, squares: int, primes: int) -> str: - """Light cost: a little string formatting per round.""" - return f"round {round_number}: sum_of_squares={squares:,}, primes={primes}" - - -def main() -> None: - deadline = time.time() + PROFILE_SECONDS - round_number = 0 - summary = "" - while time.time() < deadline: - squares = sum_of_squares(100_000) - primes = count_primes(2_000) - summary = format_round(round_number, squares, primes) - round_number += 1 - print(summary) - print(f"Profiled {round_number} rounds over ~{PROFILE_SECONDS:.0f}s of CPU work.") - - -if __name__ == "__main__": - main() diff --git a/examples/pyproject.toml b/examples/pyproject.toml deleted file mode 100644 index b8ec93635..000000000 --- a/examples/pyproject.toml +++ /dev/null @@ -1,20 +0,0 @@ -# Folder-scoped Basilisk configuration for the examples ([CHKARCH-CONFIG-MODEL]). -# -# There is no per-path override table. Configuration is scoped by WHERE the -# file lives: `basilisk` walks up from each checked file and the nearest table -# that decides a rule wins outright. This file is that nearest table for -# everything under examples/, and the repository root's -# `pyproject.toml [tool.basilisk]` still decides every rule left undecided here -# (including the typeshed pin, which is a non-rule key and merges through). -# -# The examples model the incremental-adoption story the docs teach: PEP -# typing-spec violations stay errors, while Basilisk's opt-in strictness rules -# surface as warnings — "this type-checks, but strictness isn't at full yet". -# See website/src/docs/quick-start.md step 3. -[tool.basilisk.rules] -BSK-0001 = "warning" -BSK-0002 = "warning" -BSK-0003 = "warning" -BSK-0004 = "warning" -BSK-0005 = "warning" -BSK-0025 = "warning" diff --git a/examples/redundant_annotations.py b/examples/redundant_annotations.py deleted file mode 100644 index ea2f6cc25..000000000 --- a/examples/redundant_annotations.py +++ /dev/null @@ -1,713 +0,0 @@ -# ruff: noqa: E402, E731 -# Redundant type annotations — Basilisk infers these automatically. -# -# W0050 fires when the annotation adds no information beyond what inference provides. -# E0005 does NOT fire when a subclass overrides a parent's annotated attribute. -# E402/E731 are suppressed: imports are placed per-section intentionally, and -# lambda assignments are part of the test fixtures for annotation inference. - -# --------------------------------------------------------------------------- -# Module-level: scalar literals are always inferrable -# --------------------------------------------------------------------------- - -count: int = 42 # W0050 — obviously int -name: str = "hello" # W0050 — obviously str -rate: float = 3.14 # W0050 — obviously float -enabled: bool = True # W0050 — obviously bool -disabled: bool = False # W0050 — obviously bool -header: bytes = b"\x00\xff" # W0050 — obviously bytes -nothing: None = None # W0050 — obviously None - -# Edge cases: zero/empty values -zero: int = 0 # W0050 — still obviously int -empty: str = "" # W0050 — still obviously str -zero_f: float = 0.0 # W0050 — still obviously float - -# --------------------------------------------------------------------------- -# Module-level: annotations that ADD information (no W0050) -# --------------------------------------------------------------------------- - -widened: float = 42 # NO warning — int widened to float -items: list[int] = [1, 2, 3] # NO warning — collection type is useful -pairs: dict[str, int] = {"a": 1} # NO warning — collection type is useful -nums: set[int] = {1, 2, 3} # NO warning — collection type is useful -coords: tuple[int, int] = (1, 2) # NO warning — collection type is useful - - -# --------------------------------------------------------------------------- -# Class attributes: same rules apply -# --------------------------------------------------------------------------- - - -class Settings: - retries: int = 3 # W0050 — redundant - label: str = "default" # W0050 — redundant - threshold: float = 0.5 # W0050 — redundant - verbose: bool = True # W0050 — redundant - magic: bytes = b"\x00" # W0050 — redundant - nothing: None = None # W0050 — redundant - - -# --------------------------------------------------------------------------- -# Subclass overrides: inherited annotation satisfies E0005 -# --------------------------------------------------------------------------- - - -class BaseRoute: - path: str = "/" - method: str = "GET" - auth_required: bool = False - priority: int = 0 - timeout: float = 30.0 - - -class AuthenticatedRoute(BaseRoute): - auth_required = True # NO E0005 — inherits bool from BaseRoute - - -class AdminRoute(AuthenticatedRoute): - priority = 100 # NO E0005 — inherits int from BaseRoute (grandparent) - path = "/admin" # NO E0005 — inherits str from BaseRoute (grandparent) - - -class ApiRoute(AuthenticatedRoute): - method = "POST" # NO E0005 — inherits str from BaseRoute - timeout = 60.0 # NO E0005 — inherits float from BaseRoute - path = "/api" # NO E0005 — inherits str from BaseRoute - - -# --------------------------------------------------------------------------- -# Deep inheritance: annotation flows through the whole chain -# --------------------------------------------------------------------------- - - -class A: - tag: str = "a" - - -class B(A): - tag = "b" # NO E0005 — inherits from A - - -class C(B): - tag = "c" # NO E0005 — inherits from A through B - - -class D(C): - tag = "d" # NO E0005 — inherits from A through B -> C - - -# --------------------------------------------------------------------------- -# Multiple inheritance: annotation from ANY base suffices -# --------------------------------------------------------------------------- - - -class PriorityMixin: - priority: int = 0 - - -class Serializable: - pass - - -class PrioritizedItem(PriorityMixin, Serializable): - priority = 10 # NO E0005 — inherits from PriorityMixin - - -class WeightMixin: - weight: float = 1.0 - - -class WeightedItem(Serializable, WeightMixin): - weight = 5.0 # NO E0005 — inherits from WeightMixin (second base) - - -# --------------------------------------------------------------------------- -# Diamond inheritance: reachable through either path -# --------------------------------------------------------------------------- - - -class Root: - value: int = 0 - - -class Left(Root): - pass - - -class Right(Root): - pass - - -class Diamond(Left, Right): - value = 42 # NO E0005 — reachable through Left -> Root or Right -> Root - - -# --------------------------------------------------------------------------- -# Sibling classes independently overriding -# --------------------------------------------------------------------------- - - -class Animal: - sound = "..." - legs = 4 - - -class Dog(Animal): - sound = "woof" # NO E0005 - - -class Cat(Animal): - sound = "meow" # NO E0005 - - -class Snake(Animal): - legs = 0 # NO E0005 - sound = "hiss" # NO E0005 - - -# --------------------------------------------------------------------------- -# Annotation-only parent (no default): child still inherits the type -# --------------------------------------------------------------------------- - - -class AbstractHandler: - name: str - - -class ConcreteHandler(AbstractHandler): - name = "default" # NO E0005 — parent declared `name: str` - - -# --------------------------------------------------------------------------- -# Config pattern: production/staging overrides -# --------------------------------------------------------------------------- - - -class DatabaseConfig: - host = "localhost" - port = 5432 - pool_size = 10 - ssl = False - - -class ProductionDB(DatabaseConfig): - host = "db.prod.internal" - port = 5433 - ssl = True - pool_size = 50 - - -class StagingDB(DatabaseConfig): - host = "db.staging.internal" - pool_size = 5 - - -# --------------------------------------------------------------------------- -# Scalar literals in standalone classes — type is inferrable, NO E0005 -# --------------------------------------------------------------------------- - - -class Standalone: - value = 42 # NO E0005 — scalar literal, type is trivially `int` - - -class UnannotatedParent: - raw = 99 # NO E0005 — scalar literal, type is trivially `int` - - -class ChildOfUnannotated(UnannotatedParent): - raw = 100 # NO E0005 — scalar literal, type is trivially `int` - - -class UnrelatedToBaseRoute: - path = "/unrelated" # NO E0005 — scalar literal, type is trivially `str` - - -# --------------------------------------------------------------------------- -# Function parameters: annotation is required (no W0050 — params need types) -# --------------------------------------------------------------------------- - - -def greet(name: str, count: int = 1) -> str: # NO W0050 — params need annotations - return name * count - - -# --------------------------------------------------------------------------- -# Function return types: redundant when inferrable from body -# --------------------------------------------------------------------------- - - -def get_count() -> int: # W0050 — return type inferrable from `return 42` - return 42 - - -def get_name() -> str: # W0050 — return type inferrable from `return "hello"` - return "hello" - - -def get_flag() -> bool: # W0050 — return type inferrable from `return True` - return True - - -def get_rate() -> float: # W0050 — return type inferrable from `return 3.14` - return 3.14 - - -def get_data() -> bytes: # W0050 — return type inferrable from `return b"\x00"` - return b"\x00" - - -def get_nothing() -> None: # W0050 — return type inferrable from `return None` - return None - - -def implicit_none() -> None: # W0050 — no return statement implies None - pass - - -# --------------------------------------------------------------------------- -# Function return types that ADD information (no W0050) -# --------------------------------------------------------------------------- - - -def get_items() -> list[int]: # NO W0050 — collection type adds info - return [1, 2, 3] - - -def get_mapping() -> dict[str, int]: # NO W0050 — collection type adds info - return {"a": 1} - - -def widen_return() -> float: # NO W0050 — widening int to float - return 42 - - -def conditional_return(flag: bool) -> str: # NO W0050 — multiple return paths - if flag: - return "yes" - return "no" - - -# --------------------------------------------------------------------------- -# Local variables: redundant annotations -# --------------------------------------------------------------------------- - - -def local_scalars() -> None: - x: int = 10 # W0050 — obviously int - y: str = "world" # W0050 — obviously str - z: float = 2.71 # W0050 — obviously float - flag: bool = False # W0050 — obviously bool - raw: bytes = b"\xff" # W0050 — obviously bytes - nope: None = None # W0050 — obviously None - _ = (x, y, z, flag, raw, nope) - - -def local_non_redundant() -> None: - items: list[int] = [1, 2] # NO W0050 — collection type adds info - widened: float = 0 # NO W0050 — int widened to float - mapping: dict[str, bool] = {} # NO W0050 — empty collection needs type - _ = (items, widened, mapping) - - -# --------------------------------------------------------------------------- -# For-loop variables: redundant annotations -# --------------------------------------------------------------------------- - - -def loop_annotations() -> None: - total: int = 0 # W0050 — obviously int - for i in range(10): - total += i - _ = total - - -# --------------------------------------------------------------------------- -# Comprehension targets captured into annotated variables -# --------------------------------------------------------------------------- - - -def comprehension_annotations() -> None: - squares: list[int] = [x * x for x in range(5)] # NO W0050 — list[int] adds info - names: list[str] = [s.upper() for s in ["a", "b"]] # NO W0050 — list[str] adds info - _ = (squares, names) - - -# --------------------------------------------------------------------------- -# Lambda: return annotation not possible, but assignment annotation -# --------------------------------------------------------------------------- - -double = 2 # NO E0003 — scalar literal, type is trivially `int` -fn = lambda x: x * 2 # NO E0003 — not an unresolvable expression - - -# --------------------------------------------------------------------------- -# Property: redundant return annotations -# --------------------------------------------------------------------------- - - -class Circle: - def __init__(self, radius: float) -> None: # W0050 — __init__ always returns None - self._radius = radius - - @property - def radius(self) -> float: # NO W0050 — property return types are documentation - return self._radius - - @property - def area(self) -> float: # NO W0050 — computed, annotation documents interface - return 3.14159 * self._radius**2 - - @property - def name(self) -> str: # W0050 — trivially returns a literal - return "circle" - - @property - def is_unit(self) -> bool: # W0050 — trivially returns a comparison - return self._radius == 1.0 - - -# --------------------------------------------------------------------------- -# __init__ and __new__: always return None / cls (redundant) -# --------------------------------------------------------------------------- - - -class Widget: - def __init__(self) -> None: # W0050 — __init__ always returns None - self.value = 0 - - def __repr__(self) -> str: # W0050 — inferrable from return f"..." - return f"Widget({self.value})" - - def __str__(self) -> str: # W0050 — inferrable from return "..." - return "widget" - - def __len__(self) -> int: # W0050 — inferrable from return - return self.value - - def __bool__(self) -> bool: # W0050 — inferrable from return True/False - return self.value > 0 - - -# --------------------------------------------------------------------------- -# Staticmethod and classmethod -# --------------------------------------------------------------------------- - - -class Factory: - @staticmethod - def create_default() -> int: # W0050 — inferrable from `return 0` - return 0 - - @classmethod - def from_string( - cls, text: str - ) -> "Factory": # NO W0050 — cls return is not inferrable - return cls() - - -# --------------------------------------------------------------------------- -# Nested functions: same rules apply -# --------------------------------------------------------------------------- - - -def outer() -> None: - def inner_redundant() -> int: # W0050 — inferrable - return 99 - - def inner_needed() -> list[int]: # NO W0050 — collection type adds info - return [1, 2, 3] - - x: int = inner_redundant() # W0050 — return type known to be int - y = inner_needed() - _ = (x, y) - - -# --------------------------------------------------------------------------- -# Walrus operator (:=): annotated target -# --------------------------------------------------------------------------- - - -def walrus_examples() -> None: - if (n := 10) > 5: # NO W0050 — walrus can't carry annotation - _ = n - - -# --------------------------------------------------------------------------- -# Type alias assignments: NOT redundant (these define types, not values) -# --------------------------------------------------------------------------- - -from typing import TypeAlias - -Vector: TypeAlias = list[float] # NO W0050 — type alias definition -Matrix: TypeAlias = list[list[float]] # NO W0050 — type alias definition - - -# --------------------------------------------------------------------------- -# Annotated but no initializer (declaration-only): NOT redundant -# --------------------------------------------------------------------------- - - -class DeclarationOnly: - name: str # NO W0050 — no value, annotation is the declaration - age: int # NO W0050 — no value, annotation is the declaration - - -# --------------------------------------------------------------------------- -# Augmented assignment: annotation on first use, then augmented -# --------------------------------------------------------------------------- - - -def augmented_assign() -> None: - total: int = 0 # W0050 — obviously int - total += 10 - _ = total - - -# --------------------------------------------------------------------------- -# Global/nonlocal: annotation at module level, used in function -# --------------------------------------------------------------------------- - -_counter: int = 0 # W0050 — obviously int - - -def increment() -> None: - global _counter - _counter += 1 - - -# --------------------------------------------------------------------------- -# Dataclass-style: fields with explicit types -# --------------------------------------------------------------------------- - -from dataclasses import dataclass - - -@dataclass -class Point: - x: float # NO W0050 — dataclass field, annotation required - y: float # NO W0050 — dataclass field, annotation required - - -@dataclass -class LabeledPoint: - x: float # NO W0050 — dataclass field, annotation required - y: float # NO W0050 — dataclass field, annotation required - label: str = "origin" # NO W0050 — dataclass field, annotation required for default - - -# --------------------------------------------------------------------------- -# NamedTuple: annotations are part of the structure definition -# --------------------------------------------------------------------------- - -from typing import NamedTuple - - -class Coordinate(NamedTuple): - x: float # NO W0050 — NamedTuple field, annotation required - y: float # NO W0050 — NamedTuple field, annotation required - label: str = "point" # NO W0050 — NamedTuple field, annotation required - - -# --------------------------------------------------------------------------- -# TypedDict: annotations are the definition -# --------------------------------------------------------------------------- - -from typing import TypedDict - - -class UserDict(TypedDict): - name: str # NO W0050 — TypedDict field, annotation IS the definition - age: int # NO W0050 — TypedDict field, annotation IS the definition - - -# --------------------------------------------------------------------------- -# Constructor calls: annotation redundant when type matches constructor -# --------------------------------------------------------------------------- - - -def constructor_annotations() -> None: - x: int = int(42) # W0050 — int() returns int - y: str = str("hello") # W0050 — str() returns str - z: float = float(1.0) # W0050 — float() returns float - b: bool = bool(True) # W0050 — bool() returns bool - r: bytes = bytes(b"") # W0050 — bytes() returns bytes - lst: list = list() # W0050 — list() returns list - dct: dict = dict() # W0050 — dict() returns dict - st: set = set() # W0050 — set() returns set - _ = (x, y, z, b, r, lst, dct, st) - - -def constructor_non_redundant() -> None: - items: list[int] = list() # NO W0050 — parameterized type adds info - mapping: dict[str, int] = dict() # NO W0050 — parameterized type adds info - _ = (items, mapping) - - -# --------------------------------------------------------------------------- -# Cast and assertion patterns -# --------------------------------------------------------------------------- - -from typing import cast - - -def cast_patterns() -> None: - x: int = cast(int, some_value()) # NO W0050 — cast is explicit intent - _ = x - - -def some_value() -> object: - return 42 - - -# --------------------------------------------------------------------------- -# Multiple assignment targets -# --------------------------------------------------------------------------- - - -def multi_assign() -> None: - b = 10 - a: int = b # W0050 — b is already int - _ = (a, b) - - -# --------------------------------------------------------------------------- -# String literal types (forward references): NOT redundant -# --------------------------------------------------------------------------- - - -class Node: - def next(self) -> "Node": # NO W0050 — forward reference, not inferrable - return Node() - - -# --------------------------------------------------------------------------- -# Union types: NOT redundant -# --------------------------------------------------------------------------- - -from typing import Union, Optional - -maybe_int: Optional[int] = None # NO W0050 — Optional adds info beyond None -either: Union[int, str] = 42 # NO W0050 — Union adds info beyond int - - -# --------------------------------------------------------------------------- -# Final: annotation may be redundant but Final qualifier is not -# --------------------------------------------------------------------------- - -from typing import Final - -MAX_SIZE: Final[int] = 100 # W0050 — int is redundant (Final alone suffices) -MAX_NAME: Final = "limit" # NO W0050 — no redundant type, just Final - - -# --------------------------------------------------------------------------- -# Callable annotations -# --------------------------------------------------------------------------- - -from typing import Callable - - -def apply_func( - func: Callable[[int], int], value: int -) -> int: # NO W0050 — Callable needed - return func(value) - - -# --------------------------------------------------------------------------- -# Async functions: same rules apply -# --------------------------------------------------------------------------- - -import asyncio - - -async def async_redundant() -> int: # W0050 — inferrable from `return 42` - return 42 - - -async def async_needed() -> list[int]: # NO W0050 — collection type adds info - return [1, 2, 3] - - -async def async_none() -> None: # W0050 — async with no return implies None - await asyncio.sleep(0) - - -# --------------------------------------------------------------------------- -# Generator annotations -# --------------------------------------------------------------------------- - -from typing import Generator, Iterator - - -def gen_needed() -> Generator[int, None, None]: # NO W0050 — Generator type adds info - yield 1 - yield 2 - - -def iter_needed() -> Iterator[str]: # NO W0050 — Iterator type adds info - yield "a" - yield "b" - - -# --------------------------------------------------------------------------- -# Context managers -# --------------------------------------------------------------------------- - -from contextlib import contextmanager - - -@contextmanager -def managed_resource() -> Generator[ - str, None, None -]: # NO W0050 — Generator type needed - yield "resource" - - -# --------------------------------------------------------------------------- -# Overloaded functions: annotations are required -# --------------------------------------------------------------------------- - -from typing import overload - - -@overload -def process(x: int) -> int: # NO W0050 — overload signatures required - ... -@overload -def process(x: str) -> str: # NO W0050 — overload signatures required - ... -def process(x: int | str) -> int | str: - return x - - -# --------------------------------------------------------------------------- -# Protocol: annotations define the interface -# --------------------------------------------------------------------------- - -from typing import Protocol - - -class Drawable(Protocol): - def draw(self) -> None: # NO W0050 — Protocol method signature - ... - - x: int # NO W0050 — Protocol attribute declaration - - -# --------------------------------------------------------------------------- -# Abstract methods: annotations define the contract -# --------------------------------------------------------------------------- - -from abc import ABC, abstractmethod - - -class Shape(ABC): - @abstractmethod - def area(self) -> float: # NO W0050 — abstract method contract - ... - - @abstractmethod - def perimeter(self) -> float: # NO W0050 — abstract method contract - ... diff --git a/examples/tests/__init__.py b/examples/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/tests/test_data_pipeline.py b/examples/tests/test_data_pipeline.py deleted file mode 100644 index 3b959e3c6..000000000 --- a/examples/tests/test_data_pipeline.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Tests for examples/data_pipeline.py — ETL data pipeline.""" - -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - -from data_pipeline import ( - BaseWriter, - Column, - ParquetWriter, - PartitionKey, - coerce_field, - detect_encoding, - read_source, -) - - -class TestCoerceField: - def test_str_to_int(self) -> None: - assert coerce_field("42", int) == 42 - - def test_int_to_str_renamed(self) -> None: - assert coerce_field(42, str) == "42" - - def test_str_to_float(self) -> None: - assert coerce_field("3.14", float) == 3.14 - - -class TestDetectEncoding: - def test_utf8_bom(self) -> None: - assert detect_encoding(b"\xef\xbb\xbfhello") == "utf-8-sig" - - def test_utf16_le_bom(self) -> None: - assert detect_encoding(b"\xff\xfehello") == "utf-16" - - def test_utf16_be_bom(self) -> None: - assert detect_encoding(b"\xfe\xffhello") == "utf-16" - - -class TestReadSource: - def test_returns_empty_list(self) -> None: - result = read_source("nonexistent.csv") - assert result == [] - - -class TestColumnHierarchy: - def test_column_fields(self) -> None: - col = Column() - col.name = "id" - col.dtype = "int" - col.nullable = False - assert col.name == "id" - - def test_partition_key_inherits(self) -> None: - assert issubclass(PartitionKey, Column) - - def test_partition_key_nullable_default(self) -> None: - pk = PartitionKey() - assert pk.nullable == 0 - - -class TestWriterHierarchy: - def test_base_writer_flush(self) -> None: - writer = BaseWriter() - assert writer.flush([b"a", b"b"]) == 2 - - def test_parquet_writer_flush(self) -> None: - writer = ParquetWriter() - assert writer.flush([b"a", b"b"]) == 4 - - def test_parquet_is_base(self) -> None: - assert issubclass(ParquetWriter, BaseWriter) diff --git a/examples/tests/test_finance.py b/examples/tests/test_finance.py deleted file mode 100644 index d2ac74616..000000000 --- a/examples/tests/test_finance.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Tests for examples/finance.py — financial calculations.""" - -import math -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - -from finance import ( - BaseSettlement, - T2Settlement, - apply_slippage, - black_scholes, - kelly_criterion, - present_value, - round_to_tick, -) - - -class TestBlackScholes: - def test_returns_float(self) -> None: - result = black_scholes(100, 100, 1.0, 0.05, 0.2) - assert isinstance(result, float) - - def test_atm_option_positive(self) -> None: - result = black_scholes(100, 100, 1.0, 0.05, 0.2) - assert result > 0 - - def test_deep_itm_approaches_intrinsic(self) -> None: - result = black_scholes(200, 100, 0.01, 0.05, 0.2) - assert result > 90 - - -class TestPresentValue: - def test_single_cash_flow(self) -> None: - pv = present_value([100.0], 0.1) - assert pv == 100.0 - - def test_discounting_reduces_value(self) -> None: - pv = present_value([0, 100.0], 0.1) - assert pv < 100.0 - - def test_zero_discount_rate(self) -> None: - pv = present_value([10.0, 20.0, 30.0], 0.0) - assert math.isclose(pv, 60.0) - - -class TestKellyCriterion: - def test_fair_coin_positive_edge(self) -> None: - fraction = kelly_criterion(0.6, 1.0, 1.0) - assert fraction > 0 - - def test_losing_bet_negative(self) -> None: - fraction = kelly_criterion(0.3, 1.0, 1.0) - assert fraction < 0 - - def test_certain_win(self) -> None: - fraction = kelly_criterion(1.0, 1.0, 1.0) - assert math.isclose(fraction, 1.0) - - -class TestRoundToTick: - def test_exact_multiple(self) -> None: - assert round_to_tick(100.0, 5) == 100.0 - - def test_rounds_to_nearest(self) -> None: - result = round_to_tick(103.0, 5) - assert result == 105.0 - - -class TestApplySlippage: - def test_buy_increases_price(self) -> None: - result = apply_slippage("buy", 100.0, 10.0) - assert result is not None - assert result > 100.0 - - def test_sell_decreases_price(self) -> None: - result = apply_slippage("sell", 100.0, 10.0) - assert result is not None - assert result < 100.0 - - def test_unknown_side_returns_none(self) -> None: - result = apply_slippage("short", 100.0, 10.0) - assert result is None - - -class TestSettlement: - def test_base_settlement(self) -> None: - s = BaseSettlement() - assert s.settle(1000.0, "USD") == "1000.0 USD" - - def test_t2_settlement(self) -> None: - s = T2Settlement() - assert s.settle(1000.0, "USD") == "T+2: 1000.0 USD" - - def test_t2_is_base(self) -> None: - assert issubclass(T2Settlement, BaseSettlement) diff --git a/examples/tests/test_good.py b/examples/tests/test_good.py deleted file mode 100644 index 2f3c42334..000000000 --- a/examples/tests/test_good.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Tests for examples/good.py — fully typed code.""" - -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - -from good import User, add, greet - - -def test_greet_returns_greeting() -> None: - assert greet("Alice") == "Hello Alice" - - -def test_greet_empty_name() -> None: - assert greet("") == "Hello " - - -def test_add_positive_numbers() -> None: - assert add(2, 3) == 5 - - -def test_add_negative_numbers() -> None: - assert add(-1, -4) == -5 - - -def test_add_zero() -> None: - assert add(0, 0) == 0 - - -class TestUser: - def test_init(self) -> None: - user = User("Bob", 30) - assert user.name == "Bob" - assert user.age == 30 - - def test_birthday_increments_age(self) -> None: - user = User("Carol", 25) - user.birthday() - assert user.age == 26 - - def test_multiple_birthdays(self) -> None: - user = User("Dave", 40) - for _ in range(5): - user.birthday() - assert user.age == 45 diff --git a/examples/weird_violations.py b/examples/weird_violations.py deleted file mode 100644 index e40cc8441..000000000 --- a/examples/weird_violations.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -Weird and subtle violations — the cases that fool other type checkers. - -These are not contrived: every pattern appears in real codebases. -Basilisk catches all of them. - -Run: basilisk check examples/weird_violations.py -""" - -from __future__ import annotations - -from typing import Any, overload - - -# ── E0003: empty dict hiding inside a function default ─────────────────────── -# (Basilisk checks module-level assignments) -_cache = {} # BSK-0003: type of values unknown - - -# ── E0014: bool is a subtype of int in Python, but Basilisk still flags -# assigning a bool literal to a float field ──────────────────────────────── -ratio: float = True # assignment_compatibility: bool literal, not float - - -# ── E0014: negative int literal assigned to a str field ────────────────────── -sentinel: str = -1 # assignment_compatibility: int, not str - - -# ── E0017: attribute type flipped from mutable to immutable in child ───────── -class Config: - values: list[str] - max_size: int - - -class FrozenConfig(Config): - values: tuple[str, ...] = () # classes_override_2: tuple overrides list - max_size: str = "unlimited" # classes_override_2: str overrides int - - -# ── E0018: name used before assignment even though it looks like a constant ─── -def describe_algorithm() -> str: - return f"Using {ALGO_NAME} with seed {ALGO_SEED}" # names_undefined: ALGO_NAME not yet defined - - -ALGO_NAME: str = "DBSCAN" -ALGO_SEED: int = 42 - - -# ── E0019: exactly-one-path binding — the 'elif' still leaves a gap ────────── -def pick_strategy(score: float, mode: str) -> str: - if score > 0.9: - strategy = "aggressive" - elif mode == "safe": - strategy = "conservative" - # no else — if score <= 0.9 and mode != "safe", strategy is unbound - return strategy # names_unbound: strategy may be unbound - - -# ── E0019: augmented assignment in a try block ─────────────────────────────── -def sum_with_retry(values: list[int], retries: int) -> int: - if retries > 0: - result = 0 - for v in values: - result += v - return result # names_unbound: result may be unbound - - -# ── E0021: unannotated overload params look identical to the checker ───────── -# (differs only in return type — unannotated param means both have same signature) -@overload -def load(path) -> bytes: ... # BSK-0001: path untyped - - -@overload -def load(path) -> str: ... # BSK-0001 + overloads_consistency: duplicate - - -def load(path: str) -> bytes | str: - with open(path, "rb") as fh: - return fh.read() - - -# ── E0021: unannotated + Any together — Any is explicit, param is bare ──────── -@overload -def wrap(value) -> list[Any]: ... # BSK-0001: value untyped - - -@overload -def wrap(value) -> list[Any]: ... # BSK-0001 + overloads_consistency: duplicate - - -def wrap(value: Any) -> list[Any]: # returns_compatibility: Any without justification - return [value] - - -# ── E0022: list literal as dict key in a local dict ───────────────────────── -def make_tag_index() -> dict[list[str], float]: - return {["tag_a", "tag_b"]: 1.0} # dict_key_hashable: list literal as key - - -# ── E0023: match on an int with only two arms (0 and 1) ───────────────────── -def bool_from_db(raw: int) -> str: - match raw: - case 0: - return "false" - case 1: - return "true" - # match_exhaustiveness: 2, -1, 99 etc. fall through silently - - -# ── E0025: override buried inside a mixin chain ────────────────────────────── -class Serializable: - def to_json(self) -> str: - return "{}" - - -class Timestamped: - def to_json( - self, - ) -> str: # BSK-0025: no @override (inherits from Serializable via MRO) - return '{"ts": 0}' - - -# ── Combination: untyped + Any return + unhashable key ─────────────────────── -def batch_lookup(keys, db): # BSK-0001: keys, db untyped - results = {} - for key in keys: - results[key] = db.get(key) - return results # BSK-0002: no return type diff --git a/pyproject.toml b/pyproject.toml index a046221e0..83577634d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ build-backend = "maturin" [project] name = "basilisk-python" -description = "Basilisk — an open-source Python type checker and language server built in Rust: diagnostics, refactoring, formatting, integrated debugging, and profiling. Strictness is configured per rule, so a codebase can adopt type safety incrementally." +description = "Basilisk is unlisted. Its type checker produced incorrect results, and the product is being rebuilt from scratch." readme = "README-pypi.md" requires-python = ">=3.8" license = "Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND CDDL-1.0 AND CDLA-Permissive-2.0 AND ISC AND MIT AND MPL-2.0 AND Unicode-3.0 AND Unicode-DFS-2016 AND Zlib" @@ -33,9 +33,9 @@ license = "Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND CDDL-1.0 AND CDLA-Pe license-files = ["LICENSE", "NOTICES", "THIRD-PARTY-LICENSES", "RUST-DEPENDENCY-LICENSES"] authors = [{ name = "Nimblesite", email = "cftools@nimblesite.co" }] dynamic = ["version"] -keywords = ["type-checker", "typing", "static-analysis", "lsp", "pep", "python"] +keywords = ["basilisk"] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 7 - Inactive", "Environment :: Console", "Intended Audience :: Developers", "Programming Language :: Python", @@ -81,8 +81,3 @@ BSK-0050 = "warning" BSK-0061 = "warning" BSK-0062 = "warning" -# The examples grade those same rules down to `warning`; that lives in -# examples/pyproject.toml, because folder-scoped configuration is the -# nearest-deciding table on the ancestor walk ([CHKARCH-CONFIG-MODEL]) — there -# is no per-path override table, and a table this file does not define is -# simply never read. diff --git a/screenshots/blog-post-styled.png b/screenshots/blog-post-styled.png deleted file mode 100644 index 7b637f7436de49e4499dc3424cc7217eea7afa71..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 111696 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�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

- Basilisk -

- -

Basilisk

- -

English · 简体中文

- -

- An open-source Python type checker and language server, built in Rust.
- One extension for the whole workflow — diagnostics, autocomplete, refactoring, formatting, debugging, and profiling — driven by a single bundled binary. -

- -> **You are reading the Basilisk extension listing** for VS Code, Cursor, Windsurf, and every VS Code fork — the same extension is published to the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=Nimblesite.basilisk) and [Open VSX](https://open-vsx.org/extension/Nimblesite/basilisk). - -

- Website  •  - Install  •  - Quick Start  •  - Rules  •  - Refactoring  •  - GitHub -

- -

- Basilisk in action — type checking, diagnostics, and refactoring in the editor -

- -> ## ⚠️ Do not use Basilisk's type checker in your pipeline -> -> **The type checker still contains code that isn't doing real type checking, and -> it is not yet trustworthy.** Some rules decide from the way code is *spelled* -> rather than what it means, so they can be wrong in both directions — a false -> error on correct code, or silence where there is a genuine bug. Until the audit -> below is finished, don't gate CI on `basilisk check`, don't block a merge with -> it, and don't read a clean run as a clean codebase. -> -> The rest of Basilisk — language server, refactoring, formatting, debugging, -> profiling — does not depend on those rules and is unaffected. - -## Restoring trust: audit, delete, and lean on a checker that works - -We withdrew our former conformance claim and our benchmark figures, and asked to be -[removed from the official `python/typing` results](https://github.com/python/typing/blob/main/conformance/results/results.html). -The cause was checker logic fitted to the contents of conformance test files -instead of implementing the typing specification generally: rules that matched -the *spelling* of code rather than its meaning. Rename an import or reformat a -file and the answer changed. A score produced that way is not evidence. - -**This was a mistake and a failure to verify.** Our process treated the score as -the goal, matching text raises a score faster than real analysis does, and we -published without ever asking whether a rule still held when the same program was -spelled differently. Basilisk's author has published a -[personal account and apology](https://www.christianfindlay.com/blog/basilisk-conformance-apology). - -**So we are auditing every rule and deleting the ones that don't do real type -checking.** Not rewriting them, not patching them, not marking them TODO — -deleting them, with a failing test left behind so the gap is visible instead of -hidden. A rule stays only if it decides from the resolved syntax tree and gives -the same answer when the code is spelled differently. - -**Where a rule can't be made reliable in a straightforward way, we will depend on -a different, established type checker rather than ship our own unreliable version -of it.** An answer from an engine that has earned trust is worth more to you than -a Basilisk-branded one that hasn't. No replacement figure gets published until it -survives off-suite and mutation testing. - -That means Basilisk gets **smaller** before it gets better. Expect fewer rules, -fewer diagnostics, and a lower conformance number. We will report each drop -rather than avoid it. What is left will be code that is honest about what it -does — nothing else. - -### Basilisk is much more than a type checker - -Type checking is one part of it. The rest is a complete Python workflow in a -single Rust binary — language server, refactoring, formatting, integrated -debugging, profiling, and the editor extensions — and none of it rests on the -rules under audit. That is what we are sharpening while the audit runs: make the -parts that are genuinely useful solid, and remove anything that could hand you a -misleading result. The point of getting smaller is to end up with a tool you can -believe. - -[Read the full correction →](https://www.basilisk-python.dev/docs/conformance/)  •  -[Integrity audit →](https://github.com/Nimblesite/Basilisk/blob/main/docs/CONFORMANCE-INTEGRITY-AUDIT.md) - -## What you get - -One extension covers the whole Python workflow. A single bundled Rust binary -drives it — no Node.js, no npm, no `pip install`: - -- **Diagnostics as you type** — incremental analysis powered by [Salsa](https://github.com/salsa-rs/salsa) -- **Autocomplete, hover, go-to-definition, find references, rename** -- **Refactoring code actions** — extract, inline, move symbol, organize imports -- **Integrated debugging** — F5 to debug via bundled [debugpy](https://github.com/microsoft/debugpy); no separate extension -- **Integrated profiling** — CPU heat map, flame graph, and a memory dashboard with leak detection -- **Activity panel** — module tree with per-module type-health coverage, plus feature toggles -- **Inlay hints** and **Ruff** formatting/import-organization, built in -- **Standard-library types from [typeshed](https://github.com/python/typeshed)** — a complete `stdlib/` snapshot is compiled into the binary, so hover and diagnostics work offline with no configuration - -Strictness is configured **per rule**, never by a mode: the unconfigured default -enables the typing-spec rule set, and each rule can be graded down to -`warning`/`info` so a codebase can adopt type safety incrementally. Every -diagnostic carries a `help`, a `note`, and a link to a per-rule explainer, so a -red squiggle tells you *why*. - -## Install - -**Editor extension** — install *Basilisk* from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=Nimblesite.basilisk) or [Open VSX](https://open-vsx.org/extension/Nimblesite/basilisk) (Cursor, Windsurf, and other forks read Open VSX). The Basilisk binary is bundled for macOS (Apple Silicon), Linux (x86_64, aarch64), and Windows (x86_64, aarch64) — nothing else to install. Zed and Neovim 0.10+ extensions are available too. - -**CLI** — on [PyPI as `basilisk-python`](https://pypi.org/project/basilisk-python/); the installed command is `basilisk`: - -```sh -uv tool install basilisk-python # or: pipx install basilisk-python, pip install basilisk-python -``` - -Also via Homebrew (`brew install Nimblesite/tap/basilisk`), Scoop (`scoop bucket add nimblesite https://github.com/Nimblesite/scoop-bucket && scoop install basilisk`), and [GitHub Releases](https://github.com/Nimblesite/Basilisk/releases). Every channel ships the same single Rust CLI, built from this repository at the same version, with no runtime dependencies. Point `basilisk.executablePath` at your own build to have the extension use it. Full options: [install guide](https://www.basilisk-python.dev/docs/installation/). - -## Try it - -The [`examples/`](https://github.com/Nimblesite/Basilisk/blob/main/examples/) folder has ready-to-go Python files: - -```sh -basilisk check examples/bad.py # 8 typing-spec errors — always on, no config needed -basilisk analyze examples/bad.py # the opt-in strictness warnings on the same file -basilisk analyze examples/good.py # clean, even at full strictness -basilisk check examples/mixed.py # one real type error -basilisk check examples/ # the whole folder at once -``` - -Machine-readable output for CI and tooling: - -```sh -basilisk check path/to/your_code.py --output json --color never -``` - -The two commands read one rule universe split by provenance ([`CHKARCH-COMMANDS`](https://github.com/Nimblesite/Basilisk/blob/main/docs/specs/CHECKER-ARCHITECTURE-SPEC.md)): `check` reports -the `pep`-tagged typing-spec rules and nothing else — that set is always on, and -while a config table may grade one of them down to `warning`/`info`, none may -switch it off. `analyze` reports the non-`pep` house rules, which stay silent -until a table selects them. Only `analyze` emits `BSK-` diagnostics. - -## Standard-library types, always offline - -Basilisk resolves the standard library from [typeshed](https://github.com/python/typeshed), -and checking **never downloads anything**. Out of the box it uses the complete -typeshed `stdlib/` snapshot compiled into the binary, reporting the source as -unpinned — so stdlib types work on a plane, behind a firewall, or in an -air-gapped CI runner, with no configuration. - -Pin an exact commit with `typeshed-commit = "<40-char sha>"` under -`[tool.basilisk]`. A pin does exactly one thing: it verifies, offline, that the -typeshed tree in the local store hashes to that commit. If the commit is not on -this machine the run fails hard with `NO SOURCE` rather than substituting -another source — bring it down first with `basilisk typeshed download` (with no -`--commit` it downloads the latest and writes the pin for you), or use the -editor's **Download latest** button. Alternatively, point `typeshed-path` at -your own typeshed tree. Full options: -[configuration guide](https://www.basilisk-python.dev/docs/configuration/). +# Basilisk is unlisted -## Development +> **You are reading the Basilisk extension listing** for VS Code, Cursor, Windsurf, and every VS Code fork. -```sh -cargo build # build all crates -cargo test # run all tests -cargo clippy # lint (zero warnings policy) -cargo fmt # format -``` +**Basilisk's type checker was producing incorrect results.** Rules decided from the way code was *spelled* rather than what it meant, so they could be wrong in both directions — a false error on correct code, or silence on a real bug. -Rust 1.87+ required. +**We asked for Basilisk to be removed from the `python/typing` conformance results, and it has been removed.** The score it held was not evidence of anything. -## Contributing +**We cannot tell you how much of the checker this affects.** The code responsible is not isolated to a known set of rules. We will not estimate. That uncertainty is the reason for everything below. -Basilisk is built by a human + AI partnership, with the work split on purpose. See -[CONTRIBUTING.md](https://github.com/Nimblesite/Basilisk/blob/main/CONTRIBUTING.md) — **For Humans** (testing, code-quality review, -conformance/security audits, IDE feature parity, sharpening the AI instructions) and -**For AI** (the technical execution, under the standing rules in [CLAUDE.md](https://github.com/Nimblesite/Basilisk/blob/main/CLAUDE.md)). +**A code-quality tool that does not produce correct results is worse than useless.** Basilisk is being delisted everywhere it was published — VS Code Marketplace, Open VSX, PyPI, Homebrew, Scoop — and the type checker is being made inert. Remove it from your pipeline; it checks nothing, and it fails rather than reporting a clean run. -## Acknowledgments +**We are not fixing Basilisk's type checker code. We are rebuilding Basilisk.** What comes next is a new product, built from the ground up, shipping only what can be shown to be trustworthy. That most likely will not include type checking. Nothing is relisted until it has been rebuilt from components we can vouch for. If type checking ever returns, it will come from established third-party engines, or code audited by a third party. + +Basilisk's author has published a full public account: [an apology](https://www.christianfindlay.com/blog/basilisk-conformance-apology). + +## What to do now -Basilisk builds on the open-source community — with thanks to: +**Remove Basilisk from your pipeline, your pre-commit hooks, and your editor.** Uninstall the CLI and the extension. -- **[Astral](https://astral.sh/)** — [Ruff](https://github.com/astral-sh/ruff), whose parser, AST, and formatter crates Basilisk embeds (MIT). The foundation we rely on most. -- **[typeshed](https://github.com/python/typeshed)** — standard-library type stubs (Apache-2.0, with MIT-licensed parts). -- **[Salsa](https://github.com/salsa-rs/salsa)** — incremental query engine. -- **[Rayon](https://github.com/rayon-rs/rayon)** — data parallelism. -- **[tower-lsp](https://github.com/ebkalderon/tower-lsp)** — LSP scaffolding. -- **[debugpy](https://github.com/microsoft/debugpy)** — debug adapter (bundled in the VS Code extension). -- The [`python/typing`](https://github.com/python/typing) conformance suite. +The type checker is being made inert: it checks nothing, and it exits non-zero so a build that still calls it fails loudly rather than reporting a clean run. Do not treat that failure as a finding about your code. -Full component list, selected licenses, and required notices: [NOTICES](https://github.com/Nimblesite/Basilisk/blob/main/NOTICES) -and [RUST-DEPENDENCY-LICENSES](https://github.com/Nimblesite/Basilisk/blob/main/RUST-DEPENDENCY-LICENSES). Each published -artifact carries its own copies: the VSIX ships Rust notices in -`RUST-DEPENDENCY-LICENSES`, npm notices in `VSCODE-DEPENDENCY-LICENSES`, and -debugpy's license and `ThirdPartyNotices.txt` inside `bundled/debugpy`; the -wheel carries the complete locked notices in its `.dist-info/licenses/` -directory. +**Treat every result Basilisk gave you as unverified.** A clean run was never evidence that your code was clean, and an error it reported may never have been real. + +Every distribution channel is being delisted. Nothing will be relisted until it has been rebuilt from components we can vouch for. + +## Acknowledgments ---- +Basilisk is built on [Ruff](https://github.com/astral-sh/ruff) by [Astral](https://astral.sh/), whose parser, AST, and formatter crates it embeds (MIT), and on standard-library type stubs from [typeshed](https://github.com/python/typeshed) (Apache-2.0, with MIT-licensed parts). Neither project is responsible for how Basilisk used them. Full component list and required notices: [NOTICES](https://github.com/Nimblesite/Basilisk/blob/main/NOTICES) and [RUST-DEPENDENCY-LICENSES](https://github.com/Nimblesite/Basilisk/blob/main/RUST-DEPENDENCY-LICENSES). ## License -Basilisk source code is MIT licensed. Binary distributions also contain -third-party components under the licenses shipped beside each artifact. +Basilisk source code is MIT licensed. Binary distributions also contain third-party components under the licenses shipped beside each artifact. Built by [NIMBLESITE PTY LTD](https://www.nimblesite.co). diff --git a/vscode-extension/README.zh.md b/vscode-extension/README.zh.md deleted file mode 100644 index 7f115e815..000000000 --- a/vscode-extension/README.zh.md +++ /dev/null @@ -1,193 +0,0 @@ - -

- Basilisk -

- -

Basilisk

- -

English · 简体中文

- -

- 用 Rust 打造的开源 Python 类型检查器与语言服务器。
- 一个扩展覆盖整套工作流 —— 诊断、自动补全、重构、格式化、调试与性能分析 —— 全部由单一捆绑的二进制文件驱动。 -

- -> **你正在阅读 Basilisk 的扩展页面**,适用于 VS Code、Cursor、Windsurf 以及所有 VS Code 分支 —— 同一个扩展同时发布到 [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=Nimblesite.basilisk) 与 [Open VSX](https://open-vsx.org/extension/Nimblesite/basilisk)。 - -

- 网站  •  - 安装  •  - 快速上手  •  - 规则  •  - 重构  •  - GitHub -

- -

- Basilisk 实战 —— 编辑器中的类型检查、诊断与重构 -

- -> ## ⚠️ 请勿在流水线中使用 Basilisk 的类型检查器 -> -> **类型检查器中仍然存在没有做真正类型检查的代码,它目前还不值得信任。** 有些规则 -> 依据的是代码的**写法**而不是含义,因此两个方向上都可能出错 —— 既可能对正确的代码 -> 报出虚假错误,也可能对真实的缺陷保持沉默。在下文所述的审计完成之前,请不要用 -> `basilisk check` 作为 CI 的门禁,不要用它拦截合并,也不要把一次干净的运行结果当作 -> 代码库是干净的。 -> -> Basilisk 的其余部分 —— 语言服务器、重构、格式化、调试、性能分析 —— 并不依赖这些 -> 规则,因此不受影响。 - -## 重建信任:审计、删除,并倚重真正可靠的检查器 - -我们撤回了此前的一致性宣称与基准测试数字,并主动请求 -[从官方 `python/typing` 结果中移除](https://github.com/python/typing/blob/main/conformance/results/results.html)。 -原因是检查器中存在针对一致性测试文件内容而写的逻辑,而不是对类型规范的通用实现: -那些规则匹配的是代码的**写法**,而不是代码的含义。改一个导入别名或重新格式化文件, -结论就会变。这样得出的分数并不能作为证据。 - -**这是一个错误、一次验证上的失职。** 我们的流程把分数当成了目标,而匹配文本比真正做 -分析更快地提高分数;我们在发布之前,始终没有问过这样一个问题 —— 同一个程序换一种 -写法时,这条规则是否依然成立。Basilisk 作者已发表 -[个人说明与致歉](https://www.christianfindlay.com/blog/basilisk-conformance-apology)。 - -**因此,我们正在逐条审计规则,并删除那些没有做真正类型检查的规则。** 不是重写,不是 -打补丁,也不是标一个 TODO —— 是删除,并留下一个失败的测试,让这个缺口可见而不是被 -掩盖。一条规则只有在依据已解析的语法树做判断、并且在代码换一种写法时给出相同结论的 -情况下,才会保留。 - -**如果一条规则无法以直截了当的方式做到可靠,我们会转而依赖另一个成熟的类型检查器, -而不是端出我们自己那份不可靠的实现。** 一个已经赢得信任的引擎给出的答案,对你而言 -比一个挂着 Basilisk 名号却没有赢得信任的答案更有价值。在通过套件之外的用例与变异 -测试之前,我们不会发布任何替代数字。 - -这意味着 Basilisk 会**先变小,再变好**。规则会更少,诊断会更少,一致性数字也会更低。 -每一次下降我们都会如实报告,而不是设法回避。留下来的,将是对自己所做之事诚实的代码 -—— 仅此而已。 - -### Basilisk 远不只是一个类型检查器 - -类型检查只是其中一部分。其余部分是装在单个 Rust 二进制文件里的完整 Python 工作流 -—— 语言服务器、重构、格式化、集成调试、性能分析,以及各个编辑器扩展 —— 它们都不 -建立在正在接受审计的规则之上。这正是我们在审计期间着力打磨的地方:把真正有用的部分 -做扎实,并移除任何可能给出误导性结果的东西。变小的意义,是最终得到一个你可以信赖的 -工具。 - -[阅读完整更正 →](https://www.basilisk-python.dev/zh/docs/conformance/)  •  -[完整性审计 →](https://github.com/Nimblesite/Basilisk/blob/main/docs/CONFORMANCE-INTEGRITY-AUDIT.md) - -## 你能得到什么 - -一个扩展即可覆盖整套 Python 工作流。一切由单一捆绑的 Rust 二进制文件驱动 —— -无需 Node.js、无需 npm、无需 `pip install`: - -- **随输入实时诊断** —— 由 [Salsa](https://github.com/salsa-rs/salsa) 提供增量分析 -- **自动补全、悬停信息、跳转到定义、查找引用、重命名** -- **重构代码操作** —— 提取、内联、移动符号、整理导入 -- **集成调试** —— 按 F5 即可通过捆绑的 [debugpy](https://github.com/microsoft/debugpy) 调试;无需额外扩展 -- **集成性能分析** —— CPU 热力图、火焰图,以及带泄漏检测的内存面板 -- **活动面板** —— 模块树与逐模块的类型健康度覆盖率,并可切换功能开关 -- 内置 **Inlay hints** 与 **Ruff** 格式化/导入整理 -- **来自 [typeshed](https://github.com/python/typeshed) 的标准库类型** —— 完整的 `stdlib/` 快照已编译进二进制文件,因此悬停与诊断在离线且零配置的情况下依然可用 - -严格程度按**规则**配置,而不是靠模式切换:未配置的默认值即启用类型规范规则集, -每条规则都可以降级为 `warning`/`info`,让代码库能够渐进地采用类型安全。每条诊断 -都附带 `help`、`note` 以及指向每条规则详解页的链接,因此一条红色波浪线总能告诉你 -*为什么*。 - -## 安装 - -**编辑器扩展** —— 从 [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=Nimblesite.basilisk) 或 [Open VSX](https://open-vsx.org/extension/Nimblesite/basilisk) 安装 *Basilisk*(Cursor、Windsurf 等分支读取 Open VSX)。Basilisk 二进制文件已为 macOS(Apple Silicon)、Linux(x86_64、aarch64)与 Windows(x86_64、aarch64)捆绑 —— 无需再安装其他东西。Zed 与 Neovim 0.10+ 的扩展同样可用。 - -**命令行工具** —— 在 [PyPI 上名为 `basilisk-python`](https://pypi.org/project/basilisk-python/);安装后的命令是 `basilisk`: - -```sh -uv tool install basilisk-python # 或:pipx install basilisk-python、pip install basilisk-python -``` - -也可通过 Homebrew(`brew install Nimblesite/tap/basilisk`)、Scoop(`scoop bucket add nimblesite https://github.com/Nimblesite/scoop-bucket && scoop install basilisk`)与 [GitHub Releases](https://github.com/Nimblesite/Basilisk/releases) 获取。每个渠道都发布同一个 Rust 命令行工具,由本仓库在同一版本构建,且没有运行时依赖。把 `basilisk.executablePath` 指向你自己的构建,扩展就会使用它。完整选项:[安装指南](https://www.basilisk-python.dev/zh/docs/installation/)。 - -## 试一试 - -[`examples/`](https://github.com/Nimblesite/Basilisk/blob/main/examples/) 目录中有可直接运行的 Python 文件: - -```sh -basilisk check examples/bad.py # 8 处类型规范错误 —— 始终启用,无需配置 -basilisk analyze examples/bad.py # 同一文件上可选的严格性警告 -basilisk analyze examples/good.py # 即使在完全严格下也是干净的 -basilisk check examples/mixed.py # 一处真实的类型错误 -basilisk check examples/ # 一次检查整个目录 -``` - -供 CI 与工具使用的机器可读输出: - -```sh -basilisk check path/to/your_code.py --output json --color never -``` - -这两条命令读取的是按来源划分的同一套规则宇宙([`CHKARCH-COMMANDS`](https://github.com/Nimblesite/Basilisk/blob/main/docs/specs/CHECKER-ARCHITECTURE-SPEC.md)):`check` -只报告带 `pep` 标签的类型规范规则 —— 该集合始终启用,配置表虽可将其中某条 -降级为 `warning`/`info`,但都不能将其关闭。`analyze` 报告非 `pep` 的自有规则, -它们在被配置表选用之前始终保持沉默。只有 `analyze` 会输出 `BSK-` 诊断。 - -## 标准库类型:始终离线 - -Basilisk 从 [typeshed](https://github.com/python/typeshed) 解析标准库类型, -而且检查**从不下载任何东西**。开箱即用时它使用编译进二进制文件的完整 typeshed -`stdlib/` 快照,并将来源报告为未固定(unpinned)—— 因此在飞机上、防火墙后或 -隔离网络的 CI 中,标准库类型都无需配置即可使用。 - -在 `[tool.basilisk]` 中使用 `typeshed-commit = "<40 位 sha>"` 固定到某个确切提交。 -固定只做一件事:离线校验本地存储库中的 typeshed 树是否哈希为该提交。若该提交 -不在本机上,运行会以 `NO SOURCE` 硬失败,而不会替换为其他来源 —— 请先用 -`basilisk typeshed download` 取回(不带 `--commit` 时会下载最新提交并替你写入 -固定项),或使用编辑器中的 **Download latest** 按钮。或者,把 `typeshed-path` -指向你自己的 typeshed 目录树。完整选项参见[配置指南](https://www.basilisk-python.dev/zh/docs/configuration/)。 - -## 开发 - -```sh -cargo build # build all crates -cargo test # run all tests -cargo clippy # lint (zero warnings policy) -cargo fmt # format -``` - -需要 Rust 1.87+。 - -## 贡献 - -Basilisk 由人类与 AI 的协作打造,并有意地划分了各自的工作。请参阅 -[CONTRIBUTING.md](https://github.com/Nimblesite/Basilisk/blob/main/CONTRIBUTING.md) —— **For Humans**(测试、代码质量审查、 -一致性/安全审计、IDE 功能对等、打磨 AI 指令)以及 -**For AI**(在 [CLAUDE.md](https://github.com/Nimblesite/Basilisk/blob/main/CLAUDE.md) 既定规则下的技术执行)。 - -## 致谢 - -Basilisk 建立在开源社区之上 —— 特别感谢: - -- **[Astral](https://astral.sh/)** —— [Ruff](https://github.com/astral-sh/ruff),Basilisk 嵌入了其解析器、AST 与格式化器 crate(MIT)。我们最倚重的基础。 -- **[typeshed](https://github.com/python/typeshed)** —— 标准库类型存根(Apache-2.0,部分内容采用 MIT 许可证)。 -- **[Salsa](https://github.com/salsa-rs/salsa)** —— 增量查询引擎。 -- **[Rayon](https://github.com/rayon-rs/rayon)** —— 数据并行。 -- **[tower-lsp](https://github.com/ebkalderon/tower-lsp)** —— LSP 脚手架。 -- **[debugpy](https://github.com/microsoft/debugpy)** —— 调试适配器(捆绑于 VS Code 扩展)。 -- [`python/typing`](https://github.com/python/typing) 一致性测试套件。 - -完整的组件、所选许可证与必要声明见 [NOTICES](https://github.com/Nimblesite/Basilisk/blob/main/NOTICES) 和 -[RUST-DEPENDENCY-LICENSES](https://github.com/Nimblesite/Basilisk/blob/main/RUST-DEPENDENCY-LICENSES)。每个发布的产物也各自 -携带副本:VSIX 在 `RUST-DEPENDENCY-LICENSES` 中提供 Rust 声明,在 -`VSCODE-DEPENDENCY-LICENSES` 中提供 npm 声明,并在 `bundled/debugpy` 内保留 -debugpy 自身的许可证与 `ThirdPartyNotices.txt`;wheel 则在 `.dist-info/licenses/` -目录中携带完整的锁定声明。 - ---- - -## 许可证 - -Basilisk 源代码采用 MIT 许可证。二进制发行物还包含第三方组件;其许可证 -随每个发行物一并提供。 - -由 [NIMBLESITE PTY LTD](https://www.nimblesite.co) 构建。 diff --git a/vscode-extension/package.json b/vscode-extension/package.json index f530d6dd8..998f662d4 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -1,7 +1,7 @@ { "name": "basilisk", "displayName": "Basilisk", - "description": "An open-source Python type checker and language server built in Rust: diagnostics, autocomplete, go-to-definition, refactoring, formatting, integrated debugging, and profiling in one extension. Strictness is configured per rule, so a codebase can adopt type safety incrementally.", + "description": "Basilisk is unlisted. Its type checker produced incorrect results, and the product is being rebuilt from scratch.", "version": "0.0.0-PLACEHOLDER", "publisher": "Nimblesite", "license": "SEE LICENSE IN LICENSE.txt", @@ -22,23 +22,10 @@ "vscode": "^1.99.0" }, "categories": [ - "Programming Languages", - "Linters", - "Debuggers", "Other" ], "keywords": [ - "python", - "language server", - "pylance", - "pyright", - "type checker", - "intellisense", - "autocomplete", - "go to definition", - "rust", - "basilisk", - "lsp" + "basilisk" ], "activationEvents": [ "onLanguage:python", @@ -813,7 +800,10 @@ }, "basilisk.formatter": { "type": "string", - "enum": ["ruff", "none"], + "enum": [ + "ruff", + "none" + ], "default": "ruff", "description": "Formatter engine. 'ruff' uses the Ruff formatter embedded in the Basilisk binary (in-process — no external ruff binary is ever required or spawned); 'none' disables formatting. See LSP-FORMATTING-SPEC.md#LSPFMT-CONFIG." }, diff --git a/website/_hero_verify.mjs b/website/_hero_verify.mjs deleted file mode 100644 index 10bc481d1..000000000 --- a/website/_hero_verify.mjs +++ /dev/null @@ -1,49 +0,0 @@ -import { chromium } from 'playwright-core'; - -const chromePath = chromium.executablePath(); -const BASE = 'http://localhost:8199/'; -const OUT = '/private/tmp/claude-501/-Users-christianfindlay-Documents-Code-Basilisk/179e3680-02ec-4d6e-9c31-c7911d352e54/scratchpad'; - -const viewports = [ - { name: 'wide', width: 2560, height: 1440, dsf: 1 }, - { name: 'desktop', width: 1440, height: 900, dsf: 1 }, - { name: 'mobile', width: 390, height: 844, dsf: 2 }, -]; - -const browser = await chromium.launch({ executablePath: chromePath, headless: true }); -for (const vp of viewports) { - const page = await browser.newPage({ viewport: { width: vp.width, height: vp.height }, deviceScaleFactor: vp.dsf }); - await page.goto(BASE, { waitUntil: 'networkidle' }); - await page.waitForTimeout(250); - const m = await page.evaluate(() => { - const q = (s) => document.querySelector(s); - const r = (el) => { if (!el) return null; const b = el.getBoundingClientRect(); return { w: Math.round(b.width), h: Math.round(b.height), top: Math.round(b.top), left: Math.round(b.left), right: Math.round(b.right), bottom: Math.round(b.bottom) }; }; - const vw = window.innerWidth, vh = window.innerHeight; - const hero = q('.hero'); - const split = q('.hero__split'); - const img = q('.hero__shot'); - const sb = split ? split.getBoundingClientRect() : null; - return { - vw, vh, - heroH: hero ? Math.round(hero.getBoundingClientRect().height) : null, - split: r(split), - // side gutter = empty space from viewport edge to the content block - gutterLeft: sb ? Math.round(sb.left) : null, - gutterRight: sb ? Math.round(vw - sb.right) : null, - // vertical gap from hero top/bottom to the content block (within the hero) - gapTop: (split && hero) ? Math.round(sb.top - hero.getBoundingClientRect().top) : null, - gapBottom: (split && hero) ? Math.round(hero.getBoundingClientRect().bottom - sb.bottom) : null, - text: r(q('.hero__split > div:first-child')), - shot: r(img), - shotDisplayRatio: img ? (img.getBoundingClientRect().width / img.getBoundingClientRect().height).toFixed(3) : null, - shotNatRatio: img ? (img.naturalWidth / img.naturalHeight).toFixed(3) : null, - metaLines: (() => { const meta = q('.hero__meta'); if (!meta) return null; const tops = new Set([...meta.querySelectorAll('span')].map(s => Math.round(s.getBoundingClientRect().top))); return tops.size; })(), - }; - }); - console.log(`\n=== ${vp.name} ${vp.width}x${vp.height} ===`); - console.log(JSON.stringify(m)); - await page.screenshot({ path: `${OUT}/hero-${vp.name}.png`, fullPage: false }); - await page.close(); -} -await browser.close(); -console.log('\nDONE'); diff --git a/website/eleventy.config.js b/website/eleventy.config.js index 1ebba9045..19348e7d0 100644 --- a/website/eleventy.config.js +++ b/website/eleventy.config.js @@ -2,39 +2,26 @@ import { readFileSync, writeFileSync, existsSync } from "fs"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; import techdoc from "eleventy-plugin-techdoc"; -import markdownIt from "markdown-it"; -import markdownItAnchor from "markdown-it-anchor"; +import withdrawal from "./src/_data/withdrawal.json" with { type: "json" }; const __dirname = dirname(fileURLToPath(import.meta.url)); -// Patch techdoc templates and behavior with project-owned versions. The plugin -// reads these files after this config loads, so the copies survive a fresh npm -// install without maintaining a fork of the package. +// Patch techdoc templates with project-owned versions. The plugin reads these +// files after this config loads, so the copies survive a fresh npm install +// without maintaining a fork of the package. const templateOverrides = [ ["src/assets/js/mobile-menu.js", "assets/js/mobile-menu.js"], ["src/_includes/layouts/base.njk", "templates/layouts/base.njk"], - ["src/_includes/layouts/blog.njk", "templates/layouts/blog.njk"], - ["src/_includes/layouts/docs.njk", "templates/layouts/docs.njk"], - ["src/_includes/pages/feed.njk", "templates/pages/feed.njk"], ["src/_includes/pages/robots.txt.njk", "templates/pages/robots.txt.njk"], ["src/_includes/pages/sitemap.njk", "templates/pages/sitemap.njk"], - ["src/_includes/pages/blog/index.njk", "templates/pages/blog/index.njk"], - ["src/_includes/pages/blog/tags.njk", "templates/pages/blog/tags.njk"], - ["src/_includes/pages/blog/tags-pages.njk", "templates/pages/blog/tags-pages.njk"], - ["src/_includes/pages/blog/categories.njk", "templates/pages/blog/categories.njk"], - ["src/_includes/pages/blog/categories-pages.njk", "templates/pages/blog/categories-pages.njk"], + ["src/_includes/pages/llms.txt.njk", "templates/pages/llms.txt.njk"], + ["src/_includes/pages/feed.njk", "templates/pages/feed.njk"], ]; // The copy is LINE-ENDING NORMALIZED, and must stay that way. These overrides // are working-tree files, so on Windows — where git's `autocrlf` default checks -// them out CRLF — a verbatim copy hands the plugin CRLF template content. Every -// probe below (`patchIndexFrontMatter`'s `^title: .*$` replacements, -// `addSharedProseClass`'s literal `class="docs-content"`, and above all -// `addLocalizedTemplateLang`'s `lang: zh` guard) is written against LF, so on a -// CRLF copy the guard silently missed an existing `lang: zh` and inserted a -// SECOND one — "duplicated mapping key", and the whole site build died on -// Windows while Linux CI stayed green. Normalizing here makes the bytes the -// plugin sees identical on every platform, so one probe cannot pass on Linux and -// fail on Windows. On Linux this is a no-op: the files are already LF. +// them out CRLF — a verbatim copy hands the plugin CRLF template content, and +// every literal probe written against LF silently misses. Normalizing here makes +// the bytes the plugin sees identical on every platform. On Linux it is a no-op. const toLf = (text) => text.replace(/\r\n/g, "\n"); for (const [source, target] of templateOverrides) { @@ -49,193 +36,40 @@ for (const [source, target] of templateOverrides) { } } -// SEO metadata for the plugin-generated blog / tags / categories index pages. -// The techdoc plugin registers these as virtual templates (node_modules-only, -// no source file to edit) whose default front matter only sets a bare title -// ("Blog", "Tags", "Categories") and no description, so every index inherits the -// generic site description — non-unique and too short for SEO. We can't add a -// same-path source override (Eleventy errors when a file collides with a virtual -// template), and the plugin registers its templates AFTER this config callback -// returns, so the virtualTemplates map is empty here. Instead we wrap -// addTemplate() below and patch each index template's front matter as the plugin -// registers it — project-level, leaving node_modules untouched. -// [SEO index metadata override] -const indexSeo = { - "blog/index.njk": { - title: "Basilisk Blog — Python Type-Checking News & Releases", - description: - "News, releases, and deep dives from Basilisk — the open-source, strict-by-default Python language server in Rust for VS Code, Cursor, Zed, and Neovim.", - }, - "blog/tags.njk": { - title: "Blog Tags — Browse Basilisk Posts by Topic", - description: - "Browse Basilisk blog posts by tag to find writing on Python type checking, strict typing, LSP features, refactoring, debugging, profiling, and release notes.", - }, - "blog/categories.njk": { - title: "Blog Categories — Browse Basilisk Posts by Section", - description: - "Browse Basilisk blog posts by category to explore announcements, deep dives, and release notes for the strict-by-default Python language server built in Rust.", - }, - "zh/blog/index.njk": { - title: "Basilisk 博客 — Python 类型检查动态与版本发布", - description: - "来自 Basilisk 项目的动态、版本发布与深入解析——一个用 Rust 构建、严格优先的开源 Python 语言服务器,支持 VS Code、Cursor、Zed 与 Neovim。", - }, - "zh/blog/tags.njk": { - title: "博客标签 — 按主题浏览 Basilisk 文章", - description: - "按标签浏览 Basilisk 博客文章,查找有关 Python 类型检查、严格类型、LSP 功能、重构、调试、性能分析与版本发布说明的内容,按主题快速定位。", - }, - "zh/blog/categories.njk": { - title: "博客分类 — 按栏目浏览 Basilisk 文章", - description: - "按分类浏览 Basilisk 博客文章,探索这个用 Rust 构建、严格优先的开源 Python 语言服务器的公告、深入解析与版本发布说明等栏目内容。", - }, -}; - -function patchIndexFrontMatter(path, content) { - const meta = indexSeo[path]; - if (!meta) { - return content; - } - return content - .replace(/^title: .*$/m, `title: "${meta.title}"`) - .replace(/^(title: .*)$/m, `$1\ndescription: "${meta.description}"`); -} - -function addSharedProseClass(path, content) { - return path === "_includes/layouts/api.njk" - ? content.replace('class="docs-content"', 'class="docs-content prose"') - : content; -} - -function addLocalizedTemplateLang(path, content) { - return path.startsWith("zh/blog/") && !content.includes("\nlang: zh\n") - ? content.replace("layout: layouts/base.njk", "layout: layouts/base.njk\nlang: zh") - : content; -} - export default function (eleventyConfig) { - const originalAddTemplate = eleventyConfig.addTemplate.bind(eleventyConfig); - eleventyConfig.addTemplate = (virtualInputPath, content, data) => - originalAddTemplate( - virtualInputPath, - patchIndexFrontMatter( - virtualInputPath, - addSharedProseClass( - virtualInputPath, - addLocalizedTemplateLang(virtualInputPath, content) - ) - ), - data - ); - eleventyConfig.addPlugin(techdoc, { + // Implements [WITHDRAWAL-COPY]: the site's own description is the approved + // one-line copy, generated from the messaging spec. site: { name: "Basilisk", url: "https://www.basilisk-python.dev", - description: - "Open-source Python type checker and language server built in Rust. Conformance and benchmark results are withdrawn during an integrity review.", + description: withdrawal.line, author: "The Basilisk Project", themeColor: "#e8500a", stylesheet: "/assets/css/styles.css", - ogImage: "/assets/images/og-image.png", organization: { name: "Basilisk", url: "https://www.basilisk-python.dev", logo: "/assets/images/favicon.png", - sameAs: [ - "https://github.com/Nimblesite/Basilisk", - ], + sameAs: ["https://github.com/Nimblesite/Basilisk"], }, }, + // The site serves one statement and a notice at every retired URL. There is + // no blog, no docs tree, and no second language to translate into: the + // approved copy exists only in English ([WITHDRAWAL-COPY]). features: { - blog: true, - docs: true, + blog: false, + docs: false, darkMode: true, - i18n: true, - }, - // Register the languages the site actually ships so the base layout emits a - // complete hreflang cluster (en + zh + x-default) and og:locale:alternate. - // Without this, supportedLanguages defaults to ['en'] and the Chinese pages - // are never declared as alternates — Google can't connect / ⇄ /zh/. - i18n: { - defaultLanguage: "en", - languages: ["en", "zh"], + i18n: false, }, }); - // Preserve CJK headings in fragment identifiers. Techdoc's default slugger - // strips all non-ASCII letters, which produces empty and numeric-only IDs on - // Chinese prose pages and breaks their heading permalinks. - const markdown = markdownIt({ html: true, breaks: false, linkify: true }).use( - markdownItAnchor, - { - level: [1, 2, 3, 4], - permalink: markdownItAnchor.permalink.headerLink(), - slugify: (value) => - value - .normalize("NFKC") - .toLowerCase() - .trim() - .replace(/[^\p{Letter}\p{Number}_-]+/gu, "-") - .replace(/^-+|-+$/g, ""), - } - ); - // Eleventy executes plugins after the project config returns, so register - // this override as the next plugin to ensure it runs after techdoc's default. - eleventyConfig.addPlugin((config) => config.setLibrary("md", markdown)); - eleventyConfig.addPassthroughCopy("src/assets"); - eleventyConfig.addPassthroughCopy({ - "node_modules/monaco-editor/min/vs": "assets/vendor/monaco/vs", - "node_modules/lz-string/libs/lz-string.min.js": "assets/vendor/lz-string.min.js", - }); eleventyConfig.addPassthroughCopy("src/CNAME"); - // [Author pages] Posts written by a given author, matched on the post's - // `author` front-matter string == the author's `name` in _data/authors.json. - // Newest first, English posts only (Chinese posts carry their own byline). - eleventyConfig.addFilter("authorPosts", (posts, authorName) => - (posts || []) - .filter((p) => p.data.author === authorName && !p.url.startsWith("/zh/")) - .sort((a, b) => b.date - a.date) - ); - - // [Author pages] Plain-text truncation for meta descriptions built from a bio. - eleventyConfig.addFilter("truncate", (str, len) => { - const s = String(str || ""); - if (s.length <= len) return s; - return s.slice(0, s.lastIndexOf(" ", len)).trimEnd() + "…"; - }); - - const categoryLabels = { - en: { announcements: "Announcements", "deep-dives": "Deep dives" }, - zh: { announcements: "公告", "deep-dives": "深度解析" }, - }; - eleventyConfig.addFilter("blogCategoryLabel", (category, lang = "en") => - categoryLabels[lang]?.[category] || - String(category || "").replaceAll("-", " ") - ); - - const docsNavActive = (node, currentUrl) => { - const current = String(currentUrl || "").replace(/^\/zh(?=\/)/, ""); - if ( - node?.kind === "rules" && - (current.startsWith("/docs/rules/") || current.startsWith("/errors/")) - ) { - return true; - } - if (node?.url === current) return true; - return [...(node?.items || []), ...(node?.children || [])].some((child) => - docsNavActive(child, current) - ); - }; - eleventyConfig.addFilter("docsNavActive", docsNavActive); - // Base layout guard: only advertise a language alternate when Eleventy - // actually generated that URL. This prevents hreflang and switcher 404s on - // English-only docs, author profiles, benchmarks, and diagnostic pages. + // actually generated that URL. eleventyConfig.addFilter("hasPageUrl", (pages, url) => (pages || []).some((page) => page.url === url) ); @@ -247,7 +81,7 @@ export default function (eleventyConfig) { includes: "_includes", data: "_data", }, - templateFormats: ["md", "njk", "html"], + templateFormats: ["njk", "html"], markdownTemplateEngine: "njk", htmlTemplateEngine: "njk", }; diff --git a/website/package-lock.json b/website/package-lock.json index 1feff7b86..743901836 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -7,16 +7,11 @@ "": { "name": "basilisk-website", "version": "1.0.0", - "dependencies": { - "lz-string": "^1.5.0", - "monaco-editor": "^0.56.0" - }, "devDependencies": { "@11ty/eleventy": "^3.1.6", "@playwright/test": "^1.62.1", "@types/node": "^26.1.2", - "eleventy-plugin-techdoc": "^0.2.0", - "markdown-it": "^15.0.0" + "eleventy-plugin-techdoc": "^0.2.0" } }, "node_modules/@11ty/dependency-tree": { @@ -748,13 +743,6 @@ "undici-types": "~8.3.0" } }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true - }, "node_modules/a-sync-waterfall": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", @@ -1066,15 +1054,6 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/dompurify": { - "version": "3.4.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", - "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -1751,6 +1730,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "uc.micro": "^3.0.0" } @@ -1793,15 +1773,6 @@ "node": ">=12" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/markdown-it": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.0.tgz", @@ -1818,6 +1789,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "argparse": "^3.0.0", "entities": "^8.0.0", @@ -1856,7 +1828,8 @@ "url": "https://github.com/sponsors/nodeca" } ], - "license": "Python-2.0" + "license": "Python-2.0", + "peer": true }, "node_modules/markdown-it/node_modules/entities": { "version": "8.0.0", @@ -1864,6 +1837,7 @@ "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1871,18 +1845,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/marked": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", - "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/mdurl": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", @@ -1963,16 +1925,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/monaco-editor": { - "version": "0.56.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.56.0.tgz", - "integrity": "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==", - "license": "MIT", - "dependencies": { - "dompurify": "3.4.8", - "marked": "14.0.0" - } - }, "node_modules/moo": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", @@ -2471,7 +2423,8 @@ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/undici-types": { "version": "8.3.0", diff --git a/website/package.json b/website/package.json index e04e73486..5f865d2b6 100644 --- a/website/package.json +++ b/website/package.json @@ -3,13 +3,11 @@ "version": "1.0.0", "private": true, "type": "module", - "description": "Website for Basilisk, the open-source Python type checker and language server built in Rust.", + "description": "Website for Basilisk.", "scripts": { "build": "eleventy", - "build:wasm": "npx --yes wasm-pack build ../crates/basilisk-wasm --target web --release --out-dir ../../website/src/assets/wasm --out-name basilisk_wasm", "start": "eleventy --serve --watch", "clean": "rm -rf _site", - "screenshots": "node screenshots/generate.mjs", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui" }, @@ -17,11 +15,6 @@ "@11ty/eleventy": "^3.1.6", "@playwright/test": "^1.62.1", "@types/node": "^26.1.2", - "eleventy-plugin-techdoc": "^0.2.0", - "markdown-it": "^15.0.0" - }, - "dependencies": { - "lz-string": "^1.5.0", - "monaco-editor": "^0.56.0" + "eleventy-plugin-techdoc": "^0.2.0" } } diff --git a/website/screenshots/ansi.mjs b/website/screenshots/ansi.mjs deleted file mode 100644 index 85cc0d1e5..000000000 --- a/website/screenshots/ansi.mjs +++ /dev/null @@ -1,72 +0,0 @@ -// 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 deleted file mode 100644 index 71d28dbbd..000000000 --- a/website/screenshots/generate.mjs +++ /dev/null @@ -1,95 +0,0 @@ -// Implements [WEBSITE-SCREENSHOTS] / [WEBSITE-SCREENSHOTS-PURPOSE]: the single, -// fully automated, reproducible command that produces the site's CLI screenshots — -// real `basilisk check --color always` output, PII-free, with a built-in guard -// that every snippet still triggers the diagnostic it documents. -// 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 deleted file mode 100644 index 700e7ea17..000000000 --- a/website/screenshots/shots.mjs +++ /dev/null @@ -1,275 +0,0 @@ -// 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-0001", - code: `def process(data) -> str: - return data.upper() -`, - }, - { - name: "e0002", - expect: "BSK-0002", - code: `def get_user(user_id: int): - return {"id": user_id} -`, - }, - { - name: "e0003", - expect: "BSK-0003", - code: `data = [] -`, - }, - { - name: "e0004", - expect: "BSK-0004", - code: `def log(*args, **kwargs) -> None: - print(args, kwargs) -`, - }, - { - name: "e0005", - expect: "BSK-0005", - code: `class Registry: - entries = [] -`, - }, - { - name: "e0010", - expect: "imports_unresolved", - code: `from legacy_module import process_data -`, - }, - { - name: "e0011", - expect: "BSK-0014", - code: `from typing import Any - - -def handle(data: Any) -> bool: - return True -`, - }, - { - name: "e0012", - expect: "calls_argument_type", - code: `def greet(name: str) -> str: - return f"Hello, {name}" - - -greet(42) -`, - }, - { - name: "e0013", - expect: "returns_compatibility_2", - code: `def get_count() -> int: - return "many" -`, - }, - { - name: "e0014", - expect: "assignment_compatibility", - code: `count: int = "zero" -`, - }, - { - name: "e0015", - expect: "callables_annotation", - code: `x: dict[str] = {} -`, - }, - { - name: "e0016", - expect: "classes_override", - 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: "names_undefined", - code: `def f() -> int: - return missing_local -`, - }, - { - name: "e0019", - expect: "names_unbound", - code: `def check(flag: bool) -> str: - if flag: - result = "yes" - return result -`, - }, - { - name: "e0025", - expect: "BSK-0025", - code: `class Base: - def process(self) -> str: - return "base" - - -class Child(Base): - def process(self) -> str: - return "child" -`, - }, - { - name: "e0017", - expect: "classes_override_2", - code: `class Base: - x: int - - -class Child(Base): - x: str -`, - }, - { - name: "e0020", - expect: "overloads_definitions", - code: `from typing import overload - - -@overload -def f(x: int) -> int: ... -@overload -def f(x: str) -> str: ... -`, - }, - { - name: "e0023", - expect: "match_exhaustiveness", - code: `def classify(x: int | str) -> str: - match x: - case int(): - return "number" -`, - }, - { - name: "e0026", - expect: "generics_basic", - code: `from typing import TypeVar - -T = TypeVar("T", int) -`, - }, - { - name: "e0027", - expect: "generics_base_class", - code: `from typing import Generic, TypeVar - -T = TypeVar("T") - - -class Box(Generic[T, T]): - ... -`, - }, - { - name: "e0029", - expect: "typeddicts_class_syntax", - code: `from typing import TypedDict - - -class Movie(TypedDict): - title: str - - def play(self) -> None: - ... -`, - }, - { - name: "e0031", - expect: "directives_cast", - code: `from typing import cast - -x = cast(int) -`, - }, - { - name: "e0033", - expect: "directives_reveal_type", - code: `reveal_type() -`, - }, - { - name: "e0040", - expect: "enums_behaviors", - code: `from enum import Enum - - -class Base(Enum): - A = 1 - - -class Sub(Base): - B = 2 -`, - }, - { - name: "e0041", - expect: "calls_argument_count", - code: `def add(x: int, y: int) -> int: - return x + y - - -add(1) -`, - }, - { - name: "e0099", - expect: "protocols_explicit", - code: `from typing import Protocol - - -class P(Protocol): - def f(self) -> None: ... - - -P() -`, - }, - { - name: "e0115", - expect: "directives_deprecated", - code: `from warnings import deprecated - - -@deprecated("use bar") -def foo() -> None: ... - - -foo() -`, - }, -]; - -// A rule shot's source file is named after the image stem (e0001 → e0001.py). -export const SHOTS = RULE_SHOTS.map((shot) => ({ ...shot, file: `${shot.name}.py` })); diff --git a/website/screenshots/terminal.mjs b/website/screenshots/terminal.mjs deleted file mode 100644 index caf4e1559..000000000 --- a/website/screenshots/terminal.mjs +++ /dev/null @@ -1,109 +0,0 @@ -// 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/404.njk b/website/src/404.njk new file mode 100644 index 000000000..ddc8f3686 --- /dev/null +++ b/website/src/404.njk @@ -0,0 +1,18 @@ +--- +layout: layouts/base.njk +permalink: /404.html +noTranslation: true +robots: "noindex, follow" +eleventyComputed: + title: "{{ withdrawal.title }}" + description: "{{ withdrawal.line }}" +--- + +{#- Implements [WITHDRAWAL-COPY-SHORT] for any URL the retired list does not + cover. Copy comes from the messaging spec via _data/withdrawal.json. -#} + diff --git a/website/src/_data/authors.json b/website/src/_data/authors.json deleted file mode 100644 index 224c8aa4b..000000000 --- a/website/src/_data/authors.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "_doc": "Author registry — the single source of truth for blog author pages. Each entry generates a page at /authors// via src/authors/author.njk, and blog post bylines (author: field, matched by `name` or `nameZh`) link to it. Add an author here, set `author: ` (or the localized `nameZh` in a zh post) in a post's front matter, and the byline links automatically.", - "authors": [ - { - "slug": "basilisk-team", - "name": "The Basilisk Project", - "nameZh": "Basilisk 项目", - "shortName": "Basilisk Team", - "role": "The team behind Basilisk", - "avatar": "/assets/images/authors/basilisk-team.png", - "bio": "The Basilisk Project is the team voice for everyone who contributes to Basilisk, an open-source Python type checker and language server built in Rust. Basilisk is built by Nimblesite through a human and AI development process. The project is currently strengthening its review, auditing, and robustness-testing practices after withdrawing its former conformance result.", - "links": [ - { "label": "GitHub", "url": "https://github.com/Nimblesite/Basilisk" }, - { "label": "Discord", "url": "https://discord.gg/4wBDSGEZQd" }, - { "label": "Nimblesite", "url": "https://www.nimblesite.co" } - ], - "sameAs": [ - "https://github.com/Nimblesite/Basilisk", - "https://www.nimblesite.co" - ] - }, - { - "slug": "christian-findlay", - "name": "Christian Findlay", - "nameZh": "Christian Findlay", - "shortName": "Christian Findlay", - "role": "Director, Nimblesite", - "avatar": "/assets/images/authors/christian-findlay.png", - "bio": "Christian Findlay is the director of Nimblesite and the person behind Basilisk. He has spent more than two decades building software across .NET, Dart, Flutter, and Rust, and writes about type systems, developer experience, and building software with AI. He is leading the project's current conformance remediation and review-process changes.", - "links": [ - { "label": "Website", "url": "https://www.christianfindlay.com" }, - { "label": "GitHub", "url": "https://github.com/MelbourneDeveloper" }, - { "label": "Medium", "url": "https://cfdevelop.medium.com" } - ], - "sameAs": [ - "https://www.christianfindlay.com", - "https://github.com/MelbourneDeveloper", - "https://cfdevelop.medium.com" - ] - } - ] -} diff --git a/website/src/_data/benchmarks.js b/website/src/_data/benchmarks.js deleted file mode 100644 index ba804314d..000000000 --- a/website/src/_data/benchmarks.js +++ /dev/null @@ -1,189 +0,0 @@ -// Eleventy global data for withdrawn historical benchmark results, read from the -// git-tracked per-machine CSV that `make bench` generated. -// -// The integrity review has withdrawn these measurements from comparison. This -// loader preserves the old table for transparency; derived medians and fastest -// fields are historical implementation details and must not drive public claims. -// -// Historical primary-machine selection (what the withdrawn table preserves): -// 1. $BASILISK_BENCH_PRIMARY (slug) 2. benchmarks/status/.primary file -// 3. otherwise rank by tool coverage (a CSV missing competitor columns must -// never win), then prefer `gha-*` (stable CI hardware), then alphabetical -import { readFileSync, readdirSync, existsSync } from "fs"; -import { dirname, join } from "path"; -import { fileURLToPath } from "url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const STATUS_DIR = join(__dirname, "../../../benchmarks/status"); - -// Parse the CSV `# tools:` header into [{ tool, version }] for the methodology -// footnote. The header looks like: -// "basilisk=basilisk 0.0.0, pyright=pyright 1.1.408, mypy=mypy 1.19.1 (compiled: yes), ..." -// i.e. comma-separated `name=<--version output>` entries. The version output -// usually repeats the tool name and may carry a trailing parenthetical, both of -// which we strip so the site shows a clean "pyright 1.1.408". This is metadata -// pass-through — the harness records each installed tool's version output, and -// the page shows a cleaned form of that recorded value. -function parseToolVersions(toolsStr) { - if (!toolsStr) return []; - return toolsStr - .split(/,\s+(?=[a-z0-9_]+=)/i) - .map((entry) => { - const eq = entry.indexOf("="); - const tool = (eq >= 0 ? entry.slice(0, eq) : entry).trim(); - let version = (eq >= 0 ? entry.slice(eq + 1) : "").replace(/\s*\(.*\)\s*$/, "").trim(); - const prefix = `${tool} `; - if (version.toLowerCase().startsWith(prefix.toLowerCase())) { - version = version.slice(prefix.length).trim(); - } - // Dev builds don't have a released version number. Preserve the source - // identifier emitted in 0.0.0-dev+g and keep a dirty marker visible. - // A bare placeholder degrades to a plain "dev build" label. - const devPin = version.match(/dev\+g([0-9a-f]+(?:-dirty)?)/i); - if (devPin) version = `dev (${devPin[1]})`; - else if (!version || /placeholder/i.test(version)) version = "dev build"; - return { tool, version }; - }) - .filter((t) => t.tool); -} - -function parseCsv(text) { - const meta = {}; - const dataLines = []; - for (const raw of text.split(/\r?\n/)) { - const line = raw.trim(); - if (!line) continue; - if (line.startsWith("#")) { - const m = line.slice(1).match(/^\s*([^:]+):\s*(.*)$/); - if (m) meta[m[1].trim()] = m[2].trim(); - } else { - dataLines.push(line); - } - } - if (dataLines.length < 2) return null; - - // Friendly minimum run count: the header begins with the number of Hyperfine - // measurements required for every file, followed by the noisy-run policy. - const runsMatch = (meta.runs || "").match(/^\d+/); - meta.runsCount = runsMatch ? runsMatch[0] : null; - meta.toolVersions = parseToolVersions(meta.tools); - - // Column layout: `fixture`, then one `_ms` per timed tool. Diagnostic - // columns follow, but the benchmark page intentionally presents timings only. - // A blank `_ms` cell means the tool was unavailable or failed preflight. - const msIdx = new Map(); - dataLines[0].split(",").forEach((c, i) => { - if (c.endsWith("_ms")) msIdx.set(c.slice(0, -"_ms".length), i); - }); - const allTools = [...msIdx.keys()]; - // Warm-cache variants (…-warm) aren't separate checkers, so exclude them from - // the historical cold medians. Their per-file values stay in rows. - const tools = allTools.filter((t) => !t.endsWith("-warm")); - const rows = dataLines.slice(1).map((line) => { - const parts = line.split(","); - const num = (i) => - i == null || parts[i] === undefined || parts[i] === "" ? null : parseFloat(parts[i]); - const values = {}; - const valueText = {}; - for (const [tool, index] of msIdx) { - values[tool] = num(index); - valueText[tool] = parts[index] ? `${parts[index]} ms` : "—"; - } - return { - fixture: parts[0], - filename: `${parts[0]}.py`, - values, - valueText, - }; - }); - return { meta, tools, allTools, rows }; -} - -function median(nums) { - const s = [...nums].sort((a, b) => a - b); - const mid = Math.floor(s.length / 2); - return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2; -} - -// Historical per-checker median cold full-file time. This and `fastest` remain -// available only to preserve the old data shape; neither is publishable while -// the benchmark methodology and results are under integrity review. -function computeToolMedians(rows, tools) { - const ms = {}; - const text = {}; - for (const tool of tools) { - if (tool.endsWith("-warm")) continue; - const vals = rows.map((r) => r.values[tool]).filter((v) => v != null && v > 0); - ms[tool] = vals.length ? Math.round(median(vals)) : null; - text[tool] = ms[tool] == null ? "—" : `${ms[tool]} ms`; - } - const ranked = Object.entries(ms).filter(([, v]) => v != null); - const fastest = ranked.length - ? ranked.reduce((best, entry) => (entry[1] < best[1] ? entry : best))[0] - : null; - return { ms, text, fastest }; -} - -// How many tool columns in a CSV carry at least one real measurement. A machine -// that only ran basilisk scores 1; a full competitor sweep scores every tool. -// Used to keep an incomplete CSV from ever becoming the site's primary and -// rendering a benchmark table full of empty competitor columns. -function toolCoverage(file) { - const parsed = parseCsv(readFileSync(join(STATUS_DIR, file), "utf-8")); - if (!parsed) return -1; - return parsed.tools.filter((t) => parsed.rows.some((r) => r.values[t] != null)) - .length; -} - -function pickPrimary(files) { - // Explicit overrides win, in order: env var, then a committed .primary pin. - const env = process.env.BASILISK_BENCH_PRIMARY; - if (env && files.includes(`${env}.csv`)) return `${env}.csv`; - const primaryFile = join(STATUS_DIR, ".primary"); - if (existsSync(primaryFile)) { - const slug = readFileSync(primaryFile, "utf-8").trim(); - if (files.includes(`${slug}.csv`)) return `${slug}.csv`; - } - // Automatic fallback: NEVER let an incomplete CSV (e.g. a machine that only - // ran basilisk) win and drop competitor columns. Rank by tool coverage first, - // then stable CI hardware (gha-*), then alphabetical for determinism. - const coverage = new Map(files.map((f) => [f, toolCoverage(f)])); - return [...files].sort( - (a, b) => - coverage.get(b) - coverage.get(a) || - (a.startsWith("gha-") ? 0 : 1) - (b.startsWith("gha-") ? 0 : 1) || - a.localeCompare(b), - )[0]; -} - -export default function () { - const empty = { - available: [], - primary: null, - meta: {}, - tools: [], - rows: [], - hasData: false, - withdrawn: true, - publicationStatus: "historical-withdrawn", - }; - if (!existsSync(STATUS_DIR)) return empty; - - const files = readdirSync(STATUS_DIR).filter((f) => f.endsWith(".csv")).sort(); - if (files.length === 0) return empty; - - const primary = pickPrimary(files); - const parsed = parseCsv(readFileSync(join(STATUS_DIR, primary), "utf-8")); - if (!parsed) return empty; - - // Preserve the old measurements as explicitly withdrawn historical data. - return { - available: files.map((f) => f.replace(/\.csv$/, "")), - primary: primary.replace(/\.csv$/, ""), - ...parsed, - toolMedians: computeToolMedians(parsed.rows, parsed.tools), - hasData: parsed.rows.length > 0, - withdrawn: true, - publicationStatus: "historical-withdrawn", - }; -} diff --git a/website/src/_data/conformance.js b/website/src/_data/conformance.js deleted file mode 100644 index 33f49c086..000000000 --- a/website/src/_data/conformance.js +++ /dev/null @@ -1,253 +0,0 @@ -// Eleventy global data retained for the conformance integrity audit. These are -// historical outputs from the python/typing harness, not a current Basilisk -// conformance result. The former result is withdrawn because fitted checker -// logic made it untrustworthy; public pages must not present these values as a -// score, standing, pass count, or proof of implementation quality. -// -// conformance/conformance_status.csv -> historical per-file output -// website/src/_data/conformance_report.json -> historical run metadata -// git log of conformance_status.csv -> historical audit trail -// -// A file was marked passing when the harness reported no diagnostic diff. That -// records what happened in the exact fixtures; it does not establish general -// conformance. Values are exposed only under `historical` with an explicit -// withdrawn status. -import { readFileSync, existsSync } from "fs"; -import { execFileSync } from "child_process"; -import { dirname, join } from "path"; -import { fileURLToPath } from "url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = join(__dirname, "../../.."); -const CONF_DIR = join(REPO_ROOT, "conformance"); -const STATUS_REL = "conformance/conformance_status.csv"; -const STATUS_CSV = join(CONF_DIR, "conformance_status.csv"); -// The exact historical python/typing snapshot and withdrawn fixture-result -// metadata. It lives in this same _data dir; it is not current-main data. -const REPORT = join(__dirname, "conformance_report.json"); - -// The day the official python/typing scoring rules replaced our earlier in-repo -// script. That script excluded some diagnostic codes and did not count false -// positives, so it miscalculated the score (up to 100%). Commits dated on/after -// this used the official scoring semantics; before, the earlier in-repo -// measurement. -const OFFICIAL_SINCE = "2026-06-23"; - -// The CSV stores lowercase category slugs; these render the few that are not a -// plain title-case word. Everything else falls back to capitalising the slug. -const CATEGORY_LABELS = { - typeddicts: "TypedDicts", - namedtuples: "NamedTuples", - typeforms: "TypeForms", - specialtypes: "Special types", -}; - -const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - -const round1 = (n) => Math.round(n * 10) / 10; -const labelFor = (slug) => CATEGORY_LABELS[slug] || (slug ? slug.charAt(0).toUpperCase() + slug.slice(1) : "—"); - -// "2026-06-21" -> "Jun 21" (manual parse — no timezone surprises). -function shortDate(iso) { - const [, m, d] = iso.split("-").map((p) => parseInt(p, 10)); - return Number.isFinite(m) && Number.isFinite(d) ? `${MONTHS[m - 1]} ${d}` : iso; -} - -// Read the machine-readable report, which is the single source for the upstream -// commit. Written by the pristine fixture runner; never hand-edited. -function readReport() { - if (!existsSync(REPORT)) return null; - try { - return JSON.parse(readFileSync(REPORT, "utf-8")); - } catch { - return null; - } -} - -// Tally one CSV body (pass/total/fp/missed) from its raw text. -function tally(csvText) { - const rows = csvText.split(/\r?\n/).slice(1).filter((l) => l.trim() && !l.startsWith("#")); - const t = { pass: 0, total: 0, fp: 0, missed: 0, byFile: rows }; - for (const line of rows) { - const f = line.split(","); - if (f.length < 7) continue; - t.total += 1; - if (f[3] === "PASS") t.pass += 1; - t.missed += parseInt(f[5], 10) || 0; - t.fp += parseInt(f[6], 10) || 0; - } - return t; -} - -function parseStatus() { - if (!existsSync(STATUS_CSV)) return null; - const text = readFileSync(STATUS_CSV, "utf-8"); - const t = tally(text); - if (!t.total) return null; - - const cats = new Map(); - const failing = []; - let caught = 0; - for (const line of t.byFile) { - const f = line.split(","); - if (f.length < 7) continue; - const passed = f[3] === "PASS"; - const slug = f[2]; - const missed = parseInt(f[5], 10) || 0; - const fp = parseInt(f[6], 10) || 0; - caught += parseInt(f[4], 10) || 0; - if (!cats.has(slug)) cats.set(slug, { slug, label: labelFor(slug), pass: 0, total: 0 }); - const entry = cats.get(slug); - entry.total += 1; - entry.pass += passed ? 1 : 0; - if (!passed) failing.push({ file: f[1], category: slug, missed, fp }); - } - - const categories = [...cats.values()] - .filter((c) => c.slug) - .map((c) => ({ ...c, pct: round1((c.pass / c.total) * 100) })) - .sort((a, b) => a.label.localeCompare(b.label)); - - return { - pass: t.pass, - total: t.total, - fail: t.total - t.pass, - caught, - missed: t.missed, - fp: t.fp, - scorePct: round1((t.pass / t.total) * 100), - categories, - categoriesTotal: categories.length, - categoriesPass100: categories.filter((c) => c.pass === c.total).length, - failing: failing.sort((a, b) => b.fp + b.missed - (a.fp + a.missed)), - }; -} - -function git(args) { - // stderr ignored: early commits hold the file under an old path, so `git show` - // legitimately fails for those — we skip them, no need to spam the build log. - return execFileSync("git", args, { cwd: REPO_ROOT, encoding: "utf-8", maxBuffer: 1 << 26, stdio: ["ignore", "pipe", "ignore"] }); -} - -// The over-time series, read from the GIT history of conformance_status.csv. -// One real data point per commit that changed the file: its commit date and the -// score that commit recorded. Points dated before OFFICIAL_SINCE were produced -// by the earlier in-repo script; on/after, by the official scoring semantics. -function gitHistory() { - let log; - try { - log = git(["log", "--follow", "--format=%H|%cs", "--", STATUS_REL]); - } catch { - return []; - } - const points = []; - for (const line of log.split(/\r?\n/).filter(Boolean)) { - const [hash, date] = line.split("|"); - let csv; - try { - csv = git(["show", `${hash}:${STATUS_REL}`]); - } catch { - continue; - } - const t = tally(csv); - if (!t.total) continue; - points.push({ - hash: hash.slice(0, 8), - date, - shortDate: shortDate(date), - pass: t.pass, - total: t.total, - fp: t.fp, - missed: t.missed, - score: round1((t.pass / t.total) * 100), - official: date >= OFFICIAL_SINCE, - }); - } - return points.reverse(); // oldest -> newest -} - -// Inline-SVG geometry for the over-time chart. Computed here (testable, DRY) so -// the Nunjucks include only loops over coordinates. Points are spaced evenly by -// commit (each is a real event); the y-axis is the pass percentage 0–100. -function buildChart(points) { - if (points.length < 2) return null; - const width = 760, height = 360, left = 48, right = 24, top = 28, bottom = 64; - const plotW = width - left - right, plotH = height - top - bottom; - const n = points.length; - const xAt = (i) => round1(left + (i / (n - 1)) * plotW); - const yAt = (score) => round1(top + (1 - score / 100) * plotH); - - let lastLabel = null; - const pts = points.map((p, i) => { - const showDate = p.shortDate !== lastLabel; - lastLabel = p.shortDate; - return { ...p, i, x: xAt(i), y: yAt(p.score), showDate }; - }); - const yTicks = [0, 25, 50, 75, 100].map((value) => ({ value, y: yAt(value) })); - - const previous = pts.filter((p) => !p.official); - const official = pts.filter((p) => p.official); - const lastPrevious = previous[previous.length - 1]; - const firstOfficial = official[0]; - const peak = pts.reduce((a, b) => (b.score > a.score ? b : a), pts[0]); - - return { - width, height, left, right, top, bottom, - baselineY: yAt(0), - pts, - yTicks, - prevPolyline: previous.map((p) => `${p.x},${p.y}`).join(" "), - officialPolyline: official.map((p) => `${p.x},${p.y}`).join(" "), - // The correction "cliff": last earlier-era point down to the first official one. - drop: lastPrevious && firstOfficial - ? { x1: lastPrevious.x, y1: lastPrevious.y, x2: firstOfficial.x, y2: firstOfficial.y, from: lastPrevious.score, to: firstOfficial.score } - : null, - peak, - current: pts[pts.length - 1], - }; -} - -export default function () { - const status = parseStatus(); - if (!status) { - return { - hasData: false, - withdrawn: true, - publicationStatus: "historical-withdrawn", - historical: null, - }; - } - - // The resolved upstream commit comes from the conformance report. - const report = readReport(); - const upstream = report?.upstream ?? {}; - const pinnedRef = upstream.sha ?? null; - - // Historical data still carries the exact python/typing commit so the audit - // can reproduce the withdrawn run. A missing commit would make that record - // incomplete, so fail rather than silently detach it from its source. - if (!pinnedRef) { - throw new Error( - "conformance: conformance_status.csv has score data but conformance_report.json " + - "records no python/typing commit — run the real python/typing harness gate", - ); - } - - const history = gitHistory(); - return { - hasData: true, - withdrawn: true, - publicationStatus: "historical-withdrawn", - historical: { - ...status, - upstreamRef: upstream.ref ?? "main", - pinnedRef, - pinnedRefShort: upstream.shortSha ?? (pinnedRef ? pinnedRef.slice(0, 7) : null), - commitDate: upstream.commitDate || null, - stale: upstream.stale ?? false, - officialHarnessSince: OFFICIAL_SINCE, - history, - chart: buildChart(history), - }, - }; -} diff --git a/website/src/_data/conformanceOfficial.js b/website/src/_data/conformanceOfficial.js deleted file mode 100644 index 67afff6fc..000000000 --- a/website/src/_data/conformanceOfficial.js +++ /dev/null @@ -1,78 +0,0 @@ -// Historical python/typing leaderboard snapshot retained only for the public -// record of Basilisk's withdrawn announcement. Basilisk is no longer listed in -// the live official results, and its row below is invalid as evidence of actual -// conformance because the implementation was fitted to exact fixtures. -// -// _data/conformance.js -> historical outputs from Basilisk's withdrawn run. -// _data/conformanceOfficial.js (this file) -// -> the dated snapshot used in the retracted post. -// -// The snapshot is pinned so the retraction can show exactly what was published. -// It must never be described as current. The live source is linked separately. -// -// Source of every value below: -// https://github.com/python/typing/blob/main/conformance/results/results.html -// as published in python/typing@3410759355c3018063d3a446102f88621fc43eb5, -// 2026-07-31. PR #2316 originally added Basilisk to the board. This snapshot is -// intentionally frozen; do not refresh it from the live leaderboard. - -const SNAPSHOT = { - source: "https://github.com/python/typing/blob/main/conformance/results/results.html", - resultsDir: "https://github.com/python/typing/tree/3410759355c3018063d3a446102f88621fc43eb5/conformance/results", - snapshotUrl: "https://github.com/python/typing/blob/3410759355c3018063d3a446102f88621fc43eb5/conformance/results/results.html", - commitUrl: "https://github.com/python/typing/commit/3410759355c3018063d3a446102f88621fc43eb5", - addedPrUrl: "https://github.com/python/typing/pull/2316", - sha: "3410759", - date: "2026-07-31", - dateLabel: "Jul 31, 2026", -}; - -// Historical leaderboard grand-total row, verbatim from that snapshot. -// Basilisk's row and comparisons derived from it are withdrawn. -const TOOLS = [ - { id: "basilisk", name: "Basilisk", version: "0.27.0", org: null, pass: 141, total: 141 }, - { id: "pyright", name: "Pyright", version: "1.1.410", org: "Microsoft", pass: 136.5, total: 141 }, - { id: "mypy", name: "mypy", version: "2.1.0", org: null, pass: 109, total: 141 }, - { id: "ty", name: "ty", version: "0.0.65", org: "Astral", pass: 122, total: 141 }, - { id: "pyrefly", name: "Pyrefly", version: "1.1.0", org: "Meta", pass: 138, total: 141 }, - { id: "zuban", name: "zuban", version: "0.8.2", org: null, pass: 140.5, total: 141 }, - { id: "pycroscope", name: "pycroscope", version: "0.4.0", org: null, pass: 130, total: 141 }, -]; - -const round1 = (n) => Math.round(n * 10) / 10; - -export default function () { - const enrich = (t) => ({ - ...t, - pct: round1((t.pass / t.total) * 100), - // A whole-number pass renders as "141"; a half-point as "140.5". - passLabel: Number.isInteger(t.pass) ? String(t.pass) : t.pass.toFixed(1), - resultsUrl: `${SNAPSHOT.resultsDir}/${t.id}`, - }); - - const tools = TOOLS.map(enrich); - const byId = Object.fromEntries(tools.map((t) => [t.id, t])); - const ranked = [...tools] - .sort((a, b) => b.pct - a.pct) - .map((t, i) => ({ ...t, rank: i + 1 })); - - const basilisk = byId.basilisk; - const perfect = tools.filter((t) => t.pass === t.total); - - return { - hasData: true, - withdrawn: true, - publicationStatus: "historical-withdrawn", - historical: { - snapshot: SNAPSHOT, - tools, - byId, - ranked, - basilisk, - basiliskRankAtSnapshot: ranked.find((t) => t.id === "basilisk").rank, - perfectCountAtSnapshot: perfect.length, - basiliskWasSolePerfectAtSnapshot: - perfect.length === 1 && perfect[0].id === "basilisk", - }, - }; -} diff --git a/website/src/_data/conformance_report.json b/website/src/_data/conformance_report.json deleted file mode 100644 index 516e350cf..000000000 --- a/website/src/_data/conformance_report.json +++ /dev/null @@ -1,1680 +0,0 @@ -{ - "_doc": "Generated by conformance/run_conformance.py from the python/typing harness at the last revision carrying the removed Basilisk adapter. This is internal fixture-regression evidence, not a current official conformance score.", - "upstream": { - "repo": "python/typing", - "ref": "a4906624f170c169cf667f962080c56d5a5ba6ff", - "sha": "a4906624f170c169cf667f962080c56d5a5ba6ff", - "shortSha": "a490662", - "commitDate": "2026-08-04", - "stale": true, - "withdrawn": true, - "status": "historical internal regression snapshot" - }, - "calculator": { - "file": "python/typing@a490662:conformance/src/main.py", - "sha256": "3cb2a27bfc689e89a541528f8bdaa8ed24ae8845ce048eaff69717ef0205b112", - "bytes": 10810, - "funcs": [ - "get_expected_errors", - "diff_expected_errors" - ] - }, - "grading": "upstream python/typing harness at the frozen last-adapter revision (src/main.py --only-run basilisk), every rule enabled; internal fixture-regression evidence only", - "score": { - "pass": 141, - "total": 141, - "fail": 0, - "scorePct": 100.0, - "caught": 970, - "missed": 0, - "falsePositives": 0 - }, - "files": [ - { - "file": "aliases_explicit.py", - "category": "aliases", - "status": "PASS", - "caught": 21, - "missed": 0, - "falsePositives": 0, - "codes": [ - "aliases_implicit" - ] - }, - { - "file": "aliases_implicit.py", - "category": "aliases", - "status": "PASS", - "caught": 22, - "missed": 0, - "falsePositives": 0, - "codes": [ - "aliases_implicit", - "annotations_forward_refs", - "generics_defaults_specialization" - ] - }, - { - "file": "aliases_newtype.py", - "category": "aliases", - "status": "PASS", - "caught": 14, - "missed": 0, - "falsePositives": 0, - "codes": [ - "aliases_newtype" - ] - }, - { - "file": "aliases_recursive.py", - "category": "aliases", - "status": "PASS", - "caught": 11, - "missed": 0, - "falsePositives": 0, - "codes": [ - "aliases_recursive", - "assignment_compatibility" - ] - }, - { - "file": "aliases_type_statement.py", - "category": "aliases", - "status": "PASS", - "caught": 24, - "missed": 0, - "falsePositives": 0, - "codes": [ - "aliases_type_statement", - "generics_syntax_scoping" - ] - }, - { - "file": "aliases_typealiastype.py", - "category": "aliases", - "status": "PASS", - "caught": 22, - "missed": 0, - "falsePositives": 0, - "codes": [ - "aliases_typealiastype" - ] - }, - { - "file": "aliases_variance.py", - "category": "aliases", - "status": "PASS", - "caught": 4, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_variance" - ] - }, - { - "file": "annotations_coroutines.py", - "category": "annotations", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [] - }, - { - "file": "annotations_forward_refs.py", - "category": "annotations", - "status": "PASS", - "caught": 19, - "missed": 0, - "falsePositives": 0, - "codes": [ - "annotations_forward_refs" - ] - }, - { - "file": "annotations_generators.py", - "category": "annotations", - "status": "PASS", - "caught": 10, - "missed": 0, - "falsePositives": 0, - "codes": [ - "annotations_generators", - "annotations_generators_2" - ] - }, - { - "file": "annotations_methods.py", - "category": "annotations", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [] - }, - { - "file": "annotations_typeexpr.py", - "category": "annotations", - "status": "PASS", - "caught": 15, - "missed": 0, - "falsePositives": 0, - "codes": [ - "aliases_implicit", - "annotations_forward_refs", - "annotations_typeexpr" - ] - }, - { - "file": "callables_annotation.py", - "category": "callables", - "status": "PASS", - "caught": 16, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility", - "callables_annotation", - "callables_protocol", - "callables_protocol_2" - ] - }, - { - "file": "callables_kwargs.py", - "category": "callables", - "status": "PASS", - "caught": 12, - "missed": 0, - "falsePositives": 0, - "codes": [ - "callables_kwargs", - "callables_protocol_2", - "calls_argument_type" - ] - }, - { - "file": "callables_protocol.py", - "category": "callables", - "status": "PASS", - "caught": 17, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility", - "callables_protocol_2" - ] - }, - { - "file": "callables_subtyping.py", - "category": "callables", - "status": "PASS", - "caught": 32, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility", - "callables_subtyping" - ] - }, - { - "file": "classes_classvar.py", - "category": "classes", - "status": "PASS", - "caught": 17, - "missed": 0, - "falsePositives": 0, - "codes": [ - "classes_classvar", - "protocols_definition_2", - "qualifiers_final_annotation" - ] - }, - { - "file": "classes_override.py", - "category": "classes", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [ - "classes_override_3" - ] - }, - { - "file": "constructors_call_init.py", - "category": "constructors", - "status": "PASS", - "caught": 5, - "missed": 0, - "falsePositives": 0, - "codes": [ - "constructors_call_init", - "generics_defaults_referential_2" - ] - }, - { - "file": "constructors_call_metaclass.py", - "category": "constructors", - "status": "PASS", - "caught": 2, - "missed": 0, - "falsePositives": 0, - "codes": [ - "calls_argument_count" - ] - }, - { - "file": "constructors_call_new.py", - "category": "constructors", - "status": "PASS", - "caught": 2, - "missed": 0, - "falsePositives": 0, - "codes": [ - "constructors_call_new" - ] - }, - { - "file": "constructors_call_type.py", - "category": "constructors", - "status": "PASS", - "caught": 8, - "missed": 0, - "falsePositives": 0, - "codes": [ - "constructors_call_type" - ] - }, - { - "file": "constructors_callable.py", - "category": "constructors", - "status": "PASS", - "caught": 12, - "missed": 0, - "falsePositives": 0, - "codes": [ - "constructors_callable" - ] - }, - { - "file": "constructors_consistency.py", - "category": "constructors", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [] - }, - { - "file": "dataclasses_descriptors.py", - "category": "dataclasses", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [] - }, - { - "file": "dataclasses_final.py", - "category": "dataclasses", - "status": "PASS", - "caught": 5, - "missed": 0, - "falsePositives": 0, - "codes": [ - "qualifiers_final_annotation_2" - ] - }, - { - "file": "dataclasses_frozen.py", - "category": "dataclasses", - "status": "PASS", - "caught": 2, - "missed": 0, - "falsePositives": 0, - "codes": [ - "dataclasses_frozen" - ] - }, - { - "file": "dataclasses_hash.py", - "category": "dataclasses", - "status": "PASS", - "caught": 4, - "missed": 0, - "falsePositives": 0, - "codes": [ - "dataclasses_hash" - ] - }, - { - "file": "dataclasses_inheritance.py", - "category": "dataclasses", - "status": "PASS", - "caught": 2, - "missed": 0, - "falsePositives": 0, - "codes": [ - "classes_override_2" - ] - }, - { - "file": "dataclasses_kwonly.py", - "category": "dataclasses", - "status": "PASS", - "caught": 3, - "missed": 0, - "falsePositives": 0, - "codes": [ - "dataclasses_kwonly" - ] - }, - { - "file": "dataclasses_match_args.py", - "category": "dataclasses", - "status": "PASS", - "caught": 1, - "missed": 0, - "falsePositives": 0, - "codes": [ - "dataclasses_match_args" - ] - }, - { - "file": "dataclasses_order.py", - "category": "dataclasses", - "status": "PASS", - "caught": 1, - "missed": 0, - "falsePositives": 0, - "codes": [ - "dataclasses_order" - ] - }, - { - "file": "dataclasses_postinit.py", - "category": "dataclasses", - "status": "PASS", - "caught": 4, - "missed": 0, - "falsePositives": 0, - "codes": [ - "dataclasses_postinit" - ] - }, - { - "file": "dataclasses_slots.py", - "category": "dataclasses", - "status": "PASS", - "caught": 4, - "missed": 0, - "falsePositives": 0, - "codes": [ - "dataclasses_slots" - ] - }, - { - "file": "dataclasses_transform_class.py", - "category": "dataclasses", - "status": "PASS", - "caught": 6, - "missed": 0, - "falsePositives": 0, - "codes": [ - "dataclasses_transform_class" - ] - }, - { - "file": "dataclasses_transform_converter.py", - "category": "dataclasses", - "status": "PASS", - "caught": 9, - "missed": 0, - "falsePositives": 0, - "codes": [ - "dataclasses_transform_class" - ] - }, - { - "file": "dataclasses_transform_field.py", - "category": "dataclasses", - "status": "PASS", - "caught": 2, - "missed": 0, - "falsePositives": 0, - "codes": [ - "dataclasses_kwonly" - ] - }, - { - "file": "dataclasses_transform_func.py", - "category": "dataclasses", - "status": "PASS", - "caught": 5, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility", - "constructors_call_init", - "dataclasses_frozen", - "dataclasses_kwonly", - "dataclasses_order" - ] - }, - { - "file": "dataclasses_transform_meta.py", - "category": "dataclasses", - "status": "PASS", - "caught": 6, - "missed": 0, - "falsePositives": 0, - "codes": [ - "dataclasses_transform_meta" - ] - }, - { - "file": "dataclasses_usage.py", - "category": "dataclasses", - "status": "PASS", - "caught": 9, - "missed": 0, - "falsePositives": 0, - "codes": [ - "calls_argument_count", - "dataclasses_inheritance", - "dataclasses_kwonly", - "dataclasses_usage" - ] - }, - { - "file": "directives_assert_type.py", - "category": "directives", - "status": "PASS", - "caught": 7, - "missed": 0, - "falsePositives": 0, - "codes": [ - "directives_assert_type", - "directives_assert_type_2" - ] - }, - { - "file": "directives_cast.py", - "category": "directives", - "status": "PASS", - "caught": 3, - "missed": 0, - "falsePositives": 0, - "codes": [ - "directives_cast" - ] - }, - { - "file": "directives_deprecated.py", - "category": "directives", - "status": "PASS", - "caught": 12, - "missed": 0, - "falsePositives": 0, - "codes": [ - "directives_deprecated" - ] - }, - { - "file": "directives_disjoint_base.py", - "category": "directives", - "status": "PASS", - "caught": 8, - "missed": 0, - "falsePositives": 0, - "codes": [ - "directives_disjoint_base" - ] - }, - { - "file": "directives_no_type_check.py", - "category": "directives", - "status": "PASS", - "caught": 1, - "missed": 0, - "falsePositives": 0, - "codes": [ - "calls_argument_count", - "calls_argument_type", - "returns_compatibility_2" - ] - }, - { - "file": "directives_reveal_type.py", - "category": "directives", - "status": "PASS", - "caught": 2, - "missed": 0, - "falsePositives": 0, - "codes": [ - "directives_reveal_type" - ] - }, - { - "file": "directives_type_checking.py", - "category": "directives", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [] - }, - { - "file": "directives_type_ignore.py", - "category": "directives", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [] - }, - { - "file": "directives_type_ignore_file1.py", - "category": "directives", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [] - }, - { - "file": "directives_type_ignore_file2.py", - "category": "directives", - "status": "PASS", - "caught": 1, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility" - ] - }, - { - "file": "directives_version_platform.py", - "category": "directives", - "status": "PASS", - "caught": 3, - "missed": 0, - "falsePositives": 0, - "codes": [ - "directives_version_platform" - ] - }, - { - "file": "enums_behaviors.py", - "category": "enums", - "status": "PASS", - "caught": 1, - "missed": 0, - "falsePositives": 0, - "codes": [ - "enums_behaviors", - "enums_expansion" - ] - }, - { - "file": "enums_definition.py", - "category": "enums", - "status": "PASS", - "caught": 1, - "missed": 0, - "falsePositives": 0, - "codes": [ - "enums_definition" - ] - }, - { - "file": "enums_expansion.py", - "category": "enums", - "status": "PASS", - "caught": 1, - "missed": 0, - "falsePositives": 0, - "codes": [ - "enums_expansion" - ] - }, - { - "file": "enums_member_names.py", - "category": "enums", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [] - }, - { - "file": "enums_member_values.py", - "category": "enums", - "status": "PASS", - "caught": 2, - "missed": 0, - "falsePositives": 0, - "codes": [ - "enums_member_values" - ] - }, - { - "file": "enums_members.py", - "category": "enums", - "status": "PASS", - "caught": 7, - "missed": 0, - "falsePositives": 0, - "codes": [ - "enums_members", - "enums_members_2" - ] - }, - { - "file": "exceptions_context_managers.py", - "category": "exceptions", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [] - }, - { - "file": "generics_base_class.py", - "category": "generics", - "status": "PASS", - "caught": 7, - "missed": 0, - "falsePositives": 0, - "codes": [ - "annotations_forward_refs", - "generics_base_class", - "generics_base_class_2", - "generics_base_class_3", - "generics_defaults_specialization" - ] - }, - { - "file": "generics_basic.py", - "category": "generics", - "status": "PASS", - "caught": 18, - "missed": 0, - "falsePositives": 0, - "codes": [ - "calls_argument_type", - "generics_base_class", - "generics_basic", - "generics_basic_2", - "generics_basic_3", - "generics_variance_inference" - ] - }, - { - "file": "generics_defaults.py", - "category": "generics", - "status": "PASS", - "caught": 5, - "missed": 0, - "falsePositives": 0, - "codes": [ - "directives_assert_type_2", - "generics_defaults", - "generics_defaults_2", - "generics_defaults_specialization" - ] - }, - { - "file": "generics_defaults_referential.py", - "category": "generics", - "status": "PASS", - "caught": 7, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_defaults_referential", - "generics_defaults_referential_2", - "generics_variance_inference" - ] - }, - { - "file": "generics_defaults_specialization.py", - "category": "generics", - "status": "PASS", - "caught": 3, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility", - "generics_defaults_specialization" - ] - }, - { - "file": "generics_paramspec_basic.py", - "category": "generics", - "status": "PASS", - "caught": 7, - "missed": 0, - "falsePositives": 0, - "codes": [ - "annotations_forward_refs", - "generics_basic" - ] - }, - { - "file": "generics_paramspec_components.py", - "category": "generics", - "status": "PASS", - "caught": 16, - "missed": 0, - "falsePositives": 0, - "codes": [ - "callables_protocol" - ] - }, - { - "file": "generics_paramspec_semantics.py", - "category": "generics", - "status": "PASS", - "caught": 9, - "missed": 0, - "falsePositives": 0, - "codes": [ - "callables_protocol" - ] - }, - { - "file": "generics_paramspec_specialization.py", - "category": "generics", - "status": "PASS", - "caught": 5, - "missed": 0, - "falsePositives": 0, - "codes": [ - "callables_protocol", - "generics_defaults_specialization" - ] - }, - { - "file": "generics_scoping.py", - "category": "generics", - "status": "PASS", - "caught": 10, - "missed": 0, - "falsePositives": 0, - "codes": [ - "directives_assert_type_2", - "generics_scoping", - "generics_variance_inference" - ] - }, - { - "file": "generics_self_advanced.py", - "category": "generics", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [] - }, - { - "file": "generics_self_attributes.py", - "category": "generics", - "status": "PASS", - "caught": 2, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_self_attributes" - ] - }, - { - "file": "generics_self_basic.py", - "category": "generics", - "status": "PASS", - "caught": 3, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_self_basic" - ] - }, - { - "file": "generics_self_protocols.py", - "category": "generics", - "status": "PASS", - "caught": 2, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_self_protocols" - ] - }, - { - "file": "generics_self_usage.py", - "category": "generics", - "status": "PASS", - "caught": 11, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_self_basic", - "generics_self_usage" - ] - }, - { - "file": "generics_syntax_compatibility.py", - "category": "generics", - "status": "PASS", - "caught": 2, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_syntax_compatibility" - ] - }, - { - "file": "generics_syntax_declarations.py", - "category": "generics", - "status": "PASS", - "caught": 10, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_basic_2", - "generics_syntax_declarations", - "generics_syntax_declarations_2" - ] - }, - { - "file": "generics_syntax_infer_variance.py", - "category": "generics", - "status": "PASS", - "caught": 18, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_typevartuple_basic", - "generics_variance_inference" - ] - }, - { - "file": "generics_syntax_scoping.py", - "category": "generics", - "status": "PASS", - "caught": 7, - "missed": 0, - "falsePositives": 0, - "codes": [ - "aliases_implicit", - "generics_syntax_scoping" - ] - }, - { - "file": "generics_type_erasure.py", - "category": "generics", - "status": "PASS", - "caught": 7, - "missed": 0, - "falsePositives": 0, - "codes": [ - "constructors_call_init", - "generics_type_erasure" - ] - }, - { - "file": "generics_typevartuple_args.py", - "category": "generics", - "status": "PASS", - "caught": 8, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_typevartuple_args" - ] - }, - { - "file": "generics_typevartuple_basic.py", - "category": "generics", - "status": "PASS", - "caught": 13, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_typevartuple_args", - "generics_typevartuple_basic", - "generics_typevartuple_basic_2", - "generics_typevartuple_basic_3", - "generics_typevartuple_specialization" - ] - }, - { - "file": "generics_typevartuple_callable.py", - "category": "generics", - "status": "PASS", - "caught": 1, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_typevartuple_callable" - ] - }, - { - "file": "generics_typevartuple_concat.py", - "category": "generics", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [] - }, - { - "file": "generics_typevartuple_overloads.py", - "category": "generics", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [] - }, - { - "file": "generics_typevartuple_specialization.py", - "category": "generics", - "status": "PASS", - "caught": 6, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_typevartuple_specialization", - "generics_typevartuple_specialization_2", - "generics_variance_inference" - ] - }, - { - "file": "generics_typevartuple_unpack.py", - "category": "generics", - "status": "PASS", - "caught": 1, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_typevartuple_unpack" - ] - }, - { - "file": "generics_upper_bound.py", - "category": "generics", - "status": "PASS", - "caught": 3, - "missed": 0, - "falsePositives": 0, - "codes": [ - "directives_assert_type_2", - "generics_basic", - "generics_typevartuple_basic", - "generics_upper_bound" - ] - }, - { - "file": "generics_variance.py", - "category": "generics", - "status": "PASS", - "caught": 9, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_typevartuple_basic", - "generics_variance" - ] - }, - { - "file": "generics_variance_inference.py", - "category": "generics", - "status": "PASS", - "caught": 23, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_variance_inference" - ] - }, - { - "file": "historical_positional.py", - "category": "historical", - "status": "PASS", - "caught": 4, - "missed": 0, - "falsePositives": 0, - "codes": [ - "historical_positional" - ] - }, - { - "file": "literals_interactions.py", - "category": "literals", - "status": "PASS", - "caught": 4, - "missed": 0, - "falsePositives": 0, - "codes": [ - "calls_argument_type", - "tuples_index_2" - ] - }, - { - "file": "literals_literalstring.py", - "category": "literals", - "status": "PASS", - "caught": 9, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility", - "generics_upper_bound_2", - "literals_literalstring", - "literals_parameterizations", - "literals_semantics_2" - ] - }, - { - "file": "literals_parameterizations.py", - "category": "literals", - "status": "PASS", - "caught": 17, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility", - "generics_scoping", - "generics_variance_inference", - "literals_parameterizations", - "literals_parameterizations_2", - "literals_semantics_2" - ] - }, - { - "file": "literals_semantics.py", - "category": "literals", - "status": "PASS", - "caught": 4, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility", - "literals_semantics_2" - ] - }, - { - "file": "namedtuples_define_class.py", - "category": "namedtuples", - "status": "PASS", - "caught": 15, - "missed": 0, - "falsePositives": 0, - "codes": [ - "constructors_call_init", - "namedtuples_define_class", - "namedtuples_usage" - ] - }, - { - "file": "namedtuples_define_functional.py", - "category": "namedtuples", - "status": "PASS", - "caught": 9, - "missed": 0, - "falsePositives": 0, - "codes": [ - "calls_argument_count", - "namedtuples_define_functional" - ] - }, - { - "file": "namedtuples_type_compat.py", - "category": "namedtuples", - "status": "PASS", - "caught": 2, - "missed": 0, - "falsePositives": 0, - "codes": [ - "namedtuples_type_compat" - ] - }, - { - "file": "namedtuples_usage.py", - "category": "namedtuples", - "status": "PASS", - "caught": 8, - "missed": 0, - "falsePositives": 0, - "codes": [ - "namedtuples_usage" - ] - }, - { - "file": "narrowing_typeguard.py", - "category": "narrowing", - "status": "PASS", - "caught": 4, - "missed": 0, - "falsePositives": 0, - "codes": [ - "narrowing_typeguard", - "narrowing_typeis" - ] - }, - { - "file": "narrowing_typeis.py", - "category": "narrowing", - "status": "PASS", - "caught": 9, - "missed": 0, - "falsePositives": 0, - "codes": [ - "narrowing_typeguard", - "narrowing_typeis", - "narrowing_typeis_2" - ] - }, - { - "file": "overloads_basic.py", - "category": "overloads", - "status": "PASS", - "caught": 1, - "missed": 0, - "falsePositives": 0, - "codes": [ - "overloads_basic" - ] - }, - { - "file": "overloads_consistency.py", - "category": "overloads", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [ - "overloads_consistency_3" - ] - }, - { - "file": "overloads_definitions.py", - "category": "overloads", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [ - "classes_override_3", - "overloads_consistency_2", - "overloads_definitions", - "qualifiers_final_decorator" - ] - }, - { - "file": "overloads_definitions_stub.pyi", - "category": "overloads", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [ - "classes_override_3", - "overloads_consistency_2", - "overloads_definitions", - "qualifiers_final_decorator" - ] - }, - { - "file": "overloads_evaluation.py", - "category": "overloads", - "status": "PASS", - "caught": 4, - "missed": 0, - "falsePositives": 0, - "codes": [ - "calls_argument_count", - "calls_argument_type", - "overloads_evaluation" - ] - }, - { - "file": "protocols_class_objects.py", - "category": "protocols", - "status": "PASS", - "caught": 8, - "missed": 0, - "falsePositives": 0, - "codes": [ - "protocols_class_objects_2", - "protocols_explicit" - ] - }, - { - "file": "protocols_definition.py", - "category": "protocols", - "status": "PASS", - "caught": 21, - "missed": 0, - "falsePositives": 0, - "codes": [ - "classes_classvar", - "protocols_definition", - "protocols_definition_2" - ] - }, - { - "file": "protocols_explicit.py", - "category": "protocols", - "status": "PASS", - "caught": 6, - "missed": 0, - "falsePositives": 0, - "codes": [ - "protocols_explicit", - "protocols_explicit_2", - "protocols_explicit_3", - "protocols_subtyping" - ] - }, - { - "file": "protocols_generic.py", - "category": "protocols", - "status": "PASS", - "caught": 9, - "missed": 0, - "falsePositives": 0, - "codes": [ - "generics_variance_inference", - "protocols_generic" - ] - }, - { - "file": "protocols_merging.py", - "category": "protocols", - "status": "PASS", - "caught": 6, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility", - "protocols_definition_2", - "protocols_explicit", - "protocols_merging" - ] - }, - { - "file": "protocols_modules.py", - "category": "protocols", - "status": "PASS", - "caught": 3, - "missed": 0, - "falsePositives": 0, - "codes": [ - "protocols_modules" - ] - }, - { - "file": "protocols_recursive.py", - "category": "protocols", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [] - }, - { - "file": "protocols_runtime_checkable.py", - "category": "protocols", - "status": "PASS", - "caught": 6, - "missed": 0, - "falsePositives": 0, - "codes": [ - "protocols_runtime_checkable", - "protocols_runtime_checkable_2" - ] - }, - { - "file": "protocols_self.py", - "category": "protocols", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [] - }, - { - "file": "protocols_subtyping.py", - "category": "protocols", - "status": "PASS", - "caught": 7, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility", - "protocols_explicit" - ] - }, - { - "file": "protocols_variance.py", - "category": "protocols", - "status": "PASS", - "caught": 5, - "missed": 0, - "falsePositives": 0, - "codes": [ - "protocols_variance", - "protocols_variance_2" - ] - }, - { - "file": "qualifiers_annotated.py", - "category": "qualifiers", - "status": "PASS", - "caught": 20, - "missed": 0, - "falsePositives": 0, - "codes": [ - "qualifiers_annotated", - "qualifiers_annotated_2" - ] - }, - { - "file": "qualifiers_final_annotation.py", - "category": "qualifiers", - "status": "PASS", - "caught": 26, - "missed": 0, - "falsePositives": 0, - "codes": [ - "calls_argument_count", - "classes_classvar", - "namedtuples_define_functional", - "qualifiers_final_annotation", - "qualifiers_final_annotation_2" - ] - }, - { - "file": "qualifiers_final_decorator.py", - "category": "qualifiers", - "status": "PASS", - "caught": 3, - "missed": 0, - "falsePositives": 0, - "codes": [ - "overloads_consistency_2", - "qualifiers_final_decorator" - ] - }, - { - "file": "specialtypes_any.py", - "category": "specialtypes", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [] - }, - { - "file": "specialtypes_never.py", - "category": "specialtypes", - "status": "PASS", - "caught": 3, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility", - "specialtypes_never", - "specialtypes_never_2" - ] - }, - { - "file": "specialtypes_none.py", - "category": "specialtypes", - "status": "PASS", - "caught": 3, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility", - "calls_argument_type" - ] - }, - { - "file": "specialtypes_promotions.py", - "category": "specialtypes", - "status": "PASS", - "caught": 1, - "missed": 0, - "falsePositives": 0, - "codes": [ - "specialtypes_promotions" - ] - }, - { - "file": "specialtypes_type.py", - "category": "specialtypes", - "status": "PASS", - "caught": 9, - "missed": 0, - "falsePositives": 0, - "codes": [ - "callables_annotation", - "generics_defaults_specialization", - "specialtypes_type" - ] - }, - { - "file": "tuples_type_compat.py", - "category": "tuples", - "status": "PASS", - "caught": 16, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility", - "directives_assert_type_2", - "tuples_type_compat" - ] - }, - { - "file": "tuples_type_form.py", - "category": "tuples", - "status": "PASS", - "caught": 11, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility", - "tuples_type_form", - "tuples_type_form_2" - ] - }, - { - "file": "tuples_unpacked.py", - "category": "tuples", - "status": "PASS", - "caught": 4, - "missed": 0, - "falsePositives": 0, - "codes": [ - "tuples_type_form" - ] - }, - { - "file": "typeddicts_alt_syntax.py", - "category": "typeddicts", - "status": "PASS", - "caught": 4, - "missed": 0, - "falsePositives": 0, - "codes": [ - "typeddicts_alt_syntax" - ] - }, - { - "file": "typeddicts_class_syntax.py", - "category": "typeddicts", - "status": "PASS", - "caught": 4, - "missed": 0, - "falsePositives": 0, - "codes": [ - "typeddicts_class_syntax", - "typeddicts_class_syntax_2", - "typeddicts_extra_items" - ] - }, - { - "file": "typeddicts_extra_items.py", - "category": "typeddicts", - "status": "PASS", - "caught": 22, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility", - "callables_kwargs", - "typeddicts_extra_items", - "typeddicts_operations" - ] - }, - { - "file": "typeddicts_final.py", - "category": "typeddicts", - "status": "PASS", - "caught": 0, - "missed": 0, - "falsePositives": 0, - "codes": [] - }, - { - "file": "typeddicts_inheritance.py", - "category": "typeddicts", - "status": "PASS", - "caught": 2, - "missed": 0, - "falsePositives": 0, - "codes": [ - "typeddicts_inheritance" - ] - }, - { - "file": "typeddicts_operations.py", - "category": "typeddicts", - "status": "PASS", - "caught": 11, - "missed": 0, - "falsePositives": 0, - "codes": [ - "typeddicts_operations" - ] - }, - { - "file": "typeddicts_readonly.py", - "category": "typeddicts", - "status": "PASS", - "caught": 6, - "missed": 0, - "falsePositives": 0, - "codes": [ - "typeddicts_readonly" - ] - }, - { - "file": "typeddicts_readonly_consistency.py", - "category": "typeddicts", - "status": "PASS", - "caught": 7, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility" - ] - }, - { - "file": "typeddicts_readonly_inheritance.py", - "category": "typeddicts", - "status": "PASS", - "caught": 11, - "missed": 0, - "falsePositives": 0, - "codes": [ - "typeddicts_inheritance", - "typeddicts_operations", - "typeddicts_readonly" - ] - }, - { - "file": "typeddicts_readonly_kwargs.py", - "category": "typeddicts", - "status": "PASS", - "caught": 1, - "missed": 0, - "falsePositives": 0, - "codes": [ - "typeddicts_readonly" - ] - }, - { - "file": "typeddicts_readonly_update.py", - "category": "typeddicts", - "status": "PASS", - "caught": 1, - "missed": 0, - "falsePositives": 0, - "codes": [ - "typeddicts_operations", - "typeddicts_readonly" - ] - }, - { - "file": "typeddicts_required.py", - "category": "typeddicts", - "status": "PASS", - "caught": 4, - "missed": 0, - "falsePositives": 0, - "codes": [ - "typeddicts_required" - ] - }, - { - "file": "typeddicts_type_consistency.py", - "category": "typeddicts", - "status": "PASS", - "caught": 9, - "missed": 0, - "falsePositives": 0, - "codes": [ - "typeddicts_operations" - ] - }, - { - "file": "typeddicts_usage.py", - "category": "typeddicts", - "status": "PASS", - "caught": 5, - "missed": 0, - "falsePositives": 0, - "codes": [ - "typeddicts_operations", - "typeddicts_usage" - ] - }, - { - "file": "typeforms_typeform.py", - "category": "typeforms", - "status": "PASS", - "caught": 16, - "missed": 0, - "falsePositives": 0, - "codes": [ - "assignment_compatibility", - "classes_classvar" - ] - } - ] -} diff --git a/website/src/_data/examples.js b/website/src/_data/examples.js deleted file mode 100644 index 3812058d0..000000000 --- a/website/src/_data/examples.js +++ /dev/null @@ -1,17 +0,0 @@ -// 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-0014), 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-\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/i18n.json b/website/src/_data/i18n.json deleted file mode 100644 index fbc20aeb6..000000000 --- a/website/src/_data/i18n.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "en": { - "nav": { - "docs": "Docs", - "rules": "Rules", - "blog": "Blog", - "discord": "Discord", - "github": "GitHub", - "getStarted": "Get Started" - }, - "blog": { - "title": "Blog", - "eyebrow": "The Basilisk Journal", - "headline": "Ideas for safer Python.", - "subtitle": "Updates, announcements, and deep-dives from the Basilisk project.", - "description": "Updates, announcements, and deep-dives from the Basilisk project.", - "tags": "Tags", - "categories": "Categories", - "tagsTitle": "Tags", - "tagsDescription": "Browse blog posts by tag.", - "categoriesTitle": "Categories", - "categoriesDescription": "Browse blog posts by category.", - "readMore": "Read more", - "publishedOn": "Published on", - "browse": "Browse the blog", - "allStories": "All stories", - "latestEyebrow": "From the project", - "latest": "Latest writing", - "article": "article", - "articles": "articles", - "categoryResultsDescription": "Writing collected under this category.", - "tagResultsDescription": "Writing collected under this topic.", - "noTags": "No tags yet. Check back soon!", - "noCategories": "No categories yet. Check back soon!" - }, - "docs": { - "onThisPage": "On this page", - "editOnGithub": "Edit on GitHub", - "nextPage": "Next", - "prevPage": "Previous" - }, - "footer": { - "madeWith": "Built with", - "license": "MIT License", - "copyright": "The Basilisk Project", - "product": "Product", - "community": "Community", - "legal": "Legal", - "documentation": "Documentation", - "rulesReference": "Rules Reference", - "blog": "Blog", - "authors": "Authors", - "github": "GitHub", - "issues": "Issues", - "discord": "Discord", - "discussions": "Discussions", - "mit": "MIT" - } - }, - "zh": { - "nav": { - "docs": "文档", - "rules": "规则", - "blog": "博客", - "discord": "Discord", - "github": "GitHub", - "getStarted": "立即开始" - }, - "blog": { - "title": "博客", - "eyebrow": "Basilisk 技术期刊", - "headline": "探索更安全的 Python。", - "subtitle": "来自 Basilisk 项目的更新、公告和深度解析。", - "description": "来自 Basilisk 项目的更新、公告和深度解析。", - "tags": "标签", - "categories": "分类", - "tagsTitle": "标签", - "tagsDescription": "按标签浏览博客文章。", - "categoriesTitle": "分类", - "categoriesDescription": "按分类浏览博客文章。", - "readMore": "阅读更多", - "publishedOn": "发布于", - "browse": "浏览博客", - "allStories": "全部文章", - "latestEyebrow": "项目动态", - "latest": "最新文章", - "article": "篇文章", - "articles": "篇文章", - "categoryResultsDescription": "此分类下的全部文章。", - "tagResultsDescription": "此主题下的全部文章。", - "noTags": "暂无标签。", - "noCategories": "暂无分类。" - }, - "docs": { - "onThisPage": "本页内容", - "editOnGithub": "在 GitHub 上编辑", - "nextPage": "下一页", - "prevPage": "上一页" - }, - "footer": { - "madeWith": "构建于", - "license": "MIT 许可证", - "copyright": "Basilisk 项目", - "product": "产品", - "community": "社区", - "legal": "法律", - "documentation": "文档", - "rulesReference": "规则参考", - "blog": "博客", - "authors": "作者", - "github": "GitHub", - "issues": "问题", - "discord": "Discord", - "discussions": "讨论", - "mit": "MIT 许可证" - } - } -} diff --git a/website/src/_data/navigation.json b/website/src/_data/navigation.json index 53af2ed38..f9a72c6fc 100644 --- a/website/src/_data/navigation.json +++ b/website/src/_data/navigation.json @@ -1,92 +1,9 @@ { "main": [ - { "key": "playground", "text": "Playground", "url": "/playground/" }, - { "key": "docs", "text": "Docs", "url": "/docs/" }, - { "key": "rules", "text": "Rules", "url": "/docs/rules/" }, - { "key": "blog", "text": "Blog", "url": "/blog/" }, - { "key": "discord", "text": "Discord", "url": "https://discord.gg/4wBDSGEZQd", "external": true }, { "key": "github", "text": "GitHub", "url": "https://github.com/Nimblesite/Basilisk", "external": true } ], - "docs": [ - { - "title": "Introduction", - "titleZh": "简介", - "url": "/docs/" - }, - { - "title": "Getting started", - "titleZh": "开始使用", - "items": [ - { - "title": "Installation", - "titleZh": "安装", - "children": [ - { "title": "Overview", "titleZh": "概览", "url": "/docs/installation/" }, - { "title": "VS Code & Cursor", "titleZh": "VS Code 与 Cursor", "url": "/docs/install-vscode/" }, - { "title": "Zed", "titleZh": "Zed", "url": "/docs/install-zed/" }, - { "title": "Neovim", "titleZh": "Neovim", "url": "/docs/install-neovim/" }, - { "title": "CLI & package managers", "titleZh": "CLI 与包管理器", "url": "/docs/install-cli/" } - ] - }, - { "title": "Quick start", "titleZh": "快速开始", "url": "/docs/quick-start/" } - ] - }, - { - "title": "Set up & adopt", - "titleZh": "配置与采用", - "items": [ - { "title": "Configuration", "titleZh": "配置", "url": "/docs/configuration/" }, - { "title": "Migration guide", "titleZh": "迁移指南", "url": "/docs/migration/" } - ] - }, - { - "title": "Type checking", - "titleZh": "类型检查", - "items": [ - { "title": "Rules", "titleZh": "规则", "url": "/docs/rules/", "kind": "rules" }, - { "title": "Conformance", "titleZh": "规范符合性", "url": "/docs/conformance/" }, - { "title": "Type checker comparison", "titleZh": "类型检查器对比", "url": "/docs/comparison/" } - ] - }, - { - "title": "Developer tools", - "titleZh": "开发者工具", - "items": [ - { "title": "Refactoring", "titleZh": "重构", "url": "/docs/refactoring/" }, - { "title": "Debugging", "titleZh": "调试", "url": "/docs/debugging/" }, - { "title": "Profiler", "titleZh": "性能分析", "url": "/docs/profiler/" } - ] - }, - { - "title": "Project", - "titleZh": "项目", - "items": [ - { "title": "Benchmarks", "titleZh": "性能基准", "url": "/docs/benchmarks/" }, - { "title": "Releases", "titleZh": "版本发布", "url": "/docs/releases/" } - ] - } - ], + "docs": [], "footer": [ - { - "key": "product", - "title": "Product", - "items": [ - { "key": "documentation", "text": "Documentation", "url": "/docs/" }, - { "key": "rulesReference", "text": "Rules Reference", "url": "/docs/rules/" }, - { "key": "blog", "text": "Blog", "url": "/blog/" }, - { "key": "authors", "text": "Authors", "url": "/authors/" } - ] - }, - { - "key": "community", - "title": "Community", - "items": [ - { "key": "github", "text": "GitHub", "url": "https://github.com/Nimblesite/Basilisk" }, - { "key": "issues", "text": "Issues", "url": "https://github.com/Nimblesite/Basilisk/issues" }, - { "key": "discord", "text": "Discord", "url": "https://discord.gg/4wBDSGEZQd" }, - { "key": "discussions", "text": "Discussions", "url": "https://github.com/Nimblesite/Basilisk/discussions" } - ] - }, { "key": "legal", "title": "Legal", diff --git a/website/src/_data/releases.js b/website/src/_data/releases.js deleted file mode 100644 index 0c693c445..000000000 --- a/website/src/_data/releases.js +++ /dev/null @@ -1,150 +0,0 @@ -// Eleventy global data: the Basilisk GitHub Releases, fetched FRESH at every -// build from the public GitHub REST API — never hand-maintained. This mirrors -// the build-time data pattern of _data/conformance.js and _data/benchmarks.js: -// everything the /docs/releases/ page shows is whatever the API returns at build -// time (tag, title, date, release notes rendered from the release's markdown -// body, and downloadable assets). -// -// Drafts are excluded (not yet published). Prereleases are kept and badged. -// -// The build NEVER fails on a network/API error: exactly like conformance.js it -// degrades to `{ hasData: false }` and the page renders an empty state linking -// to GitHub, so an offline dev build or a rate-limited CI run still produces a -// valid site. When `GITHUB_TOKEN`/`GH_TOKEN` is present (CI) it is used to raise -// the API rate limit; the public, unauthenticated path works too. -import markdownIt from "markdown-it"; - -const OWNER = "Nimblesite"; -const REPO = "Basilisk"; -const API = `https://api.github.com/repos/${OWNER}/${REPO}/releases?per_page=100`; -const RELEASES_URL = `https://github.com/${OWNER}/${REPO}/releases`; - -// Release notes are authored by the maintainers (trusted), so raw HTML is -// allowed. `breaks: true` matches how GitHub itself renders release bodies. -const md = markdownIt({ html: true, linkify: true, breaks: true }); - -// Withdrawn-claim redaction. Release notes arrive verbatim from GitHub, and some -// historical entries quote the conformance result that has since been retracted. -// [CHKARCH-CONFORMANCE] forbids publishing, quoting, or marketing any conformance -// figure, so rendering those lines unchanged would keep republishing a claim we -// have withdrawn. Any line pairing a conformance subject with a figure is replaced -// by a visible marker — nothing is silently dropped, the marker links to the -// correction, and every release heading still links to the unmodified GitHub -// release so the original wording stays one click away. -const CLAIM_SUBJECT = /conformance|conformant/i; -const CLAIM_FIGURE = /\d+(?:\.\d+)?\s*%|\b\d{1,4}\s*\/\s*\d{1,4}\b|\b\d+\s+false\s+positives?\b/i; -const LIST_MARKER = /^(\s*(?:[-*+]|\d+\.)\s+)/; -const REDACTION = "*[withdrawn conformance claim redacted — see the correction](/docs/conformance/)*"; - -// Replace each claim-bearing line with the marker, preserving its list marker so -// the surrounding changelog structure still renders. -function redactWithdrawnClaims(markdown) { - return markdown - .split(/\r?\n/) - .map((line) => { - if (!CLAIM_SUBJECT.test(line) || !CLAIM_FIGURE.test(line)) return line; - const marker = line.match(LIST_MARKER); - return `${marker ? marker[1] : ""}${REDACTION}`; - }) - .join("\n"); -} - -const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - -// "2026-06-23T10:16:43Z" -> "Jun 23, 2026". UTC getters keep the output -// deterministic regardless of the build machine's timezone. -function formatDate(iso) { - if (!iso) return null; - const date = new Date(iso); - return Number.isNaN(date.getTime()) - ? iso - : `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}, ${date.getUTCFullYear()}`; -} - -// Bytes -> "1.2 MB" style, base-1024. -function formatBytes(bytes) { - if (!Number.isFinite(bytes) || bytes <= 0) return "0 B"; - const units = ["B", "KB", "MB", "GB"]; - const exp = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); - const value = bytes / 1024 ** exp; - return `${exp === 0 ? value : Math.round(value * 10) / 10} ${units[exp]}`; -} - -// Pull the `rel="next"` URL out of a GitHub `Link` response header (string -// splitting, no regex). Returns null when there is no next page. -function nextPageUrl(linkHeader) { - if (!linkHeader) return null; - for (const part of linkHeader.split(",")) { - const [target, ...attrs] = part.split(";"); - if (attrs.some((attr) => attr.trim() === 'rel="next"')) { - return target.trim().slice(1, -1); // strip the surrounding < > - } - } - return null; -} - -async function fetchAllReleases() { - const headers = { - Accept: "application/vnd.github+json", - "User-Agent": `${OWNER}-${REPO}-website-build`, - "X-GitHub-Api-Version": "2022-11-28", - }; - const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN; - if (token) headers.Authorization = `Bearer ${token}`; - - const releases = []; - let url = API; - while (url) { - const response = await fetch(url, { headers }); - if (!response.ok) { - throw new Error(`GitHub API ${response.status} ${response.statusText}`); - } - releases.push(...(await response.json())); - url = nextPageUrl(response.headers.get("link")); - } - return releases; -} - -// Shape one API release into the flat record the template renders. -function toRecord(release) { - return { - tag: release.tag_name, - name: release.name || release.tag_name, - url: release.html_url, - date: formatDate(release.published_at || release.created_at), - dateIso: release.published_at || release.created_at, - prerelease: release.prerelease === true, - bodyHtml: release.body ? md.render(redactWithdrawnClaims(release.body)) : "", - assets: (release.assets || []).map((asset) => ({ - name: asset.name, - url: asset.browser_download_url, - size: formatBytes(asset.size), - downloads: asset.download_count || 0, - })), - }; -} - -const EMPTY = { hasData: false, releasesUrl: RELEASES_URL, count: 0, releases: [] }; - -export default async function () { - try { - const published = (await fetchAllReleases()) - .filter((release) => release.draft !== true) - .sort((a, b) => new Date(b.published_at || b.created_at) - new Date(a.published_at || a.created_at)) - .map(toRecord); - - if (!published.length) return EMPTY; - - return { - hasData: true, - releasesUrl: RELEASES_URL, - count: published.length, - latest: published[0], - releases: published, - }; - } catch (error) { - // Degrade gracefully — a broken build is worse than a stale releases page. - console.warn(`⚠ releases.js: ${error.message} — rendering empty state`); - return EMPTY; - } -} diff --git a/website/src/_data/retiredUrls.json b/website/src/_data/retiredUrls.json new file mode 100644 index 000000000..286abc833 --- /dev/null +++ b/website/src/_data/retiredUrls.json @@ -0,0 +1,299 @@ +[ + "/authors/", + "/authors/basilisk-team/", + "/authors/christian-findlay/", + "/blog/", + "/blog/ai-agents-write-python-type-checking-guardrail/", + "/blog/basilisk-037-python-type-checker-configuration/", + "/blog/basilisk-100-percent-python-typing-conformance/", + "/blog/categories/", + "/blog/categories/announcements/", + "/blog/categories/deep-dives/", + "/blog/free-threaded-python-why-type-checking-matters-more/", + "/blog/introducing-basilisk/", + "/blog/openai-acquires-astral-what-it-means-for-basilisk/", + "/blog/python-315-typeform-fastapi-pydantic-annotations/", + "/blog/tags/", + "/blog/tags/ai-coding-assistants/", + "/blog/tags/fastapi/", + "/blog/tags/pydantic/", + "/blog/tags/python-performance/", + "/blog/tags/python-tooling/", + "/blog/tags/python-typing/", + "/blog/type-manipulation-pep-827/", + "/docs/", + "/docs/benchmarks/", + "/docs/comparison/", + "/docs/configuration/", + "/docs/conformance/", + "/docs/debugging/", + "/docs/formatting/", + "/docs/install-cli/", + "/docs/install-neovim/", + "/docs/install-vscode/", + "/docs/install-zed/", + "/docs/installation/", + "/docs/migration/", + "/docs/profiler/", + "/docs/quick-start/", + "/docs/refactoring/", + "/docs/releases/", + "/docs/rules/", + "/docs/rules/basilisk/dependencies/", + "/docs/rules/basilisk/imports/", + "/docs/rules/basilisk/redundancy/", + "/docs/rules/basilisk/strictness/", + "/docs/rules/basilisk/stubs/", + "/docs/rules/basilisk/style/", + "/docs/rules/basilisk/suppressions/", + "/docs/rules/missing-annotations/", + "/docs/rules/pep/aliases/", + "/docs/rules/pep/annotations/", + "/docs/rules/pep/callables/", + "/docs/rules/pep/classes/", + "/docs/rules/pep/constructors/", + "/docs/rules/pep/core/", + "/docs/rules/pep/dataclasses/", + "/docs/rules/pep/directives/", + "/docs/rules/pep/enums/", + "/docs/rules/pep/generics/", + "/docs/rules/pep/historical/", + "/docs/rules/pep/literals/", + "/docs/rules/pep/namedtuples/", + "/docs/rules/pep/narrowing/", + "/docs/rules/pep/overloads/", + "/docs/rules/pep/protocols/", + "/docs/rules/pep/qualifiers/", + "/docs/rules/pep/specialtypes/", + "/docs/rules/pep/tuples/", + "/docs/rules/pep/typeddicts/", + "/docs/rules/type-safety/", + "/errors/", + "/errors/BSK-0001/", + "/errors/BSK-0002/", + "/errors/BSK-0003/", + "/errors/BSK-0004/", + "/errors/BSK-0005/", + "/errors/BSK-0011/", + "/errors/BSK-0012/", + "/errors/BSK-0013/", + "/errors/BSK-0014/", + "/errors/BSK-0025/", + "/errors/BSK-0040/", + "/errors/BSK-0050/", + "/errors/BSK-0060/", + "/errors/BSK-0061/", + "/errors/BSK-0062/", + "/errors/BSK-0063/", + "/errors/BSK-0152/", + "/errors/aliases_implicit/", + "/errors/aliases_newtype/", + "/errors/aliases_recursive/", + "/errors/aliases_type_statement/", + "/errors/aliases_typealiastype/", + "/errors/annotations_forward_refs/", + "/errors/annotations_generators/", + "/errors/annotations_generators_2/", + "/errors/annotations_typeexpr/", + "/errors/assignment_compatibility/", + "/errors/callables_annotation/", + "/errors/callables_kwargs/", + "/errors/callables_protocol/", + "/errors/callables_protocol_2/", + "/errors/callables_subtyping/", + "/errors/calls_argument_count/", + "/errors/calls_argument_type/", + "/errors/classes_classvar/", + "/errors/classes_override/", + "/errors/classes_override_2/", + "/errors/classes_override_3/", + "/errors/constructors_call_init/", + "/errors/constructors_call_new/", + "/errors/constructors_call_type/", + "/errors/constructors_callable/", + "/errors/dataclasses_frozen/", + "/errors/dataclasses_hash/", + "/errors/dataclasses_inheritance/", + "/errors/dataclasses_kwonly/", + "/errors/dataclasses_match_args/", + "/errors/dataclasses_order/", + "/errors/dataclasses_postinit/", + "/errors/dataclasses_slots/", + "/errors/dataclasses_transform_class/", + "/errors/dataclasses_transform_meta/", + "/errors/dataclasses_usage/", + "/errors/dict_key_hashable/", + "/errors/directives_assert_type/", + "/errors/directives_assert_type_2/", + "/errors/directives_cast/", + "/errors/directives_deprecated/", + "/errors/directives_disjoint_base/", + "/errors/directives_reveal_type/", + "/errors/directives_version_platform/", + "/errors/enums_behaviors/", + "/errors/enums_definition/", + "/errors/enums_expansion/", + "/errors/enums_member_values/", + "/errors/enums_members/", + "/errors/enums_members_2/", + "/errors/generics_base_class/", + "/errors/generics_base_class_2/", + "/errors/generics_base_class_3/", + "/errors/generics_basic/", + "/errors/generics_basic_2/", + "/errors/generics_basic_3/", + "/errors/generics_defaults/", + "/errors/generics_defaults_2/", + "/errors/generics_defaults_referential/", + "/errors/generics_defaults_referential_2/", + "/errors/generics_defaults_specialization/", + "/errors/generics_scoping/", + "/errors/generics_self_attributes/", + "/errors/generics_self_basic/", + "/errors/generics_self_protocols/", + "/errors/generics_self_usage/", + "/errors/generics_syntax_compatibility/", + "/errors/generics_syntax_declarations/", + "/errors/generics_syntax_declarations_2/", + "/errors/generics_syntax_scoping/", + "/errors/generics_type_erasure/", + "/errors/generics_typevartuple_args/", + "/errors/generics_typevartuple_basic/", + "/errors/generics_typevartuple_basic_2/", + "/errors/generics_typevartuple_basic_3/", + "/errors/generics_typevartuple_callable/", + "/errors/generics_typevartuple_specialization/", + "/errors/generics_typevartuple_specialization_2/", + "/errors/generics_typevartuple_unpack/", + "/errors/generics_upper_bound/", + "/errors/generics_upper_bound_2/", + "/errors/generics_variance/", + "/errors/generics_variance_inference/", + "/errors/historical_positional/", + "/errors/imports_missing_name/", + "/errors/imports_module_attribute/", + "/errors/imports_unresolved/", + "/errors/literals_literalstring/", + "/errors/literals_parameterizations/", + "/errors/literals_parameterizations_2/", + "/errors/literals_semantics/", + "/errors/literals_semantics_2/", + "/errors/match_exhaustiveness/", + "/errors/namedtuples_define_class/", + "/errors/namedtuples_define_functional/", + "/errors/namedtuples_type_compat/", + "/errors/namedtuples_usage/", + "/errors/names_unbound/", + "/errors/names_undefined/", + "/errors/narrowing_typeguard/", + "/errors/narrowing_typeis/", + "/errors/narrowing_typeis_2/", + "/errors/overloads_basic/", + "/errors/overloads_consistency/", + "/errors/overloads_consistency_2/", + "/errors/overloads_consistency_3/", + "/errors/overloads_definitions/", + "/errors/overloads_evaluation/", + "/errors/protocols_class_objects/", + "/errors/protocols_class_objects_2/", + "/errors/protocols_definition/", + "/errors/protocols_definition_2/", + "/errors/protocols_explicit/", + "/errors/protocols_explicit_2/", + "/errors/protocols_explicit_3/", + "/errors/protocols_generic/", + "/errors/protocols_merging/", + "/errors/protocols_modules/", + "/errors/protocols_runtime_checkable/", + "/errors/protocols_runtime_checkable_2/", + "/errors/protocols_subtyping/", + "/errors/protocols_variance/", + "/errors/protocols_variance_2/", + "/errors/qualifiers_annotated/", + "/errors/qualifiers_annotated_2/", + "/errors/qualifiers_final_annotation/", + "/errors/qualifiers_final_annotation_2/", + "/errors/qualifiers_final_decorator/", + "/errors/returns_compatibility/", + "/errors/returns_compatibility_2/", + "/errors/specialtypes_never/", + "/errors/specialtypes_never_2/", + "/errors/specialtypes_promotions/", + "/errors/specialtypes_type/", + "/errors/tuples_index/", + "/errors/tuples_index_2/", + "/errors/tuples_type_compat/", + "/errors/tuples_type_form/", + "/errors/tuples_type_form_2/", + "/errors/typeddicts_alt_syntax/", + "/errors/typeddicts_class_syntax/", + "/errors/typeddicts_class_syntax_2/", + "/errors/typeddicts_extra_items/", + "/errors/typeddicts_inheritance/", + "/errors/typeddicts_operations/", + "/errors/typeddicts_readonly/", + "/errors/typeddicts_required/", + "/errors/typeddicts_usage/", + "/errors/typeshed_source_license_changed/", + "/errors/typeshed_source_unpinned/", + "/errors/typeshed_source_user_managed/", + "/errors/version_target_syntax/", + "/playground/", + "/zh/", + "/zh/blog/", + "/zh/blog/basilisk-100-percent-python-typing-conformance/", + "/zh/blog/categories/", + "/zh/blog/categories/announcements/", + "/zh/blog/categories/deep-dives/", + "/zh/blog/free-threaded-python-why-type-checking-matters-more/", + "/zh/blog/introducing-basilisk/", + "/zh/blog/openai-acquires-astral-what-it-means-for-basilisk/", + "/zh/blog/tags/", + "/zh/blog/tags/python-performance/", + "/zh/blog/tags/python-tooling/", + "/zh/blog/tags/python-typing/", + "/zh/docs/", + "/zh/docs/comparison/", + "/zh/docs/configuration/", + "/zh/docs/conformance/", + "/zh/docs/debugging/", + "/zh/docs/install-cli/", + "/zh/docs/install-neovim/", + "/zh/docs/install-vscode/", + "/zh/docs/install-zed/", + "/zh/docs/installation/", + "/zh/docs/migration/", + "/zh/docs/profiler/", + "/zh/docs/quick-start/", + "/zh/docs/refactoring/", + "/zh/docs/rules/", + "/zh/docs/rules/basilisk/dependencies/", + "/zh/docs/rules/basilisk/imports/", + "/zh/docs/rules/basilisk/redundancy/", + "/zh/docs/rules/basilisk/strictness/", + "/zh/docs/rules/basilisk/stubs/", + "/zh/docs/rules/basilisk/style/", + "/zh/docs/rules/basilisk/suppressions/", + "/zh/docs/rules/missing-annotations/", + "/zh/docs/rules/pep/aliases/", + "/zh/docs/rules/pep/annotations/", + "/zh/docs/rules/pep/callables/", + "/zh/docs/rules/pep/classes/", + "/zh/docs/rules/pep/constructors/", + "/zh/docs/rules/pep/core/", + "/zh/docs/rules/pep/dataclasses/", + "/zh/docs/rules/pep/directives/", + "/zh/docs/rules/pep/enums/", + "/zh/docs/rules/pep/generics/", + "/zh/docs/rules/pep/historical/", + "/zh/docs/rules/pep/literals/", + "/zh/docs/rules/pep/namedtuples/", + "/zh/docs/rules/pep/narrowing/", + "/zh/docs/rules/pep/overloads/", + "/zh/docs/rules/pep/protocols/", + "/zh/docs/rules/pep/qualifiers/", + "/zh/docs/rules/pep/specialtypes/", + "/zh/docs/rules/pep/tuples/", + "/zh/docs/rules/pep/typeddicts/", + "/zh/docs/rules/type-safety/" +] diff --git a/website/src/_data/ruleStats.js b/website/src/_data/ruleStats.js deleted file mode 100644 index d1138d7ac..000000000 --- a/website/src/_data/ruleStats.js +++ /dev/null @@ -1,33 +0,0 @@ -// 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; -// Provenance split — the axis that actually matters on the homepage: the PEP -// typing-spec rules the conformance suite grades (on by default) are counted -// SEPARATELY from Basilisk's opt-in house-style rules (off by default). Never -// lump the two into one headline number. Provenance is the checker's own -// `opt_in_spec` signal, threaded through rules.json by gen_rules_reference.py. -const optIn = rules.filter((rule) => rule.provenance === "basilisk").length; -const pep = rules.length - optIn; -// The opt-in rules that enforce *real* typing beyond the spec — the ones the -// checker tags `strictness` (require an annotation on every parameter, return, -// variable, vararg, and attribute; require @override; reject implicit `Any`; -// annotate lambdas). Counted from the checker's own tags, so it can't drift. -const strictness = rules.filter((rule) => - (rule.tags || []).includes("strictness"), -).length; - -export default { - total: rules.length, - errors, - warnings: rules.length - errors, - pep, - optIn, - strictness, -}; diff --git a/website/src/_data/ruleTagGroups.js b/website/src/_data/ruleTagGroups.js deleted file mode 100644 index 25a011ca1..000000000 --- a/website/src/_data/ruleTagGroups.js +++ /dev/null @@ -1,95 +0,0 @@ -import rules from "./rules.json" with { type: "json" }; - -const LABELS = { - core: ["Cross-cutting core", "跨领域核心规则"], - aliases: ["Type aliases", "类型别名"], - annotations: ["Annotations", "类型注解"], - callables: ["Callables", "可调用对象"], - classes: ["Classes", "类"], - constructors: ["Constructors", "构造器"], - dataclasses: ["Dataclasses", "数据类"], - directives: ["Typing directives", "类型指令"], - enums: ["Enums", "枚举"], - exceptions: ["Exceptions", "异常"], - generics: ["Generics", "泛型"], - historical: ["Historical behavior", "历史行为"], - literals: ["Literals", "字面量"], - namedtuples: ["Named tuples", "命名元组"], - narrowing: ["Type narrowing", "类型缩小"], - overloads: ["Overloads", "重载"], - protocols: ["Protocols", "协议"], - qualifiers: ["Type qualifiers", "类型限定符"], - specialtypes: ["Special types", "特殊类型"], - tuples: ["Tuples", "元组"], - typeddicts: ["Typed dictionaries", "类型字典"], - typeforms: ["Type forms", "类型形式"], - strictness: ["Strictness", "严格性"], - dependencies: ["Dependencies", "依赖管理"], - style: ["Style", "代码风格"], - imports: ["Imports", "导入"], - redundancy: ["Redundancy", "冗余代码"], - stubs: ["Type stubs", "类型存根"], - suppressions: ["Suppressions", "抑制指令"], -}; - -const PEP_ORDER = [ - "core", "aliases", "annotations", "callables", "classes", "constructors", - "dataclasses", "directives", "enums", "exceptions", "generics", "historical", - "literals", "namedtuples", "narrowing", "overloads", "protocols", "qualifiers", - "specialtypes", "tuples", "typeddicts", "typeforms", -]; - -const BASILISK_ORDER = [ - "strictness", "dependencies", "style", "imports", "redundancy", "stubs", - "suppressions", -]; - -function rulesFor(provenance, tag) { - return rules.filter((rule) => { - if (rule.provenance !== provenance) return false; - if (tag === "core") return rule.tags.length === 1; - return rule.tags.includes(tag); - }); -} - -function makeGroups(provenance, order) { - return order - .map((tag) => { - const items = rulesFor(provenance, tag); - const [label, labelZh] = LABELS[tag]; - return { - provenance, - tag, - id: tag, - label, - labelZh, - count: items.length, - items, - url: `/docs/rules/${provenance}/${tag}/`, - zhUrl: `/zh/docs/rules/${provenance}/${tag}/`, - }; - }) - .filter((group) => group.count > 0); -} - -const basilisk = makeGroups("basilisk", BASILISK_ORDER); -const pep = makeGroups("pep", PEP_ORDER); -const basiliskPrimary = basilisk - .map((group) => { - const items = rules.filter( - (rule) => rule.provenance === "basilisk" && rule.tags[1] === group.tag - ); - return { ...group, count: items.length, items }; - }) - .filter((group) => group.count > 0); - -export default { - basilisk, - pep, - pages: [...basilisk, ...pep], - indexGroups: [...basiliskPrimary, ...pep], - counts: { - basilisk: rules.filter((rule) => rule.provenance === "basilisk").length, - pep: rules.filter((rule) => rule.provenance === "pep").length, - }, -}; diff --git a/website/src/_data/rules.json b/website/src/_data/rules.json deleted file mode 100644 index fc75c5e0f..000000000 --- a/website/src/_data/rules.json +++ /dev/null @@ -1,7382 +0,0 @@ -[ - { - "code": "BSK-0001", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "strictness" - ], - "summary": "Missing parameter type annotation", - "summaryHtml": "Missing parameter type annotation", - "body": [ - { - "type": "text", - "html": "receiver exemption shared by TYPEINF-SPECIAL-SELF." - }, - { - "type": "text", - "html": "Never fires where the current engine already infers the parameter type: a scalar-literal default (timeout=30 \u2192 int) determines the type, so demanding an annotation there would be redundant (TYPEINF-FUNC-DEFAULTS). Defaults that do NOT determine the type \u2014 None, empty containers, calls, lambdas, arbitrary expressions \u2014 still require an annotation." - }, - { - "type": "code", - "lang": "python", - "code": "def connect(timeout=30): # \u2713 \u2014 type inferred as int\n pass\n\ndef connect(retries): # BSK-0001 \u2014 nothing to infer from\n pass\n\ndef connect(timeout=None): # BSK-0001 \u2014 None does not determine T | None\n pass" - } - ], - "group": "Missing Annotations", - "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0001", - "references": [ - { - "label": "Typing spec: Type annotations", - "url": "https://typing.python.org/en/latest/spec/annotations.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 3107", - "url": "https://peps.python.org/pep-3107/" - } - ] - }, - { - "code": "BSK-0002", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "strictness" - ], - "summary": "Missing return type annotation", - "summaryHtml": "Missing return type annotation", - "body": [ - { - "type": "text", - "html": "Never fires where the current engine already infers the return type (TYPEINF-FUNC-RETURN): a body whose every return is bare or carries a type-determining literal \u2014 or that has no return at all (None) \u2014 needs no annotation. Returns the engine cannot infer (calls, names, arbitrary expressions) and generators (Generator...) still require one." - }, - { - "type": "code", - "lang": "python", - "code": "def answer(): # \u2713 \u2014 return type inferred as int\n return 42\n\ndef log_it(msg: str): # \u2713 \u2014 no return: inferred as None\n print(msg)\n\ndef fetch(): # BSK-0002 \u2014 call result is not inferable\n return make_value()" - } - ], - "group": "Missing Annotations", - "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0002", - "references": [ - { - "label": "Typing spec: Type annotations", - "url": "https://typing.python.org/en/latest/spec/annotations.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 3107", - "url": "https://peps.python.org/pep-3107/" - } - ] - }, - { - "code": "BSK-0003", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "strictness" - ], - "summary": "Missing variable type annotation", - "summaryHtml": "Missing variable type annotation", - "body": [ - { - "type": "text", - "html": "Fires when a module-level variable has no type annotation. This house rule is off by default \u2014 the default configuration is pure PEP conformance \u2014 and a project opts in via configuration. When enabled, 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-0003", - "references": [ - { - "label": "Typing spec: Type annotations", - "url": "https://typing.python.org/en/latest/spec/annotations.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 3107", - "url": "https://peps.python.org/pep-3107/" - } - ] - }, - { - "code": "BSK-0004", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "strictness" - ], - "summary": "Missing `*args` / `**kwargs` type annotation", - "summaryHtml": "Missing *args / **kwargs type annotation", - "body": [ - { - "type": "text", - "html": "This house rule is off by default \u2014 the default configuration is pure PEP conformance. When a project enables it, every variadic positional parameter (*args) and variadic keyword parameter (**kwargs) must carry an explicit type annotation." - } - ], - "group": "Missing Annotations", - "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0004", - "references": [ - { - "label": "Typing spec: Type annotations", - "url": "https://typing.python.org/en/latest/spec/annotations.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 3107", - "url": "https://peps.python.org/pep-3107/" - } - ] - }, - { - "code": "BSK-0005", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "strictness" - ], - "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-0005", - "references": [ - { - "label": "Typing spec: Type annotations", - "url": "https://typing.python.org/en/latest/spec/annotations.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 3107", - "url": "https://peps.python.org/pep-3107/" - } - ] - }, - { - "code": "BSK-0011", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "dependencies", - "imports" - ], - "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 # BSK-0011: 'urllib3' is a transitive dependency (via requests)" - } - ], - "group": "Dependencies", - "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0011", - "references": [ - { - "label": "PEP 621", - "url": "https://peps.python.org/pep-0621/" - } - ] - }, - { - "code": "BSK-0012", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "dependencies" - ], - "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": "Dependencies", - "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0012", - "references": [ - { - "label": "PEP 621", - "url": "https://peps.python.org/pep-0621/" - } - ] - }, - { - "code": "BSK-0013", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "dependencies" - ], - "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": "Dependencies", - "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0013", - "references": [ - { - "label": "uv: Locking and syncing", - "url": "https://docs.astral.sh/uv/concepts/projects/sync/" - } - ] - }, - { - "code": "BSK-0014", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "style", - "strictness" - ], - "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 (returns_compatibility); the two used to share a code, so a user could not silence the style nudge while keeping the real type check. BSK-0014 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: ... # BSK-0014 \u2014 parameter `name` is annotated Any\ndef parse(text: str) -> Any: ... # BSK-0014 \u2014 return annotation is Any\n\ndef greet(name: str) -> str: ... # NO warning \u2014 concrete types" - } - ], - "group": "Style", - "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0014", - "references": [ - { - "label": "Typing spec: Special types in annotations", - "url": "https://typing.python.org/en/latest/spec/special-types.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "BSK-0025", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "strictness" - ], - "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. BSK-0025 is silent when the configured python_version is below 3.12." - } - ], - "group": "Missing Annotations", - "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0025", - "references": [ - { - "label": "PEP 698", - "url": "https://peps.python.org/pep-0698/" - } - ] - }, - { - "code": "BSK-0040", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "strictness" - ], - "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": "Missing Annotations", - "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0040", - "references": [ - { - "label": "Typing spec: Type annotations", - "url": "https://typing.python.org/en/latest/spec/annotations.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 3107", - "url": "https://peps.python.org/pep-3107/" - } - ] - }, - { - "code": "BSK-0050", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "redundancy", - "style" - ], - "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 # BSK-0050 \u2014 annotation is redundant\ny: str = \"hello\" # BSK-0050 \u2014 annotation is redundant\nz: float = 42 # NO warning \u2014 annotation adds information (widening)" - } - ], - "group": "Redundancy", - "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0050", - "references": [ - { - "label": "Typing spec: Type annotations", - "url": "https://typing.python.org/en/latest/spec/annotations.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 3107", - "url": "https://peps.python.org/pep-3107/" - } - ] - }, - { - "code": "BSK-0060", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "suppressions" - ], - "summary": "Active code-specific suppression", - "summaryHtml": "Active code-specific suppression", - "body": [ - { - "type": "text", - "html": "Reports a valid source directive that names one or more Basilisk rules and actively suppresses a diagnostic or changes its effective severity." - } - ], - "group": "Suppressions", - "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0060", - "references": [] - }, - { - "code": "BSK-0061", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "suppressions" - ], - "summary": "Active blanket suppression", - "summaryHtml": "Active blanket suppression", - "body": [ - { - "type": "text", - "html": "Reports a valid source directive that actively changes diagnostics without selecting individual Basilisk rule codes." - } - ], - "group": "Suppressions", - "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0061", - "references": [] - }, - { - "code": "BSK-0062", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "suppressions" - ], - "summary": "Unused suppression directive", - "summaryHtml": "Unused suppression directive", - "body": [ - { - "type": "text", - "html": "Reports a syntactically valid directive that matches no diagnostic or does not change the effective severity of anything it matches." - } - ], - "group": "Suppressions", - "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0062", - "references": [] - }, - { - "code": "BSK-0063", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "suppressions" - ], - "summary": "Malformed suppression directive", - "summaryHtml": "Malformed suppression directive", - "body": [ - { - "type": "text", - "html": "Reports malformed directives, unknown Basilisk rule codes, conflicting directives, and unmatched block boundaries." - } - ], - "group": "Suppressions", - "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0063", - "references": [] - }, - { - "code": "BSK-0152", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "stubs" - ], - "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. This rule is off by default \u2014 the default configuration is pure PEP conformance \u2014 and a project opts in with an explicit BSK-0152 severity. Once enabled, an untyped third-party import is a hard error; a project can soften it per import (# type: warningBSK-0152) or globally ("BSK-0152" = "warning") to use non-type-safe libraries at its own risk." - }, - { - "type": "code", - "lang": "python", - "code": "import flask # E0152: Package 'flask' is installed but has no type stubs" - } - ], - "group": "Stubs", - "docsUrl": "https://www.basilisk-python.dev/errors/BSK-0152", - "references": [ - { - "label": "Typing spec: Distributing type information", - "url": "https://typing.python.org/en/latest/spec/distributing.html" - }, - { - "label": "PEP 561", - "url": "https://peps.python.org/pep-0561/" - } - ] - }, - { - "code": "aliases_implicit", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "aliases" - ], - "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/aliases_implicit", - "references": [ - { - "label": "Typing spec: Type aliases", - "url": "https://typing.python.org/en/latest/spec/aliases.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 613", - "url": "https://peps.python.org/pep-0613/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - } - ] - }, - { - "code": "aliases_newtype", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "aliases" - ], - "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/aliases_newtype", - "references": [ - { - "label": "Typing spec: Type aliases", - "url": "https://typing.python.org/en/latest/spec/aliases.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 613", - "url": "https://peps.python.org/pep-0613/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - } - ] - }, - { - "code": "aliases_recursive", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "aliases" - ], - "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/aliases_recursive", - "references": [ - { - "label": "Typing spec: Type aliases", - "url": "https://typing.python.org/en/latest/spec/aliases.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 613", - "url": "https://peps.python.org/pep-0613/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - } - ] - }, - { - "code": "aliases_type_statement", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "aliases" - ], - "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 RHS is validated **structurally** on the parsed ruff expression tree (issue #379 \u2014 substring matching both missed invalid forms and misfired on identifiers containing matched text): names, dotted names, X | Y unions, None, string forward references, and subscriptions of those are type expressions; every other expression form (literals, calls, lambdas, conditionals, comparisons, comprehensions, boolean operators) is not. Subscript arguments are never descended into \u2014 special forms like Literal..., Callable[..., X], and AnnotatedX, ... legitimately hold non-type expressions there." - }, - { - "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/aliases_type_statement", - "references": [ - { - "label": "Typing spec: Type aliases", - "url": "https://typing.python.org/en/latest/spec/aliases.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 613", - "url": "https://peps.python.org/pep-0613/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - } - ] - }, - { - "code": "aliases_typealiastype", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "aliases" - ], - "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/aliases_typealiastype", - "references": [ - { - "label": "Typing spec: Type aliases", - "url": "https://typing.python.org/en/latest/spec/aliases.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 613", - "url": "https://peps.python.org/pep-0613/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - } - ] - }, - { - "code": "annotations_forward_refs", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "annotations" - ], - "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/annotations_forward_refs", - "references": [ - { - "label": "Typing spec: Type annotations", - "url": "https://typing.python.org/en/latest/spec/annotations.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 3107", - "url": "https://peps.python.org/pep-3107/" - } - ] - }, - { - "code": "annotations_generators", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "annotations" - ], - "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/annotations_generators", - "references": [ - { - "label": "Typing spec: Type annotations", - "url": "https://typing.python.org/en/latest/spec/annotations.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 3107", - "url": "https://peps.python.org/pep-3107/" - } - ] - }, - { - "code": "annotations_generators_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "annotations" - ], - "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/annotations_generators_2", - "references": [ - { - "label": "Typing spec: Type annotations", - "url": "https://typing.python.org/en/latest/spec/annotations.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 3107", - "url": "https://peps.python.org/pep-3107/" - } - ] - }, - { - "code": "annotations_typeexpr", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "annotations" - ], - "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 System", - "docsUrl": "https://www.basilisk-python.dev/errors/annotations_typeexpr", - "references": [ - { - "label": "Typing spec: Type annotations", - "url": "https://typing.python.org/en/latest/spec/annotations.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 3107", - "url": "https://peps.python.org/pep-3107/" - } - ] - }, - { - "code": "assignment_compatibility", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep" - ], - "summary": "Assignment type incompatibility (literal mismatches)", - "summaryHtml": "Assignment type incompatibility (literal mismatches)", - "body": [ - { - "type": "text", - "html": "Owns structural TypedDict assignment for TYPEINF-SUBTYPING-TYPEDDICT." - }, - { - "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": "Every right-hand side \u2014 literal, call, constructor, method, variable \u2014 is typed by the module's ModuleOracle (NARROWPLAN-INTEGRATION Step 1: BidirEngine::synth, with synth_call resolving call returns, GitHub #397/#378), collection displays are judged in the annotation's expected-type context by engine check mode, and nominal verdicts route through crate::subtyping::SubtypingContext." - } - ], - "group": "Type System", - "docsUrl": "https://www.basilisk-python.dev/errors/assignment_compatibility", - "references": [ - { - "label": "Typing spec: Type system concepts", - "url": "https://typing.python.org/en/latest/spec/concepts.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "callables_annotation", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "callables" - ], - "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 System", - "docsUrl": "https://www.basilisk-python.dev/errors/callables_annotation", - "references": [ - { - "label": "Typing spec: Callables", - "url": "https://typing.python.org/en/latest/spec/callables.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 692", - "url": "https://peps.python.org/pep-0692/" - } - ] - }, - { - "code": "callables_kwargs", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "callables" - ], - "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/callables_kwargs", - "references": [ - { - "label": "Typing spec: Callables", - "url": "https://typing.python.org/en/latest/spec/callables.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 692", - "url": "https://peps.python.org/pep-0692/" - } - ] - }, - { - "code": "callables_protocol", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "callables" - ], - "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/callables_protocol", - "references": [ - { - "label": "Typing spec: Callables", - "url": "https://typing.python.org/en/latest/spec/callables.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 692", - "url": "https://peps.python.org/pep-0692/" - } - ] - }, - { - "code": "callables_protocol_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "callables" - ], - "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/callables_protocol_2", - "references": [ - { - "label": "Typing spec: Callables", - "url": "https://typing.python.org/en/latest/spec/callables.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 692", - "url": "https://peps.python.org/pep-0692/" - } - ] - }, - { - "code": "callables_subtyping", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "callables" - ], - "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/callables_subtyping", - "references": [ - { - "label": "Typing spec: Callables", - "url": "https://typing.python.org/en/latest/spec/callables.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 692", - "url": "https://peps.python.org/pep-0692/" - } - ] - }, - { - "code": "calls_argument_count", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep" - ], - "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/calls_argument_count", - "references": [ - { - "label": "Typing spec: Callables", - "url": "https://typing.python.org/en/latest/spec/callables.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "calls_argument_type", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep" - ], - "summary": "Argument type mismatch at a call site", - "summaryHtml": "Argument type mismatch at a call site", - "body": [ - { - "type": "text", - "html": "Every argument is judged by the TYPE the module's bidirectional engine synthesises for it (NARROWPLAN-INTEGRATION Step 3), checked against the declared parameter type through the one shared judgment (TypeJudge) \u2014 never by the syntactic shape of the expression." - }, - { - "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 System", - "docsUrl": "https://www.basilisk-python.dev/errors/calls_argument_type", - "references": [ - { - "label": "Typing spec: Callables", - "url": "https://typing.python.org/en/latest/spec/callables.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "classes_classvar", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "classes" - ], - "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/classes_classvar", - "references": [ - { - "label": "Typing spec: Class type assignability", - "url": "https://typing.python.org/en/latest/spec/class-compat.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 698", - "url": "https://peps.python.org/pep-0698/" - } - ] - }, - { - "code": "classes_override", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "classes" - ], - "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 System", - "docsUrl": "https://www.basilisk-python.dev/errors/classes_override", - "references": [ - { - "label": "Typing spec: Class type assignability", - "url": "https://typing.python.org/en/latest/spec/class-compat.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 698", - "url": "https://peps.python.org/pep-0698/" - } - ] - }, - { - "code": "classes_override_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "classes" - ], - "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 System", - "docsUrl": "https://www.basilisk-python.dev/errors/classes_override_2", - "references": [ - { - "label": "Typing spec: Class type assignability", - "url": "https://typing.python.org/en/latest/spec/class-compat.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 698", - "url": "https://peps.python.org/pep-0698/" - } - ] - }, - { - "code": "classes_override_3", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "classes" - ], - "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/classes_override_3", - "references": [ - { - "label": "Typing spec: Class type assignability", - "url": "https://typing.python.org/en/latest/spec/class-compat.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 698", - "url": "https://peps.python.org/pep-0698/" - } - ] - }, - { - "code": "constructors_call_init", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "constructors" - ], - "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/constructors_call_init", - "references": [ - { - "label": "Typing spec: Constructors", - "url": "https://typing.python.org/en/latest/spec/constructors.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "constructors_call_new", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "constructors" - ], - "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/constructors_call_new", - "references": [ - { - "label": "Typing spec: Constructors", - "url": "https://typing.python.org/en/latest/spec/constructors.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "constructors_call_type", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "constructors" - ], - "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/constructors_call_type", - "references": [ - { - "label": "Typing spec: Constructors", - "url": "https://typing.python.org/en/latest/spec/constructors.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "constructors_callable", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "constructors" - ], - "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/constructors_callable", - "references": [ - { - "label": "Typing spec: Constructors", - "url": "https://typing.python.org/en/latest/spec/constructors.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "dataclasses_frozen", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "dataclasses" - ], - "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/dataclasses_frozen", - "references": [ - { - "label": "Typing spec: Dataclasses", - "url": "https://typing.python.org/en/latest/spec/dataclasses.html" - }, - { - "label": "PEP 557", - "url": "https://peps.python.org/pep-0557/" - }, - { - "label": "PEP 681", - "url": "https://peps.python.org/pep-0681/" - } - ] - }, - { - "code": "dataclasses_hash", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "dataclasses" - ], - "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/dataclasses_hash", - "references": [ - { - "label": "Typing spec: Dataclasses", - "url": "https://typing.python.org/en/latest/spec/dataclasses.html" - }, - { - "label": "PEP 557", - "url": "https://peps.python.org/pep-0557/" - }, - { - "label": "PEP 681", - "url": "https://peps.python.org/pep-0681/" - } - ] - }, - { - "code": "dataclasses_inheritance", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "dataclasses" - ], - "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/dataclasses_inheritance", - "references": [ - { - "label": "Typing spec: Dataclasses", - "url": "https://typing.python.org/en/latest/spec/dataclasses.html" - }, - { - "label": "PEP 557", - "url": "https://peps.python.org/pep-0557/" - }, - { - "label": "PEP 681", - "url": "https://peps.python.org/pep-0681/" - } - ] - }, - { - "code": "dataclasses_kwonly", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "dataclasses" - ], - "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/dataclasses_kwonly", - "references": [ - { - "label": "Typing spec: Dataclasses", - "url": "https://typing.python.org/en/latest/spec/dataclasses.html" - }, - { - "label": "PEP 557", - "url": "https://peps.python.org/pep-0557/" - }, - { - "label": "PEP 681", - "url": "https://peps.python.org/pep-0681/" - } - ] - }, - { - "code": "dataclasses_match_args", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "dataclasses" - ], - "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/dataclasses_match_args", - "references": [ - { - "label": "Typing spec: Dataclasses", - "url": "https://typing.python.org/en/latest/spec/dataclasses.html" - }, - { - "label": "PEP 557", - "url": "https://peps.python.org/pep-0557/" - }, - { - "label": "PEP 681", - "url": "https://peps.python.org/pep-0681/" - } - ] - }, - { - "code": "dataclasses_order", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "dataclasses" - ], - "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/dataclasses_order", - "references": [ - { - "label": "Typing spec: Dataclasses", - "url": "https://typing.python.org/en/latest/spec/dataclasses.html" - }, - { - "label": "PEP 557", - "url": "https://peps.python.org/pep-0557/" - }, - { - "label": "PEP 681", - "url": "https://peps.python.org/pep-0681/" - } - ] - }, - { - "code": "dataclasses_postinit", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "dataclasses" - ], - "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/dataclasses_postinit", - "references": [ - { - "label": "Typing spec: Dataclasses", - "url": "https://typing.python.org/en/latest/spec/dataclasses.html" - }, - { - "label": "PEP 557", - "url": "https://peps.python.org/pep-0557/" - }, - { - "label": "PEP 681", - "url": "https://peps.python.org/pep-0681/" - } - ] - }, - { - "code": "dataclasses_slots", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "dataclasses" - ], - "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/dataclasses_slots", - "references": [ - { - "label": "Typing spec: Dataclasses", - "url": "https://typing.python.org/en/latest/spec/dataclasses.html" - }, - { - "label": "PEP 557", - "url": "https://peps.python.org/pep-0557/" - }, - { - "label": "PEP 681", - "url": "https://peps.python.org/pep-0681/" - } - ] - }, - { - "code": "dataclasses_transform_class", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "dataclasses" - ], - "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/dataclasses_transform_class", - "references": [ - { - "label": "Typing spec: Dataclasses", - "url": "https://typing.python.org/en/latest/spec/dataclasses.html" - }, - { - "label": "PEP 557", - "url": "https://peps.python.org/pep-0557/" - }, - { - "label": "PEP 681", - "url": "https://peps.python.org/pep-0681/" - } - ] - }, - { - "code": "dataclasses_transform_meta", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "dataclasses" - ], - "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/dataclasses_transform_meta", - "references": [ - { - "label": "Typing spec: Dataclasses", - "url": "https://typing.python.org/en/latest/spec/dataclasses.html" - }, - { - "label": "PEP 557", - "url": "https://peps.python.org/pep-0557/" - }, - { - "label": "PEP 681", - "url": "https://peps.python.org/pep-0681/" - } - ] - }, - { - "code": "dataclasses_usage", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "dataclasses" - ], - "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/dataclasses_usage", - "references": [ - { - "label": "Typing spec: Dataclasses", - "url": "https://typing.python.org/en/latest/spec/dataclasses.html" - }, - { - "label": "PEP 557", - "url": "https://peps.python.org/pep-0557/" - }, - { - "label": "PEP 681", - "url": "https://peps.python.org/pep-0681/" - } - ] - }, - { - "code": "dict_key_hashable", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep" - ], - "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 System", - "docsUrl": "https://www.basilisk-python.dev/errors/dict_key_hashable", - "references": [ - { - "label": "Typing spec: Type system concepts", - "url": "https://typing.python.org/en/latest/spec/concepts.html" - } - ] - }, - { - "code": "directives_assert_type", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "directives" - ], - "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/directives_assert_type", - "references": [ - { - "label": "Typing spec: Type checker directives", - "url": "https://typing.python.org/en/latest/spec/directives.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 702", - "url": "https://peps.python.org/pep-0702/" - } - ] - }, - { - "code": "directives_assert_type_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "directives" - ], - "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. Two judgments feed it (NARROWPLAN-INTEGRATION Step 5):" - }, - { - "type": "text", - "html": "- the resolver's flow-narrowed comparison of declared parameter types (type_mismatch on basilisk_resolver::AssertTypeCallInfo), and - the module's span-indexed oracle \u2014 the SAME engine behind hover \u2014 for expressions the resolver cannot type (call results, attributes). The oracle verdict fires only when both sides are fully known and provably DISJOINT (neither assignable to the other), so spelling variance and literal widening can never manufacture a false positive (CHKARCH-CONFORMANCE-MODE)." - }, - { - "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/directives_assert_type_2", - "references": [ - { - "label": "Typing spec: Type checker directives", - "url": "https://typing.python.org/en/latest/spec/directives.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 702", - "url": "https://peps.python.org/pep-0702/" - } - ] - }, - { - "code": "directives_cast", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "directives" - ], - "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. A quoted first argument (cast("Widget", x)) is NOT a value literal \u2014 it is the standard PEP 484 forward-reference spelling, which typeshed admits directly (cast(typ: type_T | str | Any, val)) and which ruff's TC006 actively requires \u2014 so only genuine non-string value literals are rejected (issue #335)." - }, - { - "type": "text", - "html": "A cast() is invalid wherever it appears, so every expression position is checked \u2014 return cast(1, x) and print(cast(1, x)) are as wrong as y = cast(1, x) (issue #335)." - }, - { - "type": "text", - "html": "- cast() \u2014 too few arguments - cast(1, x) \u2014 first argument is a value literal, not a type - cast("Widget", x) \u2014 OK: string forward reference - cast(int, x, y) \u2014 too many arguments" - } - ], - "group": "Type System", - "docsUrl": "https://www.basilisk-python.dev/errors/directives_cast", - "references": [ - { - "label": "Typing spec: Type checker directives", - "url": "https://typing.python.org/en/latest/spec/directives.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 702", - "url": "https://peps.python.org/pep-0702/" - } - ] - }, - { - "code": "directives_deprecated", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "directives" - ], - "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() # directives_deprecated" - } - ], - "group": "Type System", - "docsUrl": "https://www.basilisk-python.dev/errors/directives_deprecated", - "references": [ - { - "label": "Typing spec: Type checker directives", - "url": "https://typing.python.org/en/latest/spec/directives.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 702", - "url": "https://peps.python.org/pep-0702/" - } - ] - }, - { - "code": "directives_disjoint_base", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "directives" - ], - "summary": "PEP 800 disjoint bases", - "summaryHtml": "PEP 800 disjoint bases", - "body": [ - { - "type": "text", - "html": "PEP 800 introduces typing.disjoint_base. A class is a disjoint base when it is decorated @disjoint_base or defines a non-empty __slots__. A class definition must have a single dominating disjoint base among its bases:" - }, - { - "type": "code", - "lang": "python", - "code": "@disjoint_base\nclass Left: ...\n@disjoint_base\nclass Right: ...\n\nclass Both(Left, Right): ... # error \u2014 incompatible disjoint bases" - }, - { - "type": "text", - "html": "The decorator may be used only on nominal classes (including NamedTuple); it is an error to apply it to a function, a TypedDict, or a Protocol." - } - ], - "group": "Type System", - "docsUrl": "https://www.basilisk-python.dev/errors/directives_disjoint_base", - "references": [ - { - "label": "Typing spec: Type checker directives", - "url": "https://typing.python.org/en/latest/spec/directives.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 702", - "url": "https://peps.python.org/pep-0702/" - }, - { - "label": "PEP 800", - "url": "https://peps.python.org/pep-0800/" - } - ] - }, - { - "code": "directives_reveal_type", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "directives" - ], - "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/directives_reveal_type", - "references": [ - { - "label": "Typing spec: Type checker directives", - "url": "https://typing.python.org/en/latest/spec/directives.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 702", - "url": "https://peps.python.org/pep-0702/" - } - ] - }, - { - "code": "directives_version_platform", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "directives" - ], - "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/directives_version_platform", - "references": [ - { - "label": "Typing spec: Type checker directives", - "url": "https://typing.python.org/en/latest/spec/directives.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 702", - "url": "https://peps.python.org/pep-0702/" - } - ] - }, - { - "code": "enums_behaviors", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "enums" - ], - "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/enums_behaviors", - "references": [ - { - "label": "Typing spec: Enumerations", - "url": "https://typing.python.org/en/latest/spec/enums.html" - }, - { - "label": "PEP 435", - "url": "https://peps.python.org/pep-0435/" - } - ] - }, - { - "code": "enums_definition", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "enums" - ], - "summary": "access to an enum member that does not exist for the target", - "summaryHtml": "access to an enum member that does not exist for the target", - "body": [ - { - "type": "text", - "html": "Enum members may be defined conditionally on a statically-known check such as the Python version:" - }, - { - "type": "code", - "lang": "python", - "code": "class Color(Enum):\n RED = 1\n if sys.version_info >= (4, 0):\n BLUE = 3 # absent when checking for 3.12\n\nColor.BLUE # error \u2014 BLUE does not exist at the target version" - }, - { - "type": "text", - "html": "This rule flags access to a member that was defined only under an if-guard that is statically false at the configured target. It is intentionally narrow \u2014 it never touches unconditional members, inherited attributes, or functional Enum(...) calls \u2014 so it cannot fire on valid code." - } - ], - "group": "Type System", - "docsUrl": "https://www.basilisk-python.dev/errors/enums_definition", - "references": [ - { - "label": "Typing spec: Enumerations", - "url": "https://typing.python.org/en/latest/spec/enums.html" - }, - { - "label": "PEP 435", - "url": "https://peps.python.org/pep-0435/" - } - ] - }, - { - "code": "enums_expansion", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "enums" - ], - "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/enums_expansion", - "references": [ - { - "label": "Typing spec: Enumerations", - "url": "https://typing.python.org/en/latest/spec/enums.html" - }, - { - "label": "PEP 435", - "url": "https://peps.python.org/pep-0435/" - } - ] - }, - { - "code": "enums_member_values", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "enums" - ], - "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/enums_member_values", - "references": [ - { - "label": "Typing spec: Enumerations", - "url": "https://typing.python.org/en/latest/spec/enums.html" - }, - { - "label": "PEP 435", - "url": "https://peps.python.org/pep-0435/" - } - ] - }, - { - "code": "enums_members", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "enums" - ], - "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/enums_members", - "references": [ - { - "label": "Typing spec: Enumerations", - "url": "https://typing.python.org/en/latest/spec/enums.html" - }, - { - "label": "PEP 435", - "url": "https://peps.python.org/pep-0435/" - } - ] - }, - { - "code": "enums_members_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "enums" - ], - "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/enums_members_2", - "references": [ - { - "label": "Typing spec: Enumerations", - "url": "https://typing.python.org/en/latest/spec/enums.html" - }, - { - "label": "PEP 435", - "url": "https://peps.python.org/pep-0435/" - } - ] - }, - { - "code": "generics_base_class", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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 System", - "docsUrl": "https://www.basilisk-python.dev/errors/generics_base_class", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_base_class_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_base_class_2", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_base_class_3", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_base_class_3", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_basic", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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 System", - "docsUrl": "https://www.basilisk-python.dev/errors/generics_basic", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_basic_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_basic_2", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_basic_3", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "summary": "Generic type argument violations", - "summaryHtml": "Generic type argument violations", - "body": [ - { - "type": "text", - "html": "paths in TYPEINF-GENERICS, TYPEINF-GENERICS-TYPEVAR, and TYPEINF-GENERICS-CONSTRAINED." - }, - { - "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/generics_basic_3", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_defaults", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_defaults", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_defaults_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_defaults_2", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_defaults_referential", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_defaults_referential", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_defaults_referential_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_defaults_referential_2", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_defaults_specialization", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_defaults_specialization", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_scoping", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_scoping", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_self_attributes", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_self_attributes", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_self_basic", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_self_basic", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_self_protocols", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_self_protocols", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_self_usage", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_self_usage", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_syntax_compatibility", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_syntax_compatibility", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_syntax_declarations", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_syntax_declarations", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_syntax_declarations_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_syntax_declarations_2", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_syntax_scoping", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_syntax_scoping", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_type_erasure", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_type_erasure", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_typevartuple_args", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_typevartuple_args", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_typevartuple_basic", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_typevartuple_basic", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_typevartuple_basic_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_typevartuple_basic_2", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_typevartuple_basic_3", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_typevartuple_basic_3", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_typevartuple_callable", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_typevartuple_callable", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_typevartuple_specialization", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_typevartuple_specialization", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_typevartuple_specialization_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_typevartuple_specialization_2", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_typevartuple_unpack", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_typevartuple_unpack", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_upper_bound", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_upper_bound", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_upper_bound_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_upper_bound_2", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_variance", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_variance", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "generics_variance_inference", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "generics" - ], - "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/generics_variance_inference", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 612", - "url": "https://peps.python.org/pep-0612/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - }, - { - "label": "PEP 673", - "url": "https://peps.python.org/pep-0673/" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "PEP 696", - "url": "https://peps.python.org/pep-0696/" - } - ] - }, - { - "code": "historical_positional", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "historical" - ], - "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/historical_positional", - "references": [ - { - "label": "Typing spec: Historical and deprecated features", - "url": "https://typing.python.org/en/latest/spec/historical.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 570", - "url": "https://peps.python.org/pep-0570/" - } - ] - }, - { - "code": "imports_missing_name", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep" - ], - "summary": "Importing a name the resolved module does not define", - "summaryHtml": "Importing a name the resolved module does not define", - "body": [ - { - "type": "text", - "html": "from M import name only proves that the module path M resolves to a file; it says nothing about name. When M is a workspace .py source Basilisk can see every module-level binding, so importing a name that is neither bound in M, nor an existing submodule of the package, is an ImportError waiting for runtime (GitHub #55)." - }, - { - "type": "code", - "lang": "python", - "code": "from demo.late_module import provide_value # late_module.py defines nothing" - }, - { - "type": "text", - "html": "The rule is deliberately conservative \u2014 silence over guessing:" - }, - { - "type": "text", - "html": "- Every module-level binding form counts as defined: def/class, every assignment form, import/from re-exports, for/with/match/ except targets, walrus expressions, and type alias statements. - A module-level __getattr__ (PEP 562) permits any name. - A target containing from x import * has an unknowable member set and suppresses the rule for that module. - from pkg import mod is satisfied by an existing pkg/mod.py, pkg/mod.pyi, or pkg/mod/ submodule." - }, - { - "type": "text", - "html": "Scope: from-imports resolved to workspace .py sources. Stub-backed imports are covered by imports_module_attribute; site-packages sources stay with missing_type_stubs (PEP 561 draws the trust boundary there)." - } - ], - "group": "Type System", - "docsUrl": "https://www.basilisk-python.dev/errors/imports_missing_name", - "references": [ - { - "label": "Typing spec: Distributing type information", - "url": "https://typing.python.org/en/latest/spec/distributing.html" - }, - { - "label": "PEP 561", - "url": "https://peps.python.org/pep-0561/" - }, - { - "label": "PEP 562", - "url": "https://peps.python.org/pep-0562/" - } - ] - }, - { - "code": "imports_module_attribute", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep" - ], - "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 an authoritative user/local or selected Typeshed stub, Basilisk sees the declarations and re-exports in that stub. X.attr where attr is not declared is an error." - }, - { - "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: plain imports backed by a user stub or the active step-3 Typeshed source. Untyped and inline third-party imports remain outside this rule." - } - ], - "group": "Type System", - "docsUrl": "https://www.basilisk-python.dev/errors/imports_module_attribute", - "references": [ - { - "label": "Typing spec: Distributing type information", - "url": "https://typing.python.org/en/latest/spec/distributing.html" - }, - { - "label": "PEP 561", - "url": "https://peps.python.org/pep-0561/" - } - ] - }, - { - "code": "imports_unresolved", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep" - ], - "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." - }, - { - "type": "text", - "html": "This is where the static resolution model surfaces its terminal state (STUBRES-STATIC-MODEL): an import the static filesystem search could not follow \u2014 a missing dependency, but equally a computed/dynamic import or a module only a runtime sys.meta_path hook could supply \u2014 carries an implicit Any, and default-strict reports it here rather than silently accepting it." - } - ], - "group": "Type System", - "docsUrl": "https://www.basilisk-python.dev/errors/imports_unresolved", - "references": [ - { - "label": "Typing spec: Distributing type information", - "url": "https://typing.python.org/en/latest/spec/distributing.html" - }, - { - "label": "PEP 561", - "url": "https://peps.python.org/pep-0561/" - } - ] - }, - { - "code": "literals_literalstring", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "literals" - ], - "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/literals_literalstring", - "references": [ - { - "label": "Typing spec: Literals", - "url": "https://typing.python.org/en/latest/spec/literal.html" - }, - { - "label": "PEP 586", - "url": "https://peps.python.org/pep-0586/" - }, - { - "label": "PEP 675", - "url": "https://peps.python.org/pep-0675/" - } - ] - }, - { - "code": "literals_parameterizations", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "literals" - ], - "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/literals_parameterizations", - "references": [ - { - "label": "Typing spec: Literals", - "url": "https://typing.python.org/en/latest/spec/literal.html" - }, - { - "label": "PEP 586", - "url": "https://peps.python.org/pep-0586/" - }, - { - "label": "PEP 675", - "url": "https://peps.python.org/pep-0675/" - } - ] - }, - { - "code": "literals_parameterizations_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "literals" - ], - "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/literals_parameterizations_2", - "references": [ - { - "label": "Typing spec: Literals", - "url": "https://typing.python.org/en/latest/spec/literal.html" - }, - { - "label": "PEP 586", - "url": "https://peps.python.org/pep-0586/" - }, - { - "label": "PEP 675", - "url": "https://peps.python.org/pep-0675/" - } - ] - }, - { - "code": "literals_semantics", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "literals" - ], - "summary": "Augmented assignment widens `Literal` type", - "summaryHtml": "Augmented assignment widens Literal type", - "body": [ - { - "type": "text", - "html": "augmented assignment keeps the declared target type and validates whether the operation widens out of it." - }, - { - "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/literals_semantics", - "references": [ - { - "label": "Typing spec: Literals", - "url": "https://typing.python.org/en/latest/spec/literal.html" - }, - { - "label": "PEP 586", - "url": "https://peps.python.org/pep-0586/" - }, - { - "label": "PEP 675", - "url": "https://peps.python.org/pep-0675/" - } - ] - }, - { - "code": "literals_semantics_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "literals" - ], - "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/literals_semantics_2", - "references": [ - { - "label": "Typing spec: Literals", - "url": "https://typing.python.org/en/latest/spec/literal.html" - }, - { - "label": "PEP 586", - "url": "https://peps.python.org/pep-0586/" - }, - { - "label": "PEP 675", - "url": "https://peps.python.org/pep-0675/" - } - ] - }, - { - "code": "match_exhaustiveness", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep" - ], - "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 reports this as an error." - }, - { - "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 System", - "docsUrl": "https://www.basilisk-python.dev/errors/match_exhaustiveness", - "references": [ - { - "label": "Typing spec: Type narrowing", - "url": "https://typing.python.org/en/latest/spec/narrowing.html" - }, - { - "label": "PEP 634", - "url": "https://peps.python.org/pep-0634/" - } - ] - }, - { - "code": "namedtuples_define_class", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "namedtuples" - ], - "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/namedtuples_define_class", - "references": [ - { - "label": "Typing spec: Named Tuples", - "url": "https://typing.python.org/en/latest/spec/namedtuples.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "namedtuples_define_functional", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "namedtuples" - ], - "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/namedtuples_define_functional", - "references": [ - { - "label": "Typing spec: Named Tuples", - "url": "https://typing.python.org/en/latest/spec/namedtuples.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "namedtuples_type_compat", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "namedtuples" - ], - "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/namedtuples_type_compat", - "references": [ - { - "label": "Typing spec: Named Tuples", - "url": "https://typing.python.org/en/latest/spec/namedtuples.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "namedtuples_usage", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "namedtuples" - ], - "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/namedtuples_usage", - "references": [ - { - "label": "Typing spec: Named Tuples", - "url": "https://typing.python.org/en/latest/spec/namedtuples.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "names_unbound", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep" - ], - "summary": "possibly-unbound variable at a `return`", - "summaryHtml": "possibly-unbound variable at a return", - "body": [ - { - "type": "text", - "html": "NARROWPLAN-INTEGRATION Step 8 (#285(https://github.com/Nimblesite/Basilisk/issues/285)): definite assignment is tracked over ALL paths, and divergence is the walker's inference-driven analysis (NARROWPLAN-FLOW, crate::narrow::stmt_diverges) \u2014 a branch that provably never falls through (return, raise, a NoReturn-typed call, while True: without break) cannot leave the name unbound, so it drops out of the merge instead of poisoning it." - }, - { - "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 names_unbound\n\ndef guarded(flag: bool) -> int:\n if flag:\n result = 42\n else:\n return 0 # this path never reaches the return below\n return result # bound on every live path \u2014 silent" - }, - { - "type": "text", - "html": "Gradual posture (TYPEINF-TARGET-GRADUAL): a read the walk cannot prove bound on every live path fires only where the walk is exact (straight lines, if/elif/else, try success paths, match cases, with bodies); inside loop bodies, except handlers, and finally blocks \u2014 where an earlier iteration or a mid-statement exception makes "bound" path-dependent \u2014 the walk abstains." - } - ], - "group": "Type System", - "docsUrl": "https://www.basilisk-python.dev/errors/names_unbound", - "references": [ - { - "label": "Python language reference: Naming and binding", - "url": "https://docs.python.org/3/reference/executionmodel.html#naming-and-binding" - } - ] - }, - { - "code": "names_undefined", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep" - ], - "summary": "Reference to a name with no visible definition", - "summaryHtml": "Reference to a name with no visible definition", - "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, import, or PEP 695 type alias, an enclosing scope's binding, a cross-module imported symbol, or a builtin." - }, - { - "type": "text", - "html": "Also flags a module-level statement that calls a name bound nowhere in the module (issue #397), and a class that lists **its own name among its bases** (issue #398) \u2014 Python evaluates the bases tuple before binding the class name, so both raise NameError the moment the module is imported. Shadowing stays legal: class D(D) is only flagged when the class statement is the SOLE binding of that name (no earlier class, import, assignment, or builtin to inherit from). A from m import * disables both module-level passes: the star can bind any name." - }, - { - "type": "code", - "lang": "python", - "code": "def compute() -> int:\n return undefined_name # never defined \u2192 E0018\n return undefined_fn() # undefined callee \u2192 E0018\n\n\na: int = print2(\"abc\") # no `print2` anywhere \u2192 E0018\n\nclass D(D): # `D` unbound in its own bases \u2192 E0018\n pass" - } - ], - "group": "Type System", - "docsUrl": "https://www.basilisk-python.dev/errors/names_undefined", - "references": [ - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - }, - { - "label": "Python language reference: Naming and binding", - "url": "https://docs.python.org/3/reference/executionmodel.html#naming-and-binding" - } - ] - }, - { - "code": "narrowing_typeguard", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "narrowing" - ], - "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/narrowing_typeguard", - "references": [ - { - "label": "Typing spec: Type narrowing", - "url": "https://typing.python.org/en/latest/spec/narrowing.html" - }, - { - "label": "PEP 647", - "url": "https://peps.python.org/pep-0647/" - }, - { - "label": "PEP 742", - "url": "https://peps.python.org/pep-0742/" - } - ] - }, - { - "code": "narrowing_typeis", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "narrowing" - ], - "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/narrowing_typeis", - "references": [ - { - "label": "Typing spec: Type narrowing", - "url": "https://typing.python.org/en/latest/spec/narrowing.html" - }, - { - "label": "PEP 647", - "url": "https://peps.python.org/pep-0647/" - }, - { - "label": "PEP 742", - "url": "https://peps.python.org/pep-0742/" - } - ] - }, - { - "code": "narrowing_typeis_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "narrowing" - ], - "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/narrowing_typeis_2", - "references": [ - { - "label": "Typing spec: Type narrowing", - "url": "https://typing.python.org/en/latest/spec/narrowing.html" - }, - { - "label": "PEP 647", - "url": "https://peps.python.org/pep-0647/" - }, - { - "label": "PEP 742", - "url": "https://peps.python.org/pep-0742/" - } - ] - }, - { - "code": "overloads_basic", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "overloads" - ], - "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/overloads_basic", - "references": [ - { - "label": "Typing spec: Overloads", - "url": "https://typing.python.org/en/latest/spec/overload.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "overloads_consistency", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "overloads" - ], - "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 System", - "docsUrl": "https://www.basilisk-python.dev/errors/overloads_consistency", - "references": [ - { - "label": "Typing spec: Overloads", - "url": "https://typing.python.org/en/latest/spec/overload.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "overloads_consistency_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "overloads" - ], - "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; in a stub (no implementation), placing either on any but the first overload is an error." - } - ], - "group": "Type System", - "docsUrl": "https://www.basilisk-python.dev/errors/overloads_consistency_2", - "references": [ - { - "label": "Typing spec: Overloads", - "url": "https://typing.python.org/en/latest/spec/overload.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "overloads_consistency_3", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "overloads" - ], - "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/overloads_consistency_3", - "references": [ - { - "label": "Typing spec: Overloads", - "url": "https://typing.python.org/en/latest/spec/overload.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "overloads_definitions", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "overloads" - ], - "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 System", - "docsUrl": "https://www.basilisk-python.dev/errors/overloads_definitions", - "references": [ - { - "label": "Typing spec: Overloads", - "url": "https://typing.python.org/en/latest/spec/overload.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "overloads_evaluation", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "overloads" - ], - "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/overloads_evaluation", - "references": [ - { - "label": "Typing spec: Overloads", - "url": "https://typing.python.org/en/latest/spec/overload.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "protocols_class_objects", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "protocols" - ], - "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/protocols_class_objects", - "references": [ - { - "label": "Typing spec: Protocols", - "url": "https://typing.python.org/en/latest/spec/protocol.html" - }, - { - "label": "PEP 544", - "url": "https://peps.python.org/pep-0544/" - } - ] - }, - { - "code": "protocols_class_objects_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "protocols" - ], - "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/protocols_class_objects_2", - "references": [ - { - "label": "Typing spec: Protocols", - "url": "https://typing.python.org/en/latest/spec/protocol.html" - }, - { - "label": "PEP 544", - "url": "https://peps.python.org/pep-0544/" - } - ] - }, - { - "code": "protocols_definition", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "protocols" - ], - "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/protocols_definition", - "references": [ - { - "label": "Typing spec: Protocols", - "url": "https://typing.python.org/en/latest/spec/protocol.html" - }, - { - "label": "PEP 544", - "url": "https://peps.python.org/pep-0544/" - } - ] - }, - { - "code": "protocols_definition_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "protocols" - ], - "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/protocols_definition_2", - "references": [ - { - "label": "Typing spec: Protocols", - "url": "https://typing.python.org/en/latest/spec/protocol.html" - }, - { - "label": "PEP 544", - "url": "https://peps.python.org/pep-0544/" - } - ] - }, - { - "code": "protocols_explicit", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "protocols" - ], - "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/protocols_explicit", - "references": [ - { - "label": "Typing spec: Protocols", - "url": "https://typing.python.org/en/latest/spec/protocol.html" - }, - { - "label": "PEP 544", - "url": "https://peps.python.org/pep-0544/" - } - ] - }, - { - "code": "protocols_explicit_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "protocols" - ], - "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/protocols_explicit_2", - "references": [ - { - "label": "Typing spec: Protocols", - "url": "https://typing.python.org/en/latest/spec/protocol.html" - }, - { - "label": "PEP 544", - "url": "https://peps.python.org/pep-0544/" - } - ] - }, - { - "code": "protocols_explicit_3", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "protocols" - ], - "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/protocols_explicit_3", - "references": [ - { - "label": "Typing spec: Protocols", - "url": "https://typing.python.org/en/latest/spec/protocol.html" - }, - { - "label": "PEP 544", - "url": "https://peps.python.org/pep-0544/" - } - ] - }, - { - "code": "protocols_generic", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "protocols" - ], - "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/protocols_generic", - "references": [ - { - "label": "Typing spec: Protocols", - "url": "https://typing.python.org/en/latest/spec/protocol.html" - }, - { - "label": "PEP 544", - "url": "https://peps.python.org/pep-0544/" - } - ] - }, - { - "code": "protocols_merging", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "protocols" - ], - "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/protocols_merging", - "references": [ - { - "label": "Typing spec: Protocols", - "url": "https://typing.python.org/en/latest/spec/protocol.html" - }, - { - "label": "PEP 544", - "url": "https://peps.python.org/pep-0544/" - } - ] - }, - { - "code": "protocols_modules", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "protocols" - ], - "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/protocols_modules", - "references": [ - { - "label": "Typing spec: Protocols", - "url": "https://typing.python.org/en/latest/spec/protocol.html" - }, - { - "label": "PEP 544", - "url": "https://peps.python.org/pep-0544/" - } - ] - }, - { - "code": "protocols_runtime_checkable", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "protocols" - ], - "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/protocols_runtime_checkable", - "references": [ - { - "label": "Typing spec: Protocols", - "url": "https://typing.python.org/en/latest/spec/protocol.html" - }, - { - "label": "PEP 544", - "url": "https://peps.python.org/pep-0544/" - } - ] - }, - { - "code": "protocols_runtime_checkable_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "protocols" - ], - "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/protocols_runtime_checkable_2", - "references": [ - { - "label": "Typing spec: Protocols", - "url": "https://typing.python.org/en/latest/spec/protocol.html" - }, - { - "label": "PEP 544", - "url": "https://peps.python.org/pep-0544/" - } - ] - }, - { - "code": "protocols_subtyping", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "protocols" - ], - "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/protocols_subtyping", - "references": [ - { - "label": "Typing spec: Protocols", - "url": "https://typing.python.org/en/latest/spec/protocol.html" - }, - { - "label": "PEP 544", - "url": "https://peps.python.org/pep-0544/" - } - ] - }, - { - "code": "protocols_variance", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "protocols" - ], - "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/protocols_variance", - "references": [ - { - "label": "Typing spec: Protocols", - "url": "https://typing.python.org/en/latest/spec/protocol.html" - }, - { - "label": "PEP 544", - "url": "https://peps.python.org/pep-0544/" - } - ] - }, - { - "code": "protocols_variance_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "protocols" - ], - "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/protocols_variance_2", - "references": [ - { - "label": "Typing spec: Protocols", - "url": "https://typing.python.org/en/latest/spec/protocol.html" - }, - { - "label": "PEP 544", - "url": "https://peps.python.org/pep-0544/" - } - ] - }, - { - "code": "qualifiers_annotated", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "qualifiers" - ], - "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/qualifiers_annotated", - "references": [ - { - "label": "Typing spec: Type qualifiers", - "url": "https://typing.python.org/en/latest/spec/qualifiers.html" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 591", - "url": "https://peps.python.org/pep-0591/" - }, - { - "label": "PEP 593", - "url": "https://peps.python.org/pep-0593/" - } - ] - }, - { - "code": "qualifiers_annotated_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "qualifiers" - ], - "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/qualifiers_annotated_2", - "references": [ - { - "label": "Typing spec: Type qualifiers", - "url": "https://typing.python.org/en/latest/spec/qualifiers.html" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 591", - "url": "https://peps.python.org/pep-0591/" - }, - { - "label": "PEP 593", - "url": "https://peps.python.org/pep-0593/" - } - ] - }, - { - "code": "qualifiers_final_annotation", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "qualifiers" - ], - "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/qualifiers_final_annotation", - "references": [ - { - "label": "Typing spec: Type qualifiers", - "url": "https://typing.python.org/en/latest/spec/qualifiers.html" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 591", - "url": "https://peps.python.org/pep-0591/" - }, - { - "label": "PEP 593", - "url": "https://peps.python.org/pep-0593/" - } - ] - }, - { - "code": "qualifiers_final_annotation_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "qualifiers" - ], - "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/qualifiers_final_annotation_2", - "references": [ - { - "label": "Typing spec: Type qualifiers", - "url": "https://typing.python.org/en/latest/spec/qualifiers.html" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 591", - "url": "https://peps.python.org/pep-0591/" - }, - { - "label": "PEP 593", - "url": "https://peps.python.org/pep-0593/" - } - ] - }, - { - "code": "qualifiers_final_decorator", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "qualifiers" - ], - "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/qualifiers_final_decorator", - "references": [ - { - "label": "Typing spec: Type qualifiers", - "url": "https://typing.python.org/en/latest/spec/qualifiers.html" - }, - { - "label": "PEP 526", - "url": "https://peps.python.org/pep-0526/" - }, - { - "label": "PEP 591", - "url": "https://peps.python.org/pep-0591/" - }, - { - "label": "PEP 593", - "url": "https://peps.python.org/pep-0593/" - } - ] - }, - { - "code": "returns_compatibility", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep" - ], - "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": "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 System", - "docsUrl": "https://www.basilisk-python.dev/errors/returns_compatibility", - "references": [ - { - "label": "Typing spec: Type system concepts", - "url": "https://typing.python.org/en/latest/spec/concepts.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "returns_compatibility_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep" - ], - "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 System", - "docsUrl": "https://www.basilisk-python.dev/errors/returns_compatibility_2", - "references": [ - { - "label": "Typing spec: Type system concepts", - "url": "https://typing.python.org/en/latest/spec/concepts.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "specialtypes_never", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "specialtypes" - ], - "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/specialtypes_never", - "references": [ - { - "label": "Typing spec: Special types in annotations", - "url": "https://typing.python.org/en/latest/spec/special-types.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "specialtypes_never_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "specialtypes" - ], - "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/specialtypes_never_2", - "references": [ - { - "label": "Typing spec: Special types in annotations", - "url": "https://typing.python.org/en/latest/spec/special-types.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "specialtypes_promotions", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "specialtypes" - ], - "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/specialtypes_promotions", - "references": [ - { - "label": "Typing spec: Special types in annotations", - "url": "https://typing.python.org/en/latest/spec/special-types.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "specialtypes_type", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "specialtypes" - ], - "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/specialtypes_type", - "references": [ - { - "label": "Typing spec: Special types in annotations", - "url": "https://typing.python.org/en/latest/spec/special-types.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - } - ] - }, - { - "code": "tuples_index", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "tuples" - ], - "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" - }, - { - "type": "text", - "html": "The parameter of a key= lambda passed to sorted/min/max/list.sort receives one element of the iterable, so when the iterable is provably a collection of fixed-length tuples \u2014 from its annotation or from a literal of uniform tuples \u2014 the same range check applies inside the lambda:" - }, - { - "type": "code", - "lang": "python", - "code": "items = [(\"a\", 1, 2), (\"b\", 3, 4)]\nsorted(items, key=lambda pair: pair[4]) # E \u2014 4 out of range for 3-tuple" - } - ], - "group": "Type System", - "docsUrl": "https://www.basilisk-python.dev/errors/tuples_index", - "references": [ - { - "label": "Typing spec: Tuples", - "url": "https://typing.python.org/en/latest/spec/tuples.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - } - ] - }, - { - "code": "tuples_index_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "tuples" - ], - "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/tuples_index_2", - "references": [ - { - "label": "Typing spec: Tuples", - "url": "https://typing.python.org/en/latest/spec/tuples.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - } - ] - }, - { - "code": "tuples_type_compat", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "tuples" - ], - "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/tuples_type_compat", - "references": [ - { - "label": "Typing spec: Tuples", - "url": "https://typing.python.org/en/latest/spec/tuples.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - } - ] - }, - { - "code": "tuples_type_form", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "tuples" - ], - "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/tuples_type_form", - "references": [ - { - "label": "Typing spec: Tuples", - "url": "https://typing.python.org/en/latest/spec/tuples.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - } - ] - }, - { - "code": "tuples_type_form_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "tuples" - ], - "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/tuples_type_form_2", - "references": [ - { - "label": "Typing spec: Tuples", - "url": "https://typing.python.org/en/latest/spec/tuples.html" - }, - { - "label": "PEP 484", - "url": "https://peps.python.org/pep-0484/" - }, - { - "label": "PEP 646", - "url": "https://peps.python.org/pep-0646/" - } - ] - }, - { - "code": "typeddicts_alt_syntax", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "typeddicts" - ], - "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/typeddicts_alt_syntax", - "references": [ - { - "label": "Typing spec: Typed dictionaries", - "url": "https://typing.python.org/en/latest/spec/typeddict.html" - }, - { - "label": "PEP 589", - "url": "https://peps.python.org/pep-0589/" - }, - { - "label": "PEP 655", - "url": "https://peps.python.org/pep-0655/" - }, - { - "label": "PEP 705", - "url": "https://peps.python.org/pep-0705/" - }, - { - "label": "PEP 728", - "url": "https://peps.python.org/pep-0728/" - } - ] - }, - { - "code": "typeddicts_class_syntax", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "typeddicts" - ], - "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 System", - "docsUrl": "https://www.basilisk-python.dev/errors/typeddicts_class_syntax", - "references": [ - { - "label": "Typing spec: Typed dictionaries", - "url": "https://typing.python.org/en/latest/spec/typeddict.html" - }, - { - "label": "PEP 589", - "url": "https://peps.python.org/pep-0589/" - }, - { - "label": "PEP 655", - "url": "https://peps.python.org/pep-0655/" - }, - { - "label": "PEP 705", - "url": "https://peps.python.org/pep-0705/" - }, - { - "label": "PEP 728", - "url": "https://peps.python.org/pep-0728/" - } - ] - }, - { - "code": "typeddicts_class_syntax_2", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "typeddicts" - ], - "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/typeddicts_class_syntax_2", - "references": [ - { - "label": "Typing spec: Typed dictionaries", - "url": "https://typing.python.org/en/latest/spec/typeddict.html" - }, - { - "label": "PEP 589", - "url": "https://peps.python.org/pep-0589/" - }, - { - "label": "PEP 655", - "url": "https://peps.python.org/pep-0655/" - }, - { - "label": "PEP 705", - "url": "https://peps.python.org/pep-0705/" - }, - { - "label": "PEP 728", - "url": "https://peps.python.org/pep-0728/" - } - ] - }, - { - "code": "typeddicts_extra_items", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "typeddicts" - ], - "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/typeddicts_extra_items", - "references": [ - { - "label": "Typing spec: Typed dictionaries", - "url": "https://typing.python.org/en/latest/spec/typeddict.html" - }, - { - "label": "PEP 589", - "url": "https://peps.python.org/pep-0589/" - }, - { - "label": "PEP 655", - "url": "https://peps.python.org/pep-0655/" - }, - { - "label": "PEP 705", - "url": "https://peps.python.org/pep-0705/" - }, - { - "label": "PEP 728", - "url": "https://peps.python.org/pep-0728/" - } - ] - }, - { - "code": "typeddicts_inheritance", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "typeddicts" - ], - "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/typeddicts_inheritance", - "references": [ - { - "label": "Typing spec: Typed dictionaries", - "url": "https://typing.python.org/en/latest/spec/typeddict.html" - }, - { - "label": "PEP 589", - "url": "https://peps.python.org/pep-0589/" - }, - { - "label": "PEP 655", - "url": "https://peps.python.org/pep-0655/" - }, - { - "label": "PEP 705", - "url": "https://peps.python.org/pep-0705/" - }, - { - "label": "PEP 728", - "url": "https://peps.python.org/pep-0728/" - } - ] - }, - { - "code": "typeddicts_operations", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "typeddicts" - ], - "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/typeddicts_operations", - "references": [ - { - "label": "Typing spec: Typed dictionaries", - "url": "https://typing.python.org/en/latest/spec/typeddict.html" - }, - { - "label": "PEP 589", - "url": "https://peps.python.org/pep-0589/" - }, - { - "label": "PEP 655", - "url": "https://peps.python.org/pep-0655/" - }, - { - "label": "PEP 705", - "url": "https://peps.python.org/pep-0705/" - }, - { - "label": "PEP 728", - "url": "https://peps.python.org/pep-0728/" - } - ] - }, - { - "code": "typeddicts_readonly", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "typeddicts" - ], - "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/typeddicts_readonly", - "references": [ - { - "label": "Typing spec: Typed dictionaries", - "url": "https://typing.python.org/en/latest/spec/typeddict.html" - }, - { - "label": "PEP 589", - "url": "https://peps.python.org/pep-0589/" - }, - { - "label": "PEP 655", - "url": "https://peps.python.org/pep-0655/" - }, - { - "label": "PEP 705", - "url": "https://peps.python.org/pep-0705/" - }, - { - "label": "PEP 728", - "url": "https://peps.python.org/pep-0728/" - } - ] - }, - { - "code": "typeddicts_required", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "typeddicts" - ], - "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/typeddicts_required", - "references": [ - { - "label": "Typing spec: Typed dictionaries", - "url": "https://typing.python.org/en/latest/spec/typeddict.html" - }, - { - "label": "PEP 589", - "url": "https://peps.python.org/pep-0589/" - }, - { - "label": "PEP 655", - "url": "https://peps.python.org/pep-0655/" - }, - { - "label": "PEP 705", - "url": "https://peps.python.org/pep-0705/" - }, - { - "label": "PEP 728", - "url": "https://peps.python.org/pep-0728/" - } - ] - }, - { - "code": "typeddicts_usage", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep", - "typeddicts" - ], - "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/typeddicts_usage", - "references": [ - { - "label": "Typing spec: Typed dictionaries", - "url": "https://typing.python.org/en/latest/spec/typeddict.html" - }, - { - "label": "PEP 589", - "url": "https://peps.python.org/pep-0589/" - }, - { - "label": "PEP 655", - "url": "https://peps.python.org/pep-0655/" - }, - { - "label": "PEP 705", - "url": "https://peps.python.org/pep-0705/" - }, - { - "label": "PEP 728", - "url": "https://peps.python.org/pep-0728/" - } - ] - }, - { - "code": "typeshed_source_license_changed", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "stubs" - ], - "summary": "The bundled typeshed's approved LICENSE/NOTICE changed and activation was blocked pending review", - "summaryHtml": "The bundled typeshed's approved LICENSE/NOTICE changed and activation was blocked pending review", - "body": [ - { - "type": "text", - "html": "Basilisk vets the LICENSE and NOTICE files of the typeshed snapshot it bundles at build time and records their exact identity. If those legal files no longer match what was approved, Basilisk refuses to serve the stubs rather than distribute content under unknown terms." - }, - { - "type": "text", - "html": "This condition is elevated: it defaults to error, and analysis for the affected root does not run until it is resolved. Update Basilisk to a build whose bundled typeshed license is approved again." - }, - { - "type": "text", - "html": "Like any Basilisk diagnostic it can be graded, though lowering it does not make the underlying license mismatch safe:" - }, - { - "type": "code", - "lang": "toml", - "code": "[tool.basilisk.rules]\n\"typeshed_source_license_changed\" = \"error\"" - }, - { - "type": "text", - "html": "It is reported out of band (CLI banner, an editor window/showMessage, MCP status), never as a Python diagnostic, so it can never affect conformance." - } - ], - "group": "Stubs", - "docsUrl": "https://www.basilisk-python.dev/errors/typeshed_source_license_changed", - "references": [ - { - "label": "python/typeshed LICENSE", - "url": "https://github.com/python/typeshed/blob/main/LICENSE" - }, - { - "label": "python/typeshed", - "url": "https://github.com/python/typeshed" - }, - { - "label": "Basilisk configuration: typeshed source", - "url": "https://www.basilisk-python.dev/docs/configuration/" - } - ] - }, - { - "code": "typeshed_source_unpinned", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "stubs" - ], - "summary": "The active typeshed source is not pinned to an exact commit, so type checks are not reproducible across machines and CI", - "summaryHtml": "The active typeshed source is not pinned to an exact commit, so type checks are not reproducible across machines and CI", - "body": [ - { - "type": "text", - "html": "Basilisk type-checks your code against typeshed, the community's standard-library and third-party type stubs. Which revision of typeshed is active decides which symbols and signatures exist, so two machines resolving different typeshed contents can disagree about whether the same code type-checks." - }, - { - "type": "text", - "html": "Basilisk bundles a vetted typeshed snapshot inside the binary and serves it by default. A build-time snapshot is not a user pin: upgrade Basilisk and the snapshot moves. When no typeshed-commit is set \u2014 or when a custom typeshed-path folder is used, whose contents can change on disk \u2014 Basilisk raises this advisory to say the type-checking baseline is not reproducible." - }, - { - "type": "text", - "html": "Pin an exact python/typeshed commit so every machine and CI run resolves byte-identical stubs. A pin fails closed \u2014 Basilisk never silently substitutes another commit:" - }, - { - "type": "code", - "lang": "toml", - "code": "[tool.basilisk]\ntypeshed-commit = \"\u2026full 40-character SHA\u2026\"" - }, - { - "type": "text", - "html": "This is an ordinary Basilisk diagnostic. Grade it like any rule \u2014 raise it to an error in CI, or silence it once you have accepted the unpinned default:" - }, - { - "type": "code", - "lang": "toml", - "code": "[tool.basilisk.rules]\n\"typeshed_source_unpinned\" = \"error\" # or \"off\" to silence" - }, - { - "type": "text", - "html": "It is reported out of band \u2014 on the CLI's stderr banner, in the editor's Server Info panel, and as MCP status \u2014 and never as a Python diagnostic, so it can never affect conformance." - } - ], - "group": "Stubs", - "docsUrl": "https://www.basilisk-python.dev/errors/typeshed_source_unpinned", - "references": [ - { - "label": "python/typeshed", - "url": "https://github.com/python/typeshed" - }, - { - "label": "Basilisk configuration: typeshed source", - "url": "https://www.basilisk-python.dev/docs/configuration/" - } - ] - }, - { - "code": "typeshed_source_user_managed", - "scope": "analyze", - "provenance": "basilisk", - "tags": [ - "basilisk", - "stubs" - ], - "summary": "A custom typeshed folder is user-managed: you supply its license and contents, so typeshed's license terms are not applied to it", - "summaryHtml": "A custom typeshed folder is user-managed: you supply its license and contents, so typeshed's license terms are not applied to it", - "body": [ - { - "type": "text", - "html": "When you point Basilisk at a custom typeshed-path folder, Basilisk treats it as user-managed: you supply both its contents and its license. Basilisk does not attach python/typeshed's license terms to a tree it did not vet." - }, - { - "type": "text", - "html": "This advisory makes that explicit so you never unintentionally rely on a custom tree believing it carries typeshed's license, or skip the pin and content verification that the bundled and pinned sources enforce." - }, - { - "type": "text", - "html": "It composes with typeshed_source_unpinned \u2014 a custom folder is both unpinned and user-managed. Grade it like any rule:" - }, - { - "type": "code", - "lang": "toml", - "code": "[tool.basilisk.rules]\n\"typeshed_source_user_managed\" = \"warning\" # or \"off\" to silence" - }, - { - "type": "text", - "html": "It is reported out of band (CLI banner, Server Info, MCP status), never as a Python diagnostic, so it can never affect conformance." - } - ], - "group": "Stubs", - "docsUrl": "https://www.basilisk-python.dev/errors/typeshed_source_user_managed", - "references": [ - { - "label": "python/typeshed", - "url": "https://github.com/python/typeshed" - }, - { - "label": "Basilisk configuration: typeshed source", - "url": "https://www.basilisk-python.dev/docs/configuration/" - } - ] - }, - { - "code": "version_target_syntax", - "scope": "check", - "provenance": "pep", - "tags": [ - "pep" - ], - "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/version_target_syntax", - "references": [ - { - "label": "Typing spec: Generics", - "url": "https://typing.python.org/en/latest/spec/generics.html" - }, - { - "label": "PEP 695", - "url": "https://peps.python.org/pep-0695/" - } - ] - } -] diff --git a/website/src/_data/site.js b/website/src/_data/site.js new file mode 100644 index 000000000..b1343f97d --- /dev/null +++ b/website/src/_data/site.js @@ -0,0 +1,20 @@ +// Implements [WITHDRAWAL-COPY]. Site metadata is derived from the generated +// withdrawal copy rather than restated here, so the title, meta description, +// and social card can never say something the messaging spec does not. +import withdrawal from "./withdrawal.json" with { type: "json" }; + +export default { + name: "Basilisk", + title: withdrawal.title, + description: withdrawal.line, + url: "https://www.basilisk-python.dev", + themeColor: "#e8500a", + stylesheet: "/assets/css/styles.css", + github: "https://github.com/Nimblesite/Basilisk", + organization: { + name: "Basilisk", + url: "https://www.basilisk-python.dev", + logo: "/assets/images/favicon.png", + sameAs: ["https://github.com/Nimblesite/Basilisk"], + }, +}; diff --git a/website/src/_data/site.json b/website/src/_data/site.json deleted file mode 100644 index a3b969aee..000000000 --- a/website/src/_data/site.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "Basilisk", - "title": "Basilisk — Python Type Checker & Language Server", - "description": "Open-source Python type checker and language server built in Rust. Conformance and benchmark results are withdrawn during an integrity review.", - "url": "https://www.basilisk-python.dev", - "keywords": "basilisk, python type checker, python type checking, python language server, typing conformance, type checker benchmark, rust, vs code, cursor, zed, neovim", - "themeColor": "#e8500a", - "stylesheet": "/assets/css/styles.css", - "github": "https://github.com/Nimblesite/Basilisk", - "ogImage": "/assets/images/og-image.png", - "ogImageWidth": "1200", - "ogImageHeight": "630", - "organization": { - "name": "Basilisk", - "url": "https://www.basilisk-python.dev", - "logo": "/assets/images/favicon.png", - "sameAs": [ - "https://github.com/Nimblesite/Basilisk" - ] - }, - "version": "0.0.0-PLACEHOLDER" -} diff --git a/website/src/_data/withdrawal.json b/website/src/_data/withdrawal.json new file mode 100644 index 000000000..0b216269f --- /dev/null +++ b/website/src/_data/withdrawal.json @@ -0,0 +1,26 @@ +{ + "_generated": "Generated from docs/specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md by scripts/gen_withdrawal_copy.py — DO NOT EDIT.", + "line": "Basilisk is unlisted. Its type checker produced incorrect results, and the product is being rebuilt from scratch.", + "title": "Basilisk is unlisted", + "short": [ + "Basilisk is unlisted everywhere and is being rebuilt from the ground up.", + "Basilisk's type checker was producing incorrect results. We asked for it to be removed from the python/typing conformance results, and it has been removed. The code responsible is not isolated to a known set of rules, so we cannot say how many rules are affected. A code-quality tool that does not produce correct results is worse than useless — so every distribution channel is unlisted and the CLI is inert.", + "Remove Basilisk from your pipeline. Every distribution channel is being delisted, and the type checker is being made inert — it checks nothing and exits non-zero, so a build that still calls it fails loudly instead of reporting a clean run.", + "What comes next is a new product, rebuilt from scratch, shipping only what can be trusted. That most likely will not include type checking. Nothing is relisted until it has been rebuilt from components we can vouch for.", + "Basilisk's author has published a full public account: an apology." + ], + "action": [ + "Remove Basilisk from your pipeline, your pre-commit hooks, and your editor. Uninstall the CLI and the extension.", + "The type checker is being made inert: it checks nothing, and it exits non-zero so a build that still calls it fails loudly rather than reporting a clean run. Do not treat that failure as a finding about your code.", + "Treat every result Basilisk gave you as unverified. A clean run was never evidence that your code was clean, and an error it reported may never have been real.", + "Every distribution channel is being delisted. Nothing will be relisted until it has been rebuilt from components we can vouch for." + ], + "full": [ + "Basilisk's type checker was producing incorrect results. Rules decided from the way code was spelled rather than what it meant, so they could be wrong in both directions — a false error on correct code, or silence on a real bug.", + "We asked for Basilisk to be removed from the python/typing conformance results, and it has been removed. The score it held was not evidence of anything.", + "We cannot tell you how much of the checker this affects. The code responsible is not isolated to a known set of rules. We will not estimate. That uncertainty is the reason for everything below.", + "A code-quality tool that does not produce correct results is worse than useless. Basilisk is being delisted everywhere it was published — VS Code Marketplace, Open VSX, PyPI, Homebrew, Scoop — and the type checker is being made inert. Remove it from your pipeline; it checks nothing, and it fails rather than reporting a clean run.", + "We are not fixing Basilisk's type checker code. We are rebuilding Basilisk. What comes next is a new product, built from the ground up, shipping only what can be shown to be trustworthy. That most likely will not include type checking. Nothing is relisted until it has been rebuilt from components we can vouch for. If type checking ever returns, it will come from established third-party engines, or code audited by a third party.", + "Basilisk's author has published a full public account: an apology." + ] +} diff --git a/website/src/_includes/benchmark-section.njk b/website/src/_includes/benchmark-section.njk deleted file mode 100644 index 5cea1314e..000000000 --- a/website/src/_includes/benchmark-section.njk +++ /dev/null @@ -1,61 +0,0 @@ -{# - The benchmark fixture table. Every displayed timing comes directly from the - committed per-machine CSV parsed by `_data/benchmarks.js`. A row represents a - complete Python file, and its label links to that source file. -#} -{% macro resultsTable(b, repositoryUrl) %} - {% if b.hasData %} -
- - - - - - - - - - - - - - - - - - - - - {% for row in b.rows %} - - - - - - - - - - - - {% endfor %} - -
- Withdrawn historical mean wall-clock times in milliseconds for each checker to process each complete fixture file; not for tool comparison -
Fixture fileCold · no result cacheWarm · cache hit
BasiliskPyrightmypytyPyreflyzubanBasiliskmypy
- {{ row.filename }} - {{ row.valueText.basilisk }}{{ row.valueText.pyright }}{{ row.valueText.mypy }}{{ row.valueText.ty }}{{ row.valueText.pyrefly }}{{ row.valueText.zuban }}{{ row.valueText['basilisk-warm'] }}{{ row.valueText['mypy-warm'] }}
-
- {% endif %} -{% endmacro %} - -{% macro versions(b) %} - {% if b.meta.toolVersions.length %} -
-
Versions benchmarked
- {% for version in b.meta.toolVersions %} -
{{ version.tool }} {{ version.version }}
- {% endfor %} -
- {% endif %} -{% endmacro %} diff --git a/website/src/_includes/components/blog.njk b/website/src/_includes/components/blog.njk deleted file mode 100644 index afb4bb011..000000000 --- a/website/src/_includes/components/blog.njk +++ /dev/null @@ -1,41 +0,0 @@ -{% macro masthead(title, subtitle, lang) %} -
-

{{ "blog.eyebrow" | t(lang) | default("The Basilisk Journal") }}

-

{{ title }}

- {% if subtitle %}

{{ subtitle }}

{% endif %} -
-{% endmacro %} - -{% macro navigation(lang, defaultLanguage, active) %} -{% set langPrefix = "/" + lang if lang and lang != defaultLanguage else "" %} - -{% endmacro %} - -{% macro postCard(post, lang, defaultLanguage, featured=false) %} -{% set langPrefix = "/" + lang if lang and lang != defaultLanguage else "" %} - -{% endmacro %} diff --git a/website/src/_includes/components/rules.njk b/website/src/_includes/components/rules.njk deleted file mode 100644 index 7da4ea510..000000000 --- a/website/src/_includes/components/rules.njk +++ /dev/null @@ -1,51 +0,0 @@ -{% macro groupGrid(groups, lang = "en") %} - -{% endmacro %} - -{% macro groupPage(group, lang = "en") %} -{% set groupLabel = group.labelZh if lang == "zh" else group.label %} -{% set provenanceLabel = "Basilisk 可选规则" if lang == "zh" else "Basilisk rules (opt-in)" %} -{% if group.provenance == "pep" %} - {% set provenanceLabel = "Python 类型规范规则" if lang == "zh" else "Python typing-spec rules" %} -{% endif %} - - -

{{ groupLabel }}

-

- {% if group.tag == "core" and lang == "zh" %} - {{ group.count }} 条横跨多个主题、因此没有更细分类标签的 Python 类型规范核心规则。 - {% elif group.tag == "core" %} - {{ group.count }} cross-cutting Python typing-spec rules without a narrower category tag. - {% elif lang == "zh" %} - {{ group.count }} 条带有 {{ group.tag }} 标签的{{ provenanceLabel }}。 - {% else %} - {{ group.count }} {{ provenanceLabel | lower }} tagged {{ group.tag }}. - {% endif %} -

- -{% if group.provenance == "basilisk" %} -
-

{% if lang == "zh" %}这些规则默认关闭,只有在项目配置中选择相应标签后才会启用。{% else %}These Basilisk-specific rules are off by default and activate only when your project opts into the corresponding tag.{% endif %}

-
-{% endif %} - -
    - {% for rule in group.items %} -
  • - {{ rule.code }} - {{ rule.summaryHtml | safe }} -
  • - {% endfor %} -
-{% endmacro %} diff --git a/website/src/_includes/conformance-chart.njk b/website/src/_includes/conformance-chart.njk deleted file mode 100644 index 341668169..000000000 --- a/website/src/_includes/conformance-chart.njk +++ /dev/null @@ -1,50 +0,0 @@ -{# - PEP-conformance over-time chart, rendering the history of - conformance/conformance_status.csv in EVERY locale. Pure inline SVG (no JS, no - chart library), data-driven from _data/conformance.js (which reads the file's - real git history). Pages supply only translated prose. - - NO PAGE RENDERS THIS TODAY. The score it charts is withdrawn, so both locales' - conformance pages dropped the import; the macro and its `historical.chart` data - are retained for the integrity audit and for whatever replaces the withdrawn - figure. Delete both together if that replacement never needs a chart. - - WHITESPACE: this macro is embedded inside MARKDOWN pages. markdown-it ends a - raw-HTML block at the first blank line, so the rendered SVG MUST contain no - blank lines or it gets shredded (text nodes leak out of ). Every njk - control tag therefore uses `{%- ... -%}` trimming to keep the output contiguous. - - Args: - c — the global `conformance` data object (from _data/conformance.js) - t — locale strings: { label, heading, subhead, prevLegend, officialLegend, - dropNote, caption } — rendered with `| safe` (may contain inline HTML). -#} -{%- macro chart(c, t) -%} -{%- if c.chart -%} -{%- set ch = c.chart -%} -
-
{{ t.label }}{{ t.heading | safe }} - -{%- for tick in ch.yTicks %} -{{ tick.value }}% -{%- endfor %} - -{%- if ch.drop %} - -{%- endif %} - -{%- for p in ch.pts %} -{{ p.shortDate }} ({{ p.hash }}): {{ p.score }}% — {{ p.pass }}/{{ p.total }}, {{ p.fp }} false positives{{ ' · official calculator' if p.official else ' · earlier in-repo harness' }} -{%- if p.showDate %} -{{ p.shortDate }} -{%- endif %} -{%- endfor %} -{{ ch.peak.score }}% -{{ ch.current.score }}% - -

{{ t.dropNote | safe }}

-
  • {{ t.prevLegend | safe }}
  • {{ t.officialLegend | safe }}
-

{{ t.caption | safe }}

- -{%- endif -%} -{%- endmacro -%} diff --git a/website/src/_includes/layouts/base.njk b/website/src/_includes/layouts/base.njk index b2433d276..6bb16dd69 100644 --- a/website/src/_includes/layouts/base.njk +++ b/website/src/_includes/layouts/base.njk @@ -1,14 +1,10 @@ -{#- Locale-safe i18n: derive the effective language and a locale-stripped base - path straight from the URL, so language alternates never double-prefix - (/zh/zh/...) even when an auto-generated page reports the wrong `lang`. - Set `noTranslation: true` in a page's front matter to opt it out of the - language cluster entirely (e.g. the English-only Releases page). -#} -{%- set effLang = 'zh' if (page.url == '/zh/' or page.url.startsWith('/zh/')) else (lang | default('en')) -%} -{%- set basePath = (page.url | replace('/zh/', '/')) if effLang == 'zh' else page.url -%} -{%- set alternatePath = basePath if effLang == 'zh' else '/zh' + basePath -%} -{%- set hasTranslation = (not noTranslation) and (collections.all | hasPageUrl(alternatePath)) -%} - +{#- The site serves one statement and a notice at every retired URL + ([WITHDRAWAL-SURFACES]). It is monolingual and dark-only, so this layout + carries no language cluster, no theme switcher, and no social card: the + approved copy exists in English only, and there is no product image left to + share. -#} + @@ -20,81 +16,35 @@ gtag('config', 'G-JM4GYVKF8C'); - - {% set metaAuthor = author | default(site.author) %} - {% set metaKeywords = keywords | default(site.keywords) %} {{ title | default(site.title) }} - - {% if metaAuthor %}{% endif %} - {% if metaKeywords %}{% endif %} - - - + {#- [WITHDRAWAL-SURFACES]: the notice pages are byte-identical copies of one + paragraph served at every retired URL. They must stay reachable so an old + link explains itself, but must not be offered for indexing. -#} + + - + - - - - - {%- if not hasTranslation %} - - - {%- else %} - {% for langCode in supportedLanguages %} - - {% endfor %} - - {%- endif %} - - - {% set metaImage = image | default(site.ogImage) %} - {% set metaImageWidth = imageWidth | default(site.ogImageWidth) | default('1200') %} - {% set metaImageHeight = imageHeight | default(site.ogImageHeight) | default('630') %} - {% set metaImageAlt = imageAlt | default(title) | default(site.name) %} - + + - - - {%- if hasTranslation %} - {% for langCode in supportedLanguages %}{% if langCode != effLang %} - - {% endif %}{% endfor %} - {%- endif %} - {% if metaImage %} - - - - - {% endif %} - - - - + + - {% if site.twitterSite %}{% endif %} - {% if site.twitterCreator %}{% endif %} - {% if metaImage %} - {% endif %} @@ -185,8 +87,7 @@ - - {% if site.stylesheet %}{% endif %} + {% block head %}{% endblock %} @@ -194,60 +95,22 @@

LuW~=CR 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 diff --git a/scripts/gen_conformance_reference.py b/scripts/gen_conformance_reference.py deleted file mode 100644 index 80ba16ed3..000000000 --- a/scripts/gen_conformance_reference.py +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env python3 -# Implements [CHKARCH-CONFORMANCE]. See docs/specs/CHECKER-ARCHITECTURE-SPEC.md -"""Stamp the live conformance score + graded commit into the README and spec. - -Static docs (README.md, README.zh.md, the checker-architecture spec) quote the -conformance score and the exact `python/typing` commit it was measured against. -Those drift as the checker improves and `main` advances. This generator reads -`website/src/_data/conformance_report.json` — written by -`conformance/run_conformance.py` on every run from the REAL python/typing harness -output — and refreshes the quoted values in place, so the docs can never silently -contradict the self-measured number. - -It updates two kinds of spot, both render-safe (the markers are invisible HTML -comments, so they work mid-sentence, inside a list item, or inside a table cell): - - • inline markers `value` -> the value for NAME - • commit-tree URLs `github.com/python/typing/tree//conformance` -> the sha - -Usage: - python3 scripts/gen_conformance_reference.py # rewrite in place - python3 scripts/gen_conformance_reference.py --check # CI: fail if stale -""" - -from __future__ import annotations - -import json -import math -import re -import sys -from pathlib import Path - -import gen_readmes - -ROOT = Path(__file__).resolve().parents[1] -REPORT = ROOT / "website" / "src" / "_data" / "conformance_report.json" -BENCH_STATUS_DIR = ROOT / "benchmarks" / "status" -# Every published README quotes the same score, but only ONE file per language -# is authored: the READMEs are generated from these sources ([README]), so the -# markers are stamped here and `gen_readmes.py` propagates them to GitHub, the -# VSIX (Marketplace + Open VSX), and PyPI. -TARGETS = ( - ROOT / "docs" / "readme" / "README.src.md", - ROOT / "docs" / "readme" / "README.zh.src.md", - ROOT / "docs" / "specs" / "CHECKER-ARCHITECTURE-SPEC.md", -) - -# The checkers whose median cold time the README bench table quotes. Key is the -# CSV `_ms` column; the sentinel name is `bench` (e.g. -# `benchBasilisk`), stamped inline in the table cell so it never breaks the -# markdown table the way a standalone comment line would. -BENCH_TOOLS = ("basilisk", "pyright", "mypy", "ty", "pyrefly", "zuban") - -MARKER_RE = re.compile(r".*?", re.S) -TREE_SHA_RE = re.compile( - r"(github\.com/python/typing/tree/)[0-9a-fA-F]{7,40}(/conformance)" -) - - -def values(report: dict) -> dict[str, str]: - """The named values the markers may reference, from the score report.""" - score = report["score"] - upstream = report["upstream"] - return { - "score": f"{score['scorePct']}%", - "pass": str(score["pass"]), - "total": str(score["total"]), - "fp": str(score["falsePositives"]), - "missed": str(score["missed"]), - "caught": str(score["caught"]), - "short": upstream["shortSha"], - } - - -def _median_ms(nums: list[float]) -> int | None: - """Median of `nums`, rounded half-up to match the website's JS `Math.round`.""" - ordered = sorted(nums) - n = len(ordered) - if n == 0: - return None - mid = n // 2 - val = ordered[mid] if n % 2 else (ordered[mid - 1] + ordered[mid]) / 2 - return math.floor(val + 0.5) - - -def _primary_bench_csv() -> Path | None: - """The benchmark CSV the README quotes: the `.primary` pin first, then the - alphabetically-first machine. (`_data/benchmarks.js` additionally honors - $BASILISK_BENCH_PRIMARY and ranks unpinned CSVs by tool coverage; with the - committed `.primary` pin — the normal state — both resolve identically.)""" - pin = BENCH_STATUS_DIR / ".primary" - if pin.exists(): - csv = BENCH_STATUS_DIR / f"{pin.read_text(encoding='utf-8').strip()}.csv" - if csv.exists(): - return csv - csvs = sorted(BENCH_STATUS_DIR.glob("*.csv")) - return csvs[0] if csvs else None - - -def bench_values() -> dict[str, str]: - """Median cold check per tool + machine/count, read from the primary bench - CSV so the README table can never be a hand-typed figure. Empty when no CSV - exists (the markers are then left untouched, exactly like a missing score).""" - csv = _primary_bench_csv() - if csv is None: - return {} - cpu, header, rows = "", None, [] - for raw in csv.read_text(encoding="utf-8").splitlines(): - line = raw.strip() - if not line: - continue - if line.startswith("#"): - body = line[1:].strip() - if body.startswith("cpu:"): - cpu = body.split(":", 1)[1].strip() - continue - parts = line.split(",") - if header is None: - header = parts - else: - rows.append(parts) - if not header or not rows: - return {} - - col = { - name[:-3] if name.endswith("_ms") else name: i for i, name in enumerate(header) - } - - def median_for(tool: str) -> int | None: - i = col.get(tool) - if i is None: - return None - nums = [] - for r in rows: - if i < len(r) and r[i]: - try: - nums.append(float(r[i])) - except ValueError: - pass - return _median_ms(nums) - - vals: dict[str, str] = {} - for tool in BENCH_TOOLS: - m = median_for(tool) - if m is not None: - vals[f"bench{tool.capitalize()}"] = str(m) - warm = median_for("basilisk-warm") - if warm is not None: - vals["benchWarm"] = str(warm) - if cpu: - vals["benchMachine"] = cpu - vals["benchCount"] = str(len(rows)) - return vals - - -def stamp(text: str, vals: dict[str, str]) -> str: - """Refresh every inline marker and every commit-tree URL in `text`.""" - - def marker(match: re.Match[str]) -> str: - name = match.group("name") - value = vals.get(name) - if value is None: - return match.group(0) # unknown marker — leave it untouched - return f"{value}" - - text = MARKER_RE.sub(marker, text) - return TREE_SHA_RE.sub(lambda m: f"{m.group(1)}{vals['sha']}{m.group(2)}", text) - - -def main(argv: list[str]) -> int: - check = "--check" in argv - if not REPORT.exists(): - print( - f" ✗ {REPORT.relative_to(ROOT)} not found — run conformance/run_conformance.py first", - file=sys.stderr, - ) - return 1 - - report = json.loads(REPORT.read_text(encoding="utf-8")) - vals = values(report) - vals["sha"] = report["upstream"]["sha"] # full sha for the tree URLs only - vals.update(bench_values()) # median cold check per tool, from the primary CSV - - stale: list[Path] = [] - for path in TARGETS: - if not path.exists(): - continue - original = path.read_text(encoding="utf-8") - updated = stamp(original, vals) - if updated != original: - stale.append(path) - if not check: - path.write_text(updated, encoding="utf-8") - - if check: - if stale: - print( - " ✗ conformance docs are stale — run " - "scripts/gen_conformance_reference.py:", - file=sys.stderr, - ) - for path in stale: - print(f" - {path.relative_to(ROOT)}", file=sys.stderr) - return 1 - print(" conformance docs up to date.") - # A stamped source is only half the contract — the generated READMEs - # must carry the same figures ([README-STAMPED]). - return gen_readmes.main(["gen_readmes.py", "--check"]) - - if stale: - print(f" Stamped conformance {vals['score']} (commit {vals['short']}) into:") - for path in stale: - print(f" - {path.relative_to(ROOT)}") - else: - print(" conformance docs already up to date.") - # The published READMEs are rendered from the stamped sources ([README]); - # regenerating here keeps a stamp from ever landing without them. - return gen_readmes.main(["gen_readmes.py"]) - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/scripts/gen_readmes.py b/scripts/gen_readmes.py index 354e97de4..3c47a7632 100755 --- a/scripts/gen_readmes.py +++ b/scripts/gen_readmes.py @@ -2,12 +2,15 @@ # Implements [README]. See docs/specs/DOCS-README-SPEC.md """Render every published README from the single authored source. -Basilisk's front page is published to three storefronts — GitHub, the VS Code -Marketplace / Open VSX (one VSIX, one file), and PyPI. They used to be three -hand-maintained files, so they drifted ([README-PURPOSE]). Now -`docs/readme/README.src.md` (and its Chinese mirror) is the only authored copy, -and every published README is generated from it: identical except for one -paragraph saying which artifact the reader is looking at ([README-IDENTITY]). +Basilisk's front page is published to five storefronts — GitHub, the VS Code +Marketplace / Open VSX (one VSIX, one file), PyPI, the Zed extension registry, +and the Neovim plugin listing. They used to be hand-maintained files, so they +drifted ([README-PURPOSE]). Now +`docs/readme/README.src.md` is the only authored copy, and every published +README is generated from it: identical except for one paragraph saying which +artifact the reader is looking at ([README-IDENTITY]). The statement itself is +substituted from the messaging spec ([WITHDRAWAL-COPY]), so no storefront can be +edited into saying something the spec does not. Usage: python3 scripts/gen_readmes.py # rewrite the generated READMEs @@ -21,6 +24,8 @@ from dataclasses import dataclass from pathlib import Path +from gen_withdrawal_copy import copy_blocks + ROOT = Path(__file__).resolve().parents[1] SOURCE_DIR = ROOT / "docs" / "readme" @@ -40,7 +45,6 @@ class Target: key: str output: Path - alt_lang_href: str @dataclass(frozen=True) @@ -51,27 +55,19 @@ class Source: targets: tuple[Target, ...] -VSIX_README_EN = f"{REPO_BLOB}/vscode-extension/README.md" -VSIX_README_ZH = f"{REPO_BLOB}/vscode-extension/README.zh.md" - +# One language. The approved copy exists in English only +# ([WITHDRAWAL-COPY]); a Chinese README could only be an unapproved translation +# of a statement about being wrong, so the Chinese front pages are withdrawn +# rather than left carrying the old marketing. SOURCES = ( Source( path=SOURCE_DIR / "README.src.md", targets=( - Target("github", ROOT / "README.md", "README.zh.md"), - Target("vscode", ROOT / "vscode-extension" / "README.md", VSIX_README_ZH), - # The wheel listing is English-only; point its switch at the - # repository's Chinese front page rather than a page PyPI lacks. - Target("pypi", ROOT / "README-pypi.md", f"{REPO_BLOB}/README.zh.md"), - ), - ), - Source( - path=SOURCE_DIR / "README.zh.src.md", - targets=( - Target("github", ROOT / "README.zh.md", "README.md"), - Target( - "vscode", ROOT / "vscode-extension" / "README.zh.md", VSIX_README_EN - ), + Target("github", ROOT / "README.md"), + Target("vscode", ROOT / "vscode-extension" / "README.md"), + Target("pypi", ROOT / "README-pypi.md"), + Target("zed", ROOT / "basilisk-zed" / "README.md"), + Target("nvim", ROOT / "basilisk.nvim" / "README.md"), ), ), ) @@ -141,15 +137,34 @@ def html(match: re.Match[str]) -> str: return HTML_ATTR_RE.sub(html, MD_LINK_RE.sub(markdown, text)) +def withdrawal_tokens() -> dict[str, str]: + """`{{withdrawal:…}}` → the approved copy, as markdown. + + Implements [WITHDRAWAL-SURFACES]: a published README carries the statement, + and the statement has exactly one author — the messaging spec. Substituting + it here means a README cannot be edited into saying something else, and + `--check` fails the moment one is. + """ + copy = copy_blocks() + return { + "{{withdrawal:title}}": copy.title, + "{{withdrawal:line}}": copy.line, + "{{withdrawal:short}}": "\n\n".join(copy.short), + "{{withdrawal:action}}": "\n\n".join(copy.action), + "{{withdrawal:full}}": "\n\n".join(copy.full), + } + + def render(source_text: str, source_name: str, target: Target) -> str: """Render one target: variants, tokens, then link absolutisation. - The three [README-RENDER] transforms, in the order the spec fixes. Token - substitution is transform 2; `{{altLangHref}}` is a per-target expression of - one statement, not content ([README-IDENTITY]). + The three [README-RENDER] transforms, in the order the spec fixes. + `{{withdrawal:…}}` substitution is transform 2: the approved copy, identical + for every target. """ body = apply_variants(source_text, target.key) - body = body.replace("{{altLangHref}}", target.alt_lang_href) + for token, text in withdrawal_tokens().items(): + body = body.replace(token, text) if target.key != "github": body = absolutise_links(body) return GENERATED_BANNER.format(source=source_name) + body @@ -295,7 +310,7 @@ def main(argv: list[str]) -> int: listing = ", ".join(str(path.relative_to(ROOT)) for path in stale) print( f"gen_readmes: stale generated README(s): {listing}\n" - " Edit docs/readme/README.src.md (or its .zh source), then run:\n" + " Edit docs/readme/README.src.md, then run:\n" " python3 scripts/gen_readmes.py", file=sys.stderr, ) diff --git a/scripts/gen_rules_reference.py b/scripts/gen_rules_reference.py deleted file mode 100644 index 170d27fff..000000000 --- a/scripts/gen_rules_reference.py +++ /dev/null @@ -1,636 +0,0 @@ -#!/usr/bin/env python3 -"""Generate the canonical diagnostic-code reference from the checker source. - -Single source of truth: the diagnostic-code header — and the doc-comment body -beneath it — on each rule module under crates/basilisk-checker/src/rules/. A -header is either an opt-in `//! BSK-E####: ` (`E`, `W`, or `I`) code -or a PEP-conformance `//! `code_name`: ` code (the conformance -rules are named after their python/typing conformance test, e.g. -``//! `protocols_explicit`: ...``). Both styles are extracted so every code the -CLI can emit gets a page. - -Usage: - 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 - -This is the generator behind [WEBSITE-ERROR-PAGES-PURPOSE]: a landing page for -EVERY diagnostic code, built from the checker source so the pages can never drift -from the diagnostics the binary emits. -The `--data` output ([WEBSITE-ERROR-PAGES-DATA]) 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. The `--check` mode backs the CI drift guard -([WEBSITE-ERROR-PAGES-DRIFT]). 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 -from csv import DictReader -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent -RULES_DIR = ROOT / "crates" / "basilisk-checker" / "src" / "rules" -DEFAULT_DATA_OUT = ROOT / "website" / "src" / "_data" / "rules.json" -CONFORMANCE_STATUS = ROOT / "conformance" / "conformance_status.csv" -ERRORS_BASE_URL = "https://www.basilisk-python.dev/errors" -TYPING_SPEC_BASE_URL = "https://typing.python.org/en/latest/spec" - -# [WEBSITE-ERROR-PAGES-REFERENCES]: canonical documentation for every code. -# Each code-name prefix maps to its chapter of the maintained typing spec -# (https://typing.python.org/en/latest/spec/ — titles and filenames taken from -# that index verbatim). Conformance categories are named after these chapters -# upstream; the trailing entries cover Basilisk's general soundness rules whose -# prefix is not a conformance category. -SPEC_CHAPTER_BY_PREFIX = { - "aliases": ("Type aliases", "aliases.html"), - "annotations": ("Type annotations", "annotations.html"), - "callables": ("Callables", "callables.html"), - "classes": ("Class type assignability", "class-compat.html"), - "constructors": ("Constructors", "constructors.html"), - "dataclasses": ("Dataclasses", "dataclasses.html"), - "directives": ("Type checker directives", "directives.html"), - "enums": ("Enumerations", "enums.html"), - "exceptions": ("Exceptions", "exceptions.html"), - "generics": ("Generics", "generics.html"), - "historical": ("Historical and deprecated features", "historical.html"), - "literals": ("Literals", "literal.html"), - "namedtuples": ("Named Tuples", "namedtuples.html"), - "narrowing": ("Type narrowing", "narrowing.html"), - "overloads": ("Overloads", "overload.html"), - "protocols": ("Protocols", "protocol.html"), - "qualifiers": ("Type qualifiers", "qualifiers.html"), - "specialtypes": ("Special types in annotations", "special-types.html"), - "tuples": ("Tuples", "tuples.html"), - "typeddicts": ("Typed dictionaries", "typeddict.html"), - "typeforms": ("Type forms", "type-forms.html"), - "assignment": ("Type system concepts", "concepts.html"), - "calls": ("Callables", "callables.html"), - "dict": ("Type system concepts", "concepts.html"), - "imports": ("Distributing type information", "distributing.html"), - "match": ("Type narrowing", "narrowing.html"), - "returns": ("Type system concepts", "concepts.html"), - "version": ("Generics", "generics.html"), -} - -# The accepted typing PEPs each spec chapter incorporates — every rule under -# the prefix links these on top of any PEP its own doc comment cites. Numbers -# only; labels stay "PEP NNN" so nothing here can drift from peps.python.org. -PEPS_BY_PREFIX = { - "aliases": (484, 613, 695), - "annotations": (3107, 484, 526), - "callables": (484, 612, 692), - "classes": (484, 526, 698), - "constructors": (484,), - "dataclasses": (557, 681), - "directives": (484, 702), - "enums": (435,), - "generics": (484, 612, 646, 673, 695, 696), - "historical": (484,), - "literals": (586, 675), - "namedtuples": (484,), - "narrowing": (647, 742), - "overloads": (484,), - "protocols": (544,), - "qualifiers": (526, 591, 593), - "specialtypes": (484,), - "tuples": (484, 646), - "typeddicts": (589, 655, 705, 728), - "typeforms": (747,), - "assignment": (484,), - "calls": (484,), - "imports": (561,), - "match": (634,), - "returns": (484,), - "packaging": (621,), -} - -# Rules governed by something other than the typing spec link that authority -# instead: the Python language reference, or a tool's own documentation. -LANGUAGE_REFS_BY_PREFIX = { - "names": ( - { - "label": "Python language reference: Naming and binding", - "url": "https://docs.python.org/3/reference/executionmodel.html#naming-and-binding", - }, - ), - "uv": ( - { - "label": "uv: Locking and syncing", - "url": "https://docs.astral.sh/uv/concepts/projects/sync/", - }, - ), -} - -# House rules (BSK codes) carry no conformance-category prefix; each maps to -# the chapter/PEPs documenting the mechanism it polices. The suppression rules -# (BSK-0060..0063) police Basilisk's own directives — no upstream doc exists — -# and BSK-0025's doc comment already cites PEP 698 directly. -REFERENCE_PREFIX_BY_BSK_CODE = { - "BSK-0001": "annotations", - "BSK-0002": "annotations", - "BSK-0003": "annotations", - "BSK-0004": "annotations", - "BSK-0005": "annotations", - "BSK-0011": "packaging", - "BSK-0012": "packaging", - "BSK-0013": "uv", - "BSK-0014": "specialtypes", - "BSK-0040": "annotations", - "BSK-0050": "annotations", - "BSK-0152": "imports", -} - -HEADER = re.compile(r"//!\s*(BSK-\d{4}|`[a-z0-9_]+`):\s*(.*)") -DOC = re.compile(r"//!\s?(.*)") -PEP_MENTION = re.compile(r"\bPEP (\d{1,4})\b") -DOCS_URL = re.compile(r'docs_url:\s*"([^"]+)"') -SPEC_REF = re.compile(r"^Implements ") -# A rule is Basilisk-original (off by default, opt-in only) iff it overrides -# `opt_in_spec` to return `Some(..)`; core PEP-conformance rules leave it `None`. -# This reads the checker's real provenance signal (`Rule::opt_in_spec`, the single -# source of rule provenance per [CHKTAG-PROVENANCE]) — never the cosmetic `BSK-` -# code prefix, which [CHKTAG-BSK-PREFIX] declares semantically meaningless. -# `[^{]*` stops at the body brace, so a `Some(` in another fn can't false-match. -OPT_IN = re.compile(r"fn opt_in_spec\b[^{]*\{\s*Some\(") -# The free-form tags an opt-in rule declares (`tags: &["strictness", ..]`). These -# are the checker's own `OptInSpec.tags` — e.g. `strictness` marks the rules that -# make annotations mandatory beyond the spec. Non-greedy up to the first `tags:` -# inside the single opt_in_spec body; `TAG` pulls each quoted entry out. -OPT_IN_TAGS = re.compile( - r"fn opt_in_spec\b[^{]*\{\s*Some\([\s\S]*?tags:\s*&\[([^\]]*)\]" -) -TAG = re.compile(r'"([^"]+)"') - -# Coarse groups for filtering/badging on the website, derived from the rule's -# own tags — codes carry no severity class ([CHKARCH-DIAG-CODES]). -GROUP_BY_TAG = { - "strictness": "Missing Annotations", - "style": "Style", - "redundancy": "Redundancy", - "suppressions": "Suppressions", - "dependencies": "Dependencies", - "imports": "Imports", - "stubs": "Stubs", -} - - -def pep_categories() -> frozenset[str]: - """Read the canonical python/typing category vocabulary used by Basilisk. - - The checker validates the same CSV-backed vocabulary in [CHKTAG-TESTS]. - Reading it here keeps the website consumer on that source instead of - maintaining a parallel category list. - """ - with CONFORMANCE_STATUS.open(encoding="utf-8", newline="") as handle: - return frozenset( - row["category"] for row in DictReader(handle) if row.get("category") - ) - - -PEP_CATEGORIES = pep_categories() - - -def clean(text: str) -> str: - return re.sub(r"\s+", " ", text.strip().rstrip(".").strip()) - - -def is_bsk(code: str) -> bool: - return code.startswith("BSK-") - - -def scope_for(provenance: str) -> str: - # The command partition [CHKARCH-COMMANDS]: pep-tagged rules belong to - # `basilisk check` (always run); everything else to `basilisk analyze`. - return "check" if provenance == "pep" else "analyze" - - -def sort_key(code: str) -> tuple[int, int, str]: - # BSK codes first (numeric), then named conformance codes alphabetically. - if is_bsk(code): - return (0, int(code[4:]), "") - return (1, 0, code) - - -def group_for(code: str, free_form_tags: list[str]) -> str: - if not is_bsk(code): - # Named conformance rules span the broad type-system surface. - return "Type System" - for tag in free_form_tags: - if tag in GROUP_BY_TAG: - return GROUP_BY_TAG[tag] - return "Type System" - - -def pep_url(number: int) -> str: - return f"https://peps.python.org/pep-{number:04d}/" - - -def link_peps(text: str) -> str: - """Turn every `PEP NNN` mention into a link to its canonical page.""" - return PEP_MENTION.sub( - lambda m: f'PEP {int(m.group(1))}', - text, - ) - - -def inline_html(text: str) -> str: - """Render a rustdoc line as safe inline HTML: intra-doc links unwrapped, - `code` spans and *emphasis* preserved, PEP mentions linked - ([WEBSITE-ERROR-PAGES-REFERENCES]).""" - 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 link_peps(text) - - -# Implements [WEBSITE-ERROR-PAGES-REFERENCES]: the canonical-documentation list -# for one code — its typing-spec chapter, then the chapter's PEPs merged with -# every PEP the rule's own doc comment cites, then any language-reference link. -def references_for(code: str, doc_text: str) -> list[dict]: - prefix = REFERENCE_PREFIX_BY_BSK_CODE.get(code, code.partition("_")[0]) - refs: list[dict] = [] - chapter = SPEC_CHAPTER_BY_PREFIX.get(prefix) - if chapter: - title, page = chapter - refs.append( - { - "label": f"Typing spec: {title}", - "url": f"{TYPING_SPEC_BASE_URL}/{page}", - } - ) - mentioned = {int(n) for n in PEP_MENTION.findall(doc_text)} - for number in sorted(mentioned.union(PEPS_BY_PREFIX.get(prefix, ()))): - refs.append({"label": f"PEP {number}", "url": pep_url(number)}) - refs.extend(LANGUAGE_REFS_BY_PREFIX.get(prefix, ())) - return refs - - -# [STUBRES-TYPESHED-WARN] / [STUBRES-TYPESHED-CONFIG]: the typeshed -# source-status advisories are Basilisk's OWN house diagnostics, emitted by the -# stub-resolution layer (crates/basilisk-stubs/src/typeshed/warning.rs) rather -# than a checker rule, so the file-scanning extractor below never sees them. -# They deep-link to their own /errors/ page and are graded like any rule -# via [tool.basilisk.rules] / [tool.basilisk.rule-tags], so they belong in the -# same generated reference as every other code. Documented here from the shared -# spec (docs/specs/CHECKER-STUB-RESOLUTION-SPEC.md#STUBRES-TYPESHED-WARN); the -# prose is kept in agreement with the Rust `message()` and the spec status table. -TYPESHED_STATUS_REFERENCES = [ - {"label": "python/typeshed", "url": "https://github.com/python/typeshed"}, - { - "label": "Basilisk configuration: typeshed source", - "url": "https://www.basilisk-python.dev/docs/configuration/", - }, -] - -TYPESHED_STATUS_SPECS = [ - { - "code": "typeshed_source_unpinned", - "summary": ( - "The active typeshed source is not pinned to an exact commit, so " - "type checks are not reproducible across machines and CI" - ), - "body": [ - ( - "text", - "Basilisk type-checks your code against `typeshed`, the " - "community's standard-library and third-party type stubs. Which " - "revision of typeshed is active decides which symbols and " - "signatures exist, so two machines resolving different typeshed " - "contents can disagree about whether the same code type-checks.", - ), - ( - "text", - "Basilisk bundles a vetted typeshed snapshot inside the binary " - "and serves it by default. A build-time snapshot is not a *user* " - "pin: upgrade Basilisk and the snapshot moves. When no " - "`typeshed-commit` is set — or when a custom `typeshed-path` " - "folder is used, whose contents can change on disk — Basilisk " - "raises this advisory to say the type-checking baseline is not " - "reproducible.", - ), - ( - "text", - "Pin an exact `python/typeshed` commit so every machine and CI " - "run resolves byte-identical stubs. A pin fails closed — " - "Basilisk never silently substitutes another commit:", - ), - ( - "code", - "toml", - '[tool.basilisk]\ntypeshed-commit = "…full 40-character SHA…"', - ), - ( - "text", - "This is an ordinary Basilisk diagnostic. Grade it like any rule " - "— raise it to an error in CI, or silence it once you have " - "accepted the unpinned default:", - ), - ( - "code", - "toml", - "[tool.basilisk.rules]\n" - '"typeshed_source_unpinned" = "error" # or "off" to silence', - ), - ( - "text", - "It is reported out of band — on the CLI's stderr banner, in the " - "editor's Server Info panel, and as MCP status — and never as a " - "Python diagnostic, so it can never affect conformance.", - ), - ], - "references": TYPESHED_STATUS_REFERENCES, - }, - { - "code": "typeshed_source_user_managed", - "summary": ( - "A custom typeshed folder is user-managed: you supply its license " - "and contents, so typeshed's license terms are not applied to it" - ), - "body": [ - ( - "text", - "When you point Basilisk at a custom `typeshed-path` folder, " - "Basilisk treats it as user-managed: you supply both its " - "contents and its license. Basilisk does not attach " - "`python/typeshed`'s license terms to a tree it did not vet.", - ), - ( - "text", - "This advisory makes that explicit so you never unintentionally " - "rely on a custom tree believing it carries typeshed's license, " - "or skip the pin and content verification that the bundled and " - "pinned sources enforce.", - ), - ( - "text", - "It composes with `typeshed_source_unpinned` — a custom folder " - "is both unpinned and user-managed. Grade it like any rule:", - ), - ( - "code", - "toml", - "[tool.basilisk.rules]\n" - '"typeshed_source_user_managed" = "warning" # or "off" to silence', - ), - ( - "text", - "It is reported out of band (CLI banner, Server Info, MCP " - "status), never as a Python diagnostic, so it can never affect " - "conformance.", - ), - ], - "references": TYPESHED_STATUS_REFERENCES, - }, - { - "code": "typeshed_source_license_changed", - "summary": ( - "The bundled typeshed's approved LICENSE/NOTICE changed and " - "activation was blocked pending review" - ), - "body": [ - ( - "text", - "Basilisk vets the LICENSE and NOTICE files of the typeshed " - "snapshot it bundles at build time and records their exact " - "identity. If those legal files no longer match what was " - "approved, Basilisk refuses to serve the stubs rather than " - "distribute content under unknown terms.", - ), - ( - "text", - "This condition is elevated: it defaults to `error`, and " - "analysis for the affected root does not run until it is " - "resolved. Update Basilisk to a build whose bundled typeshed " - "license is approved again.", - ), - ( - "text", - "Like any Basilisk diagnostic it can be graded, though lowering " - "it does not make the underlying license mismatch safe:", - ), - ( - "code", - "toml", - '[tool.basilisk.rules]\n"typeshed_source_license_changed" = "error"', - ), - ( - "text", - "It is reported out of band (CLI banner, an editor " - "`window/showMessage`, MCP status), never as a Python " - "diagnostic, so it can never affect conformance.", - ), - ], - "references": [ - { - "label": "python/typeshed LICENSE", - "url": "https://github.com/python/typeshed/blob/main/LICENSE", - }, - *TYPESHED_STATUS_REFERENCES, - ], - }, -] - - -def typeshed_status_records() -> list[dict]: - """The three typeshed source-status advisories as reference records. - - Implements [WEBSITE-ERROR-PAGES-PURPOSE] for the stub-resolution - advisories: they get the SAME /errors/ pages as every checker code, - built from a single description that agrees with the Rust `message()` and - the spec status table ([STUBRES-TYPESHED-WARN]). - """ - records: list[dict] = [] - for spec in TYPESHED_STATUS_SPECS: - body: list[dict] = [] - for block in spec["body"]: - if block[0] == "text": - body.append({"type": "text", "html": inline_html(block[1])}) - else: - body.append({"type": "code", "lang": block[1], "code": block[2]}) - summary = clean(spec["summary"]) - records.append( - { - "code": spec["code"], - "scope": "analyze", - "provenance": "basilisk", - "tags": ["basilisk", "stubs"], - "summary": summary, - "summaryHtml": inline_html(summary), - "body": body, - "group": "Stubs", - "docsUrl": f"{ERRORS_BASE_URL}/{spec['code']}", - "references": spec["references"], - } - ) - return records - - -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 - - -# Implements [WEBSITE-ERROR-PAGES-PURPOSE]: build one record per diagnostic code -# directly from the checker rule sources, so the generated /errors// pages -# can never drift from the diagnostics the binary actually emits. -def extract() -> list[dict]: - """One record per code, including its canonical checker tag set.""" - 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) - # Provenance and opt-in tags come from the rule's own opt_in_spec, not - # its cosmetic code prefix. PEP category tags use the same canonical - # conformance CSV vocabulary validated by rule_tags.rs. - provenance = "basilisk" if OPT_IN.search(text) else "pep" - tags_match = OPT_IN_TAGS.search(text) - free_form_tags = TAG.findall(tags_match.group(1)) if tags_match else [] - for i, line in enumerate(lines): - m = HEADER.match(line.strip()) - if not m: - continue - code, summary = m.group(1).strip("`"), 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)}" - category = code.partition("_")[0] - tags = ( - ["basilisk", *free_form_tags] - if provenance == "basilisk" - else ["pep", *([category] if category in PEP_CATEGORIES else [])] - ) - records[code] = { - "code": code, - "scope": scope_for(provenance), - "provenance": provenance, - "tags": tags, - "summary": clean(summary), - "summaryHtml": inline_html(clean(summary)), - "body": parse_body(body_lines), - "group": group_for(code, free_form_tags), - "docsUrl": file_docs_url.group(1) - if file_docs_url - else f"{ERRORS_BASE_URL}/{code}", - "references": references_for(code, " ".join([summary, *body_lines])), - } - # The stub-resolution advisories live outside RULES_DIR (they are not checker - # rules) but earn the same /errors/ pages ([STUBRES-TYPESHED-WARN]). - for record in typeshed_status_records(): - records.setdefault(record["code"], record) - return [records[c] for c in sorted(records, key=sort_key)] - - -def to_markdown(records: list[dict]) -> str: - rows = ["| Code | Description |", "|---|---|"] - for r in records: - rows.append(f"| `{r['code']}` | {r['summary']} |") - return "\n".join(rows) - - -def main() -> int: - records = extract() - if "--json" in sys.argv: - print(json.dumps({r["code"]: r["summary"] for r in records}, indent=2)) - return 0 - if "--data" in sys.argv: - # [WEBSITE-ERROR-PAGES-DATA]: write website/src/_data/rules.json — one - # record per code (summary, body blocks, scope, group, docsUrl). - 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") - check = sum(r["scope"] == "check" for r in records) - analyze = len(records) - check - print( - f"Wrote {len(records)} codes ({check} check-scope PEP rules, " - f"{analyze} analyze-scope Basilisk rules) -> {out}" - ) - return 0 - if "--check" in sys.argv: - # [WEBSITE-ERROR-PAGES-DRIFT]: assert FILE contains every current code so - # CI fails when a rule is added/renamed without regenerating rules.json. - target = Path(sys.argv[sys.argv.index("--check") + 1]).read_text( - encoding="utf-8" - ) - missing = [r["code"] for r in records if r["code"] not in target] - if missing: - print(f"MISSING {len(missing)} codes: {', '.join(missing)}") - return 1 - print(f"OK: all {len(records)} codes present") - return 0 - print(to_markdown(records)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/gen_withdrawal_copy.py b/scripts/gen_withdrawal_copy.py new file mode 100644 index 000000000..9150d9652 --- /dev/null +++ b/scripts/gen_withdrawal_copy.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +"""Extract the approved withdrawal copy from the messaging spec into site data. + +Implements [WITHDRAWAL-COPY]. The single source of truth for everything Basilisk +says publicly is docs/specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md; this script lifts +its approved copy blocks out into website/src/_data/withdrawal.json so the site +renders the spec's words rather than a hand-typed copy of them. `copy_blocks()` +serves the same text as markdown to scripts/gen_readmes.py, so the site and every +published README are two renderings of one source. + + python3 scripts/gen_withdrawal_copy.py # write the data file + python3 scripts/gen_withdrawal_copy.py --check # fail if it has drifted + +Run --check in CI: the site must never say something the spec does not. +""" + +from __future__ import annotations + +import argparse +import html +import json +import re +import sys +from dataclasses import dataclass +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +SPEC_PATH = REPO_ROOT / "docs/specs/DOCS-WITHDRAWAL-MESSAGING-SPEC.md" +DATA_PATH = REPO_ROOT / "website/src/_data/withdrawal.json" +# The CLI and the extension print the SAME bytes the spec authored: both read a +# generated file rather than a hand-typed string ([WITHDRAWAL-INERT-TEXT]). +CLI_NOTICE_PATH = REPO_ROOT / "crates/basilisk-cli/src/withdrawal_notice.txt" +VSIX_NOTICE_PATH = REPO_ROOT / "vscode-extension/src/withdrawal-notice.ts" + +# The anchors naming each approved block in the spec. +ANCHOR_LINE = "{#WITHDRAWAL-COPY-LINE}" +ANCHOR_SHORT = "{#WITHDRAWAL-COPY-SHORT}" +ANCHOR_ACTION = "{#WITHDRAWAL-COPY-ACTION}" +ANCHOR_FULL = "{#WITHDRAWAL-COPY-FULL}" +ANCHOR_NOTICE = "{#WITHDRAWAL-INERT-TEXT}" + +CODE_RE = re.compile(r"`([^`]+)`") +LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)") +BOLD_RE = re.compile(r"\*\*([^*]+)\*\*") +ITALIC_RE = re.compile(r"\*([^*]+)\*") + + +class SpecError(RuntimeError): + """The spec is missing a block this script is required to publish.""" + + +def blockquote_after(lines: list[str], anchor: str) -> list[str]: + """Return the paragraphs of the blockquote following `anchor`. + + Paragraphs are joined to one line each: the spec is authored unwrapped, but a + blockquote may still carry several paragraphs separated by a bare `>`. + """ + try: + start = next(i for i, line in enumerate(lines) if anchor in line) + except StopIteration: + raise SpecError(f"{SPEC_PATH.name} has no {anchor} block") from None + + cursor = start + 1 + while cursor < len(lines) and not lines[cursor].strip(): + cursor += 1 + + quoted: list[str] = [] + while cursor < len(lines) and lines[cursor].startswith(">"): + quoted.append(lines[cursor].removeprefix(">").strip()) + cursor += 1 + + if not quoted: + raise SpecError(f"{anchor} in {SPEC_PATH.name} is not followed by a blockquote") + + # A bare `>` closes a paragraph; consecutive text lines join, so the block + # survives an editor re-wrapping the spec. + paragraphs: list[str] = [] + open_paragraph = False + for chunk in quoted: + if not chunk: + open_paragraph = False + elif open_paragraph: + paragraphs[-1] = f"{paragraphs[-1]} {chunk}" + else: + paragraphs.append(chunk) + open_paragraph = True + return paragraphs + + +def fenced_after(lines: list[str], anchor: str) -> str: + """Return the fenced code block following `anchor`, verbatim. + + This is the text the inert CLI and the extension print, so it is lifted + byte-for-byte: no wrapping, no markdown, no substitution. + """ + try: + start = next(i for i, line in enumerate(lines) if anchor in line) + except StopIteration: + raise SpecError(f"{SPEC_PATH.name} has no {anchor} block") from None + + opened = False + body: list[str] = [] + for line in lines[start + 1 :]: + if line.startswith("```"): + if opened: + return "\n".join(body) + "\n" + opened = True + elif opened: + body.append(line) + raise SpecError(f"{anchor} in {SPEC_PATH.name} has no closing code fence") + + +def to_html(markdown: str) -> str: + """Render the inline markdown the approved copy uses, and nothing else. + + Escaping runs first so the spec's text can never inject markup; the patterns + below then reintroduce exactly the four inline constructs the copy contains. + """ + text = html.escape(markdown, quote=False) + text = CODE_RE.sub(r"\1", text) + text = LINK_RE.sub(r'\1', text) + text = BOLD_RE.sub(r"\1", text) + return ITALIC_RE.sub(r"\1", text) + + +@dataclass(frozen=True) +class Copy: + """The approved blocks, as the markdown the spec authored. + + Each consumer renders this for its own medium: the site converts to HTML, + the published READMEs use the markdown unchanged. + """ + + line: str + title: str + short: tuple[str, ...] + action: tuple[str, ...] + full: tuple[str, ...] + + +def copy_blocks() -> Copy: + """Extract every approved block from the messaging spec.""" + lines = SPEC_PATH.read_text(encoding="utf-8").splitlines() + + one_line = blockquote_after(lines, ANCHOR_LINE) + if len(one_line) != 1: + raise SpecError(f"{ANCHOR_LINE} must be exactly one paragraph") + + full = blockquote_after(lines, ANCHOR_FULL) + if not full or not full[0].startswith("# "): + raise SpecError(f"{ANCHOR_FULL} must open with a level-1 heading") + + return Copy( + line=one_line[0], + title=full[0].removeprefix("# ").strip(), + short=tuple(blockquote_after(lines, ANCHOR_SHORT)), + action=tuple(blockquote_after(lines, ANCHOR_ACTION)), + full=tuple(full[1:]), + ) + + +def build() -> dict[str, object]: + """Assemble the site data payload from the spec's approved blocks.""" + copy = copy_blocks() + return { + "_generated": f"Generated from {SPEC_PATH.relative_to(REPO_ROOT)} " + "by scripts/gen_withdrawal_copy.py — DO NOT EDIT.", + "line": copy.line, + "title": copy.title, + "short": [to_html(p) for p in copy.short], + "action": [to_html(p) for p in copy.action], + "full": [to_html(p) for p in copy.full], + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--check", + action="store_true", + help="verify the data file matches the spec instead of writing it", + ) + args = parser.parse_args() + + try: + payload = json.dumps(build(), indent=2, ensure_ascii=False) + "\n" + except SpecError as error: + print(f"error: {error}", file=sys.stderr) + return 1 + + if not args.check: + DATA_PATH.write_text(payload, encoding="utf-8") + print(f"wrote {DATA_PATH.relative_to(REPO_ROOT)}") + return 0 + + current = DATA_PATH.read_text(encoding="utf-8") if DATA_PATH.exists() else "" + if current == payload: + return 0 + print( + f"error: {DATA_PATH.relative_to(REPO_ROOT)} has drifted from " + f"{SPEC_PATH.relative_to(REPO_ROOT)}. Run: python3 scripts/gen_withdrawal_copy.py", + file=sys.stderr, + ) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/test_published_readmes.py b/scripts/test_published_readmes.py new file mode 100644 index 000000000..e6a90b425 --- /dev/null +++ b/scripts/test_published_readmes.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""The published READMEs carry the statement and nothing it forbids. + +Implements [WITHDRAWAL-SURFACES]. Every storefront front page — GitHub, the VSIX +on Marketplace and Open VSX, PyPI, Zed, Neovim — is generated from +docs/readme/README.src.md with the statement substituted from the messaging spec +([WITHDRAWAL-COPY]). `gen_readmes.py --check` proves they match their source; +these tests prove the source still says the right thing, and that no hand-authored +part of it reintroduces something [WITHDRAWAL-PROHIBITED] bars. + + python3 -m pytest scripts/test_published_readmes.py +""" + +from __future__ import annotations + +import re +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from gen_readmes import SOURCES # noqa: E402 +from gen_withdrawal_copy import copy_blocks # noqa: E402 + +REPO_ROOT = Path(__file__).resolve().parents[1] + +PUBLISHED = tuple(target.output for source in SOURCES for target in source.targets) + +APOLOGY = "https://www.christianfindlay.com/blog/basilisk-conformance-apology" + +# Each pattern is something a front page must never say again. Anchored on the +# rendered markdown, so a link, a badge, or a code fence all count. +FORBIDDEN = ( + ("a conformance or pass-rate figure", re.compile(r"\d+(\.\d+)?\s*%")), + ("install instructions", re.compile(r"\b(pip|pipx|uv tool|brew|scoop|npm)\s+install\b", re.I)), + ("an editor install link", re.compile(r"vscode:extension", re.I)), + ("a marketplace or package listing link", re.compile(r"marketplace\.visualstudio\.com|open-vsx\.org|pypi\.org", re.I)), + ("a competitor comparison", re.compile(r"\b(pyright|mypy|pyrefly|zuban|pylance)\b", re.I)), + ("a benchmark claim", re.compile(r"\bbenchmark|\bfastest\b", re.I)), + ("a rule catalogue", re.compile(r"\bBSK-\d{4}\b")), + ("a `basilisk` invocation", re.compile(r"\bbasilisk (check|analyze|fix|lsp)\b")), +) + + +class PublishedReadmes(unittest.TestCase): + """Every storefront front page, as it will be published.""" + + def setUp(self) -> None: + self.readmes = {path: path.read_text(encoding="utf-8") for path in PUBLISHED} + self.assertTrue(self.readmes, "no published README targets are declared") + + def test_every_readme_opens_with_the_statement(self) -> None: + copy = copy_blocks() + for path, text in self.readmes.items(): + with self.subTest(readme=path.relative_to(REPO_ROOT)): + self.assertIn(f"# {copy.title}", text) + for paragraph in copy.full: + self.assertIn(paragraph, text) + + def test_every_readme_tells_the_reader_what_to_do(self) -> None: + # The action block is the only part that asks something of the reader, + # so it is the part most likely to be trimmed for length. + copy = copy_blocks() + for path, text in self.readmes.items(): + with self.subTest(readme=path.relative_to(REPO_ROOT)): + for paragraph in copy.action: + self.assertIn(paragraph, text) + self.assertIn("Remove Basilisk from your pipeline", text) + + def test_every_readme_links_the_apology_without_quoting_it(self) -> None: + for path, text in self.readmes.items(): + with self.subTest(readme=path.relative_to(REPO_ROOT)): + self.assertIn(APOLOGY, text) + self.assertNotRegex(text, r"I (was|am) (wrong|sorry)|in my own words") + + def test_no_readme_says_anything_prohibited(self) -> None: + for path, text in self.readmes.items(): + for label, pattern in FORBIDDEN: + with self.subTest(readme=path.relative_to(REPO_ROOT), forbidden=label): + self.assertIsNone( + pattern.search(text), + f"{path.relative_to(REPO_ROOT)} contains {label}", + ) + + def test_no_readme_shows_a_product_image(self) -> None: + # Screenshots are release evidence for a product that is being delisted; + # a marketing image beside a withdrawal notice reads as still selling. + for path, text in self.readmes.items(): + with self.subTest(readme=path.relative_to(REPO_ROOT)): + self.assertNotRegex(text, r"!\[[^\]]*\]\(| -