Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ nohup.out
tmp/
temp/
scratch/
scratchpad/
URGENT_READ_ME_NOW.md


Expand Down
28 changes: 14 additions & 14 deletions conformance/conformance_status.csv
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ BSK-E0048,aliases_explicit.py,aliases,PASS,21,0,0
BSK-E0047|BSK-E0048|BSK-E0092,aliases_implicit.py,aliases,PASS,22,0,0
BSK-E0014|BSK-E0050,aliases_newtype.py,aliases,PASS,14,0,0
BSK-E0014|BSK-E0104,aliases_recursive.py,aliases,PASS,11,0,0
BSK-E0057|BSK-E0149,aliases_type_statement.py,aliases,FAIL,24,1,0
BSK-E0057|BSK-E0149,aliases_type_statement.py,aliases,PASS,24,0,0
BSK-E0151,aliases_typealiastype.py,aliases,PASS,22,0,0
BSK-E0107,aliases_variance.py,aliases,PASS,4,0,0
,annotations_coroutines.py,annotations,PASS,0,0,0
Expand All @@ -23,7 +23,7 @@ BSK-E0012|BSK-E0140|BSK-E0141,callables_kwargs.py,callables,PASS,13,0,0
BSK-E0140,callables_protocol.py,callables,PASS,17,0,0
BSK-E0014|BSK-E0136,callables_subtyping.py,callables,PASS,32,0,0
BSK-E0014|BSK-E0036|BSK-E0044|BSK-E0121,classes_classvar.py,classes,PASS,17,0,0
,classes_override.py,classes,FAIL,0,5,0
BSK-E0159,classes_override.py,classes,PASS,0,0,0
BSK-E0111|BSK-E0128,constructors_call_init.py,constructors,PASS,5,0,0
BSK-E0041,constructors_call_metaclass.py,constructors,PASS,2,0,0
BSK-E0074,constructors_call_new.py,constructors,PASS,2,0,0
Expand All @@ -39,13 +39,13 @@ BSK-E0069,dataclasses_kwonly.py,dataclasses,PASS,3,0,0
BSK-E0059,dataclasses_match_args.py,dataclasses,PASS,1,0,0
BSK-E0060,dataclasses_order.py,dataclasses,PASS,1,0,0
BSK-E0095,dataclasses_postinit.py,dataclasses,PASS,4,0,0
BSK-E0108,dataclasses_slots.py,dataclasses,FAIL,4,1,0
BSK-E0108,dataclasses_slots.py,dataclasses,PASS,4,0,0
BSK-E0142,dataclasses_transform_class.py,dataclasses,PASS,6,0,0
BSK-E0142,dataclasses_transform_converter.py,dataclasses,PASS,9,0,0
BSK-E0069,dataclasses_transform_field.py,dataclasses,PASS,2,0,0
BSK-E0014|BSK-E0052|BSK-E0060|BSK-E0069|BSK-E0111,dataclasses_transform_func.py,dataclasses,PASS,5,0,0
BSK-E0138,dataclasses_transform_meta.py,dataclasses,PASS,6,0,0
BSK-E0005|BSK-E0041|BSK-E0069|BSK-E0096,dataclasses_usage.py,dataclasses,FAIL,8,3,0
BSK-E0005|BSK-E0041|BSK-E0069|BSK-E0096|BSK-E0157,dataclasses_usage.py,dataclasses,PASS,8,0,0
BSK-E0039|BSK-E0053,directives_assert_type.py,directives,PASS,7,0,0
BSK-E0031,directives_cast.py,directives,PASS,3,0,0
BSK-E0115,directives_deprecated.py,directives,PASS,12,0,0
Expand All @@ -56,7 +56,7 @@ BSK-E0033,directives_reveal_type.py,directives,PASS,2,0,0
,directives_type_ignore_file1.py,directives,PASS,0,0,0
BSK-E0014,directives_type_ignore_file2.py,directives,PASS,1,0,0
BSK-E0150,directives_version_platform.py,directives,PASS,3,0,0
BSK-E0040,enums_behaviors.py,enums,FAIL,1,2,0
BSK-E0040|BSK-E0061,enums_behaviors.py,enums,PASS,1,0,0
,enums_definition.py,enums,PASS,0,0,0
BSK-E0061,enums_expansion.py,enums,PASS,1,0,0
,enums_member_names.py,enums,PASS,0,0,0
Expand All @@ -65,14 +65,14 @@ BSK-E0046|BSK-E0067,enums_members.py,enums,PASS,7,0,0
,exceptions_context_managers.py,exceptions,PASS,0,0,0
BSK-E0027|BSK-E0047|BSK-E0092|BSK-E0132|BSK-E0134,generics_base_class.py,generics,PASS,7,0,0
BSK-E0026|BSK-E0027|BSK-E0043|BSK-E0148,generics_basic.py,generics,PASS,13,0,0
BSK-E0030|BSK-E0091|BSK-E0092,generics_defaults.py,generics,FAIL,5,1,0
BSK-E0030|BSK-E0053|BSK-E0091|BSK-E0092,generics_defaults.py,generics,PASS,5,0,0
BSK-E0102|BSK-E0128|BSK-E0130,generics_defaults_referential.py,generics,PASS,7,0,0
BSK-E0014|BSK-E0092,generics_defaults_specialization.py,generics,PASS,3,0,0
BSK-E0026|BSK-E0047,generics_paramspec_basic.py,generics,PASS,7,0,0
BSK-E0122,generics_paramspec_components.py,generics,PASS,16,0,0
BSK-E0122,generics_paramspec_semantics.py,generics,PASS,9,0,0
BSK-E0092|BSK-E0122,generics_paramspec_specialization.py,generics,PASS,5,0,0
BSK-E0117|BSK-E0130,generics_scoping.py,generics,FAIL,10,4,0
BSK-E0053|BSK-E0117|BSK-E0130,generics_scoping.py,generics,PASS,10,0,0
,generics_self_advanced.py,generics,PASS,0,0,0
BSK-E0075,generics_self_attributes.py,generics,PASS,2,0,0
BSK-E0078,generics_self_basic.py,generics,PASS,3,0,0
Expand All @@ -84,13 +84,13 @@ BSK-E0055|BSK-E0130,generics_syntax_infer_variance.py,generics,PASS,18,0,0
BSK-E0149,generics_syntax_scoping.py,generics,PASS,7,0,0
BSK-E0111|BSK-E0125,generics_type_erasure.py,generics,PASS,7,0,0
BSK-E0085,generics_typevartuple_args.py,generics,PASS,8,0,0
BSK-E0055|BSK-E0083|BSK-E0084|BSK-E0085|BSK-E0086,generics_typevartuple_basic.py,generics,FAIL,13,1,0
BSK-E0055|BSK-E0083|BSK-E0084|BSK-E0085|BSK-E0086,generics_typevartuple_basic.py,generics,PASS,13,0,0
BSK-E0082,generics_typevartuple_callable.py,generics,PASS,1,0,0
,generics_typevartuple_concat.py,generics,PASS,0,0,0
,generics_typevartuple_overloads.py,generics,PASS,0,0,0
BSK-E0086|BSK-E0130|BSK-E0139,generics_typevartuple_specialization.py,generics,PASS,6,0,0
BSK-E0081,generics_typevartuple_unpack.py,generics,PASS,1,0,0
BSK-E0026|BSK-E0055|BSK-E0080,generics_upper_bound.py,generics,FAIL,3,1,0
BSK-E0026|BSK-E0053|BSK-E0055|BSK-E0080,generics_upper_bound.py,generics,PASS,3,0,0
BSK-E0055|BSK-E0107,generics_variance.py,generics,PASS,9,0,0
BSK-E0130,generics_variance_inference.py,generics,PASS,23,0,0
BSK-E0071,historical_positional.py,historical,PASS,4,0,0
Expand All @@ -105,8 +105,8 @@ BSK-E0143,namedtuples_usage.py,namedtuples,PASS,8,0,0
BSK-E0101|BSK-E0112,narrowing_typeguard.py,narrowing,PASS,4,0,0
BSK-E0101|BSK-E0112|BSK-E0113,narrowing_typeis.py,narrowing,PASS,9,0,0
BSK-E0072,overloads_basic.py,overloads,PASS,1,0,0
,overloads_consistency.py,overloads,FAIL,0,2,0
BSK-E0020|BSK-E0034,overloads_definitions.py,overloads,FAIL,0,7,0
BSK-E0160,overloads_consistency.py,overloads,PASS,0,0,0
BSK-E0020|BSK-E0034|BSK-E0158|BSK-E0159,overloads_definitions.py,overloads,PASS,0,0,0
BSK-E0012|BSK-E0041|BSK-E0076,overloads_evaluation.py,overloads,PASS,4,0,0
BSK-E0099|BSK-E0146,protocols_class_objects.py,protocols,PASS,8,0,0
BSK-E0036|BSK-E0097|BSK-E0121,protocols_definition.py,protocols,PASS,21,0,0
Expand All @@ -121,18 +121,18 @@ BSK-E0014|BSK-E0099,protocols_subtyping.py,protocols,PASS,7,0,0
BSK-E0110|BSK-E0133,protocols_variance.py,protocols,PASS,5,0,0
BSK-E0045|BSK-E0058,qualifiers_annotated.py,qualifiers,PASS,20,0,0
BSK-E0014|BSK-E0041|BSK-E0044|BSK-E0054|BSK-E0064,qualifiers_final_annotation.py,qualifiers,PASS,26,0,0
BSK-E0010|BSK-E0034,qualifiers_final_decorator.py,qualifiers,FAIL,3,3,1
BSK-E0034|BSK-E0158,qualifiers_final_decorator.py,qualifiers,PASS,3,0,0
,specialtypes_any.py,specialtypes,PASS,0,0,0
BSK-E0062|BSK-E0070,specialtypes_never.py,specialtypes,PASS,3,0,0
BSK-E0012|BSK-E0014,specialtypes_none.py,specialtypes,PASS,3,0,0
BSK-E0065,specialtypes_promotions.py,specialtypes,PASS,1,0,0
BSK-E0015|BSK-E0092|BSK-E0145,specialtypes_type.py,specialtypes,PASS,9,0,0
BSK-E0014|BSK-E0023|BSK-E0045|BSK-E0147,tuples_type_compat.py,tuples,FAIL,16,0,2
BSK-E0014|BSK-E0045|BSK-E0147,tuples_type_compat.py,tuples,PASS,16,0,0
BSK-E0014|BSK-E0049|BSK-E0090,tuples_type_form.py,tuples,PASS,11,0,0
BSK-E0049,tuples_unpacked.py,tuples,PASS,4,0,0
BSK-E0037,typeddicts_alt_syntax.py,typeddicts,PASS,4,0,0
BSK-E0029|BSK-E0032,typeddicts_class_syntax.py,typeddicts,PASS,3,0,0
BSK-E0014|BSK-E0093|BSK-E0141|BSK-E0156,typeddicts_extra_items.py,typeddicts,FAIL,23,5,0
BSK-E0014|BSK-E0093|BSK-E0141|BSK-E0156,typeddicts_extra_items.py,typeddicts,PASS,23,0,0
,typeddicts_final.py,typeddicts,PASS,0,0,0
BSK-E0038,typeddicts_inheritance.py,typeddicts,PASS,2,0,0
BSK-E0093,typeddicts_operations.py,typeddicts,PASS,11,0,0
Expand Down
29 changes: 22 additions & 7 deletions conformance/score.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,15 @@ def refresh_upstream() -> int:


def ensure_fixtures(conf_dir: Path, force: bool) -> None:
"""Download python/typing's conformance `.py` fixtures into `conf_dir`.
"""Download python/typing's conformance fixtures into `conf_dir`.

Fetches BOTH the `.py` test fixtures AND the `.pyi` support stubs they import
(e.g. `qualifiers_final_decorator.py` does `from _qualifiers_final_decorator
import Base3` — a cross-module `@final` test that is meaningless unless that
sibling stub is on disk). Upstream ships both side by side; fetching only
`.py` silently drops the stubs and makes any import-resolving check score
those files wrong. Only `*.py` are ever SCORED (see `score()`); the `.pyi`
are import-only inputs.

The fixtures are git-ignored and fetched on demand (auto when missing, or via
`--fetch` / `--fetch-only`). No-op when already present at the pinned ref (a
Expand All @@ -184,7 +192,12 @@ def ensure_fixtures(conf_dir: Path, force: bool) -> None:

stamp = conf_dir / ".ref-sha"
cached_ref = stamp.read_text(encoding="utf-8").strip() if stamp.exists() else ""
present = conf_dir.exists() and any(conf_dir.glob("*.py"))
# Require BOTH the `.py` fixtures and the `.pyi` support stubs: a restored
# cache (or older checkout) that predates stub-fetching has `.py` but no
# `.pyi`, and must re-fetch rather than score the cross-module tests wrong.
present = (
conf_dir.exists() and any(conf_dir.glob("*.py")) and any(conf_dir.glob("*.pyi"))
)
if present and cached_ref == PINNED_TYPING_REF and not force:
return

Expand All @@ -197,13 +210,15 @@ def ensure_fixtures(conf_dir: Path, force: bool) -> None:
with urllib.request.urlopen(listing_req, timeout=60) as resp: # noqa: S310 (pinned https)
entries = json.loads(resp.read())
fixtures = [
e for e in entries if e.get("type") == "file" and e["name"].endswith(".py")
e
for e in entries
if e.get("type") == "file" and e["name"].endswith((".py", ".pyi"))
]
if not fixtures:
raise RuntimeError(f"no .py fixtures found at {FIXTURES_API}")
raise RuntimeError(f"no .py/.pyi fixtures found at {FIXTURES_API}")

conf_dir.mkdir(parents=True, exist_ok=True)
for stale in conf_dir.glob("*.py"):
for stale in (*conf_dir.glob("*.py"), *conf_dir.glob("*.pyi")):
stale.unlink()
for entry in fixtures:
with urllib.request.urlopen(entry["download_url"], timeout=60) as resp: # noqa: S310
Expand Down Expand Up @@ -240,8 +255,8 @@ def write_conformance_config(conf_dir: Path) -> None:

`basilisk check <file>` auto-discovers config from the file's directory, so a
`basilisk.json` here is picked up for every fixture. It survives re-fetches
(`ensure_fixtures` only unlinks `*.py`). Committed in spirit — its content is
this constant, version-controlled in score.py for full transparency.
(`ensure_fixtures` only unlinks `*.py`/`*.pyi`). Committed in spirit — its
content is this constant, version-controlled in score.py for full transparency.
"""
conf_dir.mkdir(parents=True, exist_ok=True)
(conf_dir / "basilisk.json").write_text(
Expand Down
8 changes: 4 additions & 4 deletions coverage-thresholds.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@
}
},
"conformance": {
"_doc": "Minimum PEP conformance pass percentage (files passing / total files), computed by the REAL python/typing conformance calculator (conformance/score.py imports the sha256-pinned upstream_main.py and runs its own get_expected_errors + diff_expected_errors; NOTHING is excluded from scoring — every diagnostic the binary emits is counted). A file passes only when upstream's errors_diff is empty. STRICTEST grading: every basilisk diagnostic (errors AND warnings) counts, matching how pyright is graded. The binary is run in a documented spec-conformance config (score.py SPEC_CONFORMANCE_RULES -> <tests>/basilisk.json) that disables basilisk's opinionated, non-spec house-style rules (E0001/E0002/E0004 require-annotations, E0025 require-@override, W0050 redundant-annotation, W0014 explicit-Any nudge); the typing spec defines none of these, and every checker on the conformance page is likewise run in its own type-checking mode. This configures what the binary EMITS, not how the calculator SCORES. Ratchet UP only. Current: 132/146 = 90.4% (up from 40.4% measured with house-style rules on), pinned to python/typing@268d0c4e. See [CHKARCH-CONFORMANCE-MODE]. Target is 100%.",
"threshold": 90,
"_fp_ceiling_doc": "Maximum total false-positive diagnostics across the suite (diagnostics Basilisk reports on lines the suite does NOT mark with # E, plus diagnostics outside satisfied # E[tag] groups) under the strictest errors+warnings grading, with the binary in spec-conformance mode (see _doc). Ratchet DOWN only \u2014 the mirror of the pass-percentage gate. Enforced via conformance/score.py --gate (run on the compiled binary by scripts/test-rust.sh inside make test; no Rust test involved). Current: 3 (down from 285). Drive this DOWN.",
"max_false_positives": 3
"_doc": "Minimum PEP conformance pass percentage (files passing / total files), computed by the REAL python/typing conformance calculator (conformance/score.py imports the sha256-pinned upstream_main.py and runs its own get_expected_errors + diff_expected_errors; NOTHING is excluded from scoring — every diagnostic the binary emits is counted). A file passes only when upstream's errors_diff is empty. STRICTEST grading: every basilisk diagnostic (errors AND warnings) counts, matching how pyright is graded. The binary is run in a documented spec-conformance config (score.py SPEC_CONFORMANCE_RULES -> <tests>/basilisk.json) that disables basilisk's opinionated, non-spec house-style rules (E0001/E0002/E0004 require-annotations, E0025 require-@override, W0050 redundant-annotation, W0014 explicit-Any nudge); the typing spec defines none of these, and every checker on the conformance page is likewise run in its own type-checking mode. This configures what the binary EMITS, not how the calculator SCORES. Ratchet UP only. Current: 146/146 = 100.0% (up from 40.4% measured with house-style rules on, then 90.4%), pinned to python/typing@268d0c4e. Reached purely by improving the Rust checker — the scorer, calculator and fixtures were NOT altered to inflate it. See [CHKARCH-CONFORMANCE-MODE].",
"threshold": 100,
"_fp_ceiling_doc": "Maximum total false-positive diagnostics across the suite (diagnostics Basilisk reports on lines the suite does NOT mark with # E, plus diagnostics outside satisfied # E[tag] groups) under the strictest errors+warnings grading, with the binary in spec-conformance mode (see _doc). Ratchet DOWN only \u2014 the mirror of the pass-percentage gate. Enforced via conformance/score.py --gate (run on the compiled binary by scripts/test-rust.sh inside make test; no Rust test involved). Current: 0 (down from 285) — floor reached.",
"max_false_positives": 0
}
}
23 changes: 12 additions & 11 deletions crates/basilisk-checker/src/rules/e0020.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,14 @@ impl Rule for MissingOverloadImpl {
_ctx: &super::CheckContext,
diagnostics: &mut Vec<Diagnostic>,
) {
// Build a set of Protocol/ABC class names so we can exempt their methods.
let exempt_classes: std::collections::HashSet<&str> = module
// Build a set of Protocol class names so we can exempt their methods.
// ABC classes are NOT blanket-exempt: only their `@abstractmethod`
// overload groups skip the implementation requirement — a *non*-abstract
// overloaded method in an ABC still needs a concrete implementation.
let protocol_classes: std::collections::HashSet<&str> = module
.classes
.iter()
.filter(|cls| {
is_protocol_class(cls)
|| cls
.bases
.iter()
.any(|b| b == "ABC" || b == "abc.ABC" || b == "ABCMeta")
})
.filter(|cls| is_protocol_class(cls))
.map(|cls| cls.name.as_str())
.collect();

Expand Down Expand Up @@ -82,12 +79,16 @@ impl Rule for MissingOverloadImpl {
if overloaded.len() < 2 {
continue;
}
let is_exempt_class = class_name.is_some_and(|cls| exempt_classes.contains(cls));
let is_protocol = class_name.is_some_and(|cls| protocol_classes.contains(cls));
let has_abstract = overloaded
.iter()
.any(|f| has_decorator(&f.decorators, "abstractmethod"));
// Stub files (`.pyi`) declare overloads without an implementation.
let is_stub = std::path::Path::new(&module.path)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("pyi"));

if !is_exempt_class && !has_abstract {
if !is_protocol && !has_abstract && !is_stub {
if let Some(first) = overloaded.first() {
diagnostics.push(make_diagnostic(
first,
Expand Down
13 changes: 10 additions & 3 deletions crates/basilisk-checker/src/rules/e0023.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
//! Implements [BSK-E0023] from [CHKARCH-DIAG-TYPESAFETY]. See docs/specs/CHECKER-ARCHITECTURE-SPEC.md#chkarch-diag-typesafety
//! BSK-E0023: Non-exhaustive `match` statement.
//!
//! A `match` statement that has no wildcard `case _:` branch may fail to
//! handle certain runtime values, leading to a silent fall-through (Python
//! A value-dispatch `match` statement that has no irrefutable branch may fail
//! to handle certain runtime values, leading to a silent fall-through (Python
//! does not raise an error for unmatched `match` subjects). Basilisk treats
//! this as an error in strict mode.
//!
//! Two cases are *not* flagged, matching the reference checkers:
//! * a bare capture `case name:` (no guard) is irrefutable — like `case _:`,
//! it makes the match exhaustive;
//! * a structural match (sequence/mapping patterns) decomposes open-ended
//! shapes — e.g. narrowing a tuple union of mixed arity — where a catch-all
//! is not required for correctness.

use basilisk_resolver::{MatchStmtInfo, ResolvedModule};

Expand All @@ -30,7 +37,7 @@ impl Rule for NonExhaustiveMatch {
module
.match_stmts
.iter()
.filter(|stmt| !stmt.has_wildcard)
.filter(|stmt| !stmt.has_wildcard && !stmt.has_structural_pattern)
.for_each(|stmt| diagnostics.push(make_diagnostic(stmt, &module.path)));
}
}
Expand Down
7 changes: 7 additions & 0 deletions crates/basilisk-checker/src/rules/e0034.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ impl Rule for FinalViolation {
}
}
}
// Bases imported from a sibling module (e.g. a `.pyi` stub) carry
// their `@final` methods in `imported_final_methods`.
if let Some(methods) = module.imported_final_methods.get(base_name.as_str()) {
for method_name in methods {
let _ = final_base_methods.insert(method_name.as_str());
}
}
}

// Emit an error for each child method that overrides a @final base method.
Expand Down
20 changes: 2 additions & 18 deletions crates/basilisk-checker/src/rules/e0041.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@
use std::collections::HashMap;

use basilisk_resolver::{
AttributeInfo, ClassInfo, FunctionInfo, NamedTupleDefInfo, ResolvedModule, RhsKind, Span,
AttributeInfo, ClassInfo, FunctionInfo, NamedTupleDefInfo, ResolvedModule, RhsKind,
};

use crate::diagnostic::{error_diagnostic_owned, Diagnostic, ErrorCode};
use crate::span_util::slice_span;

use super::shared::annotation_is_classvar;
use super::Rule;

const CODE: ErrorCode = ErrorCode {
Expand Down Expand Up @@ -179,23 +180,6 @@ fn metaclass_passes_through(
call_fn.vararg.is_some() && call_fn.kwarg.is_some()
}

/// Returns `true` when the annotation text denotes a `ClassVar[...]` type.
///
/// `ClassVar` fields are excluded from the dataclass `__init__` parameter list.
fn annotation_is_classvar(source: &str, span: Option<Span>) -> bool {
let Some(span) = span else {
return false;
};
let Some(text) = slice_span(source, span) else {
return false;
};
let t = text.trim();
t.starts_with("ClassVar[")
|| t.starts_with("ClassVar ")
|| t == "ClassVar"
|| t.contains(".ClassVar[")
}

/// Collects the positional (non-kw_only, non-init_false, non-ClassVar) fields of a
/// dataclass in declaration order. These are the fields that correspond positionally
/// to constructor arguments when no keyword arguments are used.
Expand Down
Loading
Loading