diff --git a/conformance/conformance_status.csv b/conformance/conformance_status.csv index 4eefbed6..de8c054c 100644 --- a/conformance/conformance_status.csv +++ b/conformance/conformance_status.csv @@ -11,7 +11,7 @@ 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-E0005|BSK-E0151,aliases_typealiastype.py,aliases,FAIL,22,0,1 +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 BSK-E0047,annotations_forward_refs.py,annotations,PASS,19,0,0 @@ -22,11 +22,11 @@ BSK-E0014|BSK-E0015|BSK-E0122|BSK-E0140,callables_annotation.py,callables,PASS,1 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-E0011|BSK-E0014|BSK-E0036|BSK-E0044|BSK-E0121,classes_classvar.py,classes,PASS,17,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-E0111|BSK-E0128,constructors_call_init.py,constructors,PASS,5,0,0 -BSK-E0011|BSK-E0041,constructors_call_metaclass.py,constructors,FAIL,2,0,1 -BSK-E0011|BSK-E0074,constructors_call_new.py,constructors,FAIL,2,0,2 +BSK-E0041,constructors_call_metaclass.py,constructors,PASS,2,0,0 +BSK-E0074,constructors_call_new.py,constructors,PASS,2,0,0 BSK-E0144,constructors_call_type.py,constructors,PASS,8,0,0 BSK-E0153,constructors_callable.py,constructors,PASS,12,0,0 ,constructors_consistency.py,constructors,PASS,0,0,0 @@ -36,10 +36,10 @@ BSK-E0052,dataclasses_frozen.py,dataclasses,PASS,2,0,0 BSK-E0063,dataclasses_hash.py,dataclasses,PASS,4,0,0 BSK-E0017,dataclasses_inheritance.py,dataclasses,PASS,2,0,0 BSK-E0069,dataclasses_kwonly.py,dataclasses,PASS,3,0,0 -BSK-E0005|BSK-E0059,dataclasses_match_args.py,dataclasses,FAIL,1,0,1 +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-E0005|BSK-E0108,dataclasses_slots.py,dataclasses,FAIL,4,1,3 +BSK-E0108,dataclasses_slots.py,dataclasses,FAIL,4,1,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 @@ -49,7 +49,7 @@ BSK-E0005|BSK-E0041|BSK-E0069|BSK-E0096,dataclasses_usage.py,dataclasses,FAIL,8, 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 -BSK-E0011|BSK-E0012|BSK-E0013|BSK-E0041,directives_no_type_check.py,directives,FAIL,1,0,1 +BSK-E0012|BSK-E0013|BSK-E0041,directives_no_type_check.py,directives,PASS,1,0,0 BSK-E0033,directives_reveal_type.py,directives,PASS,2,0,0 ,directives_type_checking.py,directives,PASS,0,0,0 ,directives_type_ignore.py,directives,PASS,0,0,0 @@ -61,8 +61,8 @@ BSK-E0040,enums_behaviors.py,enums,FAIL,1,2,0 BSK-E0061,enums_expansion.py,enums,PASS,1,0,0 ,enums_member_names.py,enums,PASS,0,0,0 BSK-E0066,enums_member_values.py,enums,PASS,2,0,0 -BSK-E0046|BSK-E0067|BSK-W0040,enums_members.py,enums,FAIL,7,0,1 -BSK-E0011,exceptions_context_managers.py,exceptions,FAIL,0,0,2 +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 @@ -77,11 +77,11 @@ BSK-E0117|BSK-E0130,generics_scoping.py,generics,FAIL,10,4,0 BSK-E0075,generics_self_attributes.py,generics,PASS,2,0,0 BSK-E0078,generics_self_basic.py,generics,PASS,3,0,0 BSK-E0077,generics_self_protocols.py,generics,PASS,2,0,0 -BSK-E0025|BSK-E0078|BSK-E0094,generics_self_usage.py,generics,FAIL,11,0,1 +BSK-E0078|BSK-E0094,generics_self_usage.py,generics,PASS,11,0,0 BSK-E0042,generics_syntax_compatibility.py,generics,PASS,2,0,0 BSK-E0043|BSK-E0089|BSK-E0105,generics_syntax_declarations.py,generics,PASS,10,0,0 BSK-E0055|BSK-E0130,generics_syntax_infer_variance.py,generics,PASS,18,0,0 -BSK-E0005|BSK-E0149,generics_syntax_scoping.py,generics,FAIL,7,0,1 +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 @@ -102,14 +102,14 @@ BSK-E0111|BSK-E0116|BSK-E0143,namedtuples_define_class.py,namedtuples,PASS,14,0, BSK-E0041|BSK-E0064,namedtuples_define_functional.py,namedtuples,PASS,9,0,0 BSK-E0073,namedtuples_type_compat.py,namedtuples,PASS,2,0,0 BSK-E0143,namedtuples_usage.py,namedtuples,PASS,8,0,0 -BSK-E0011|BSK-E0101|BSK-E0112,narrowing_typeguard.py,narrowing,PASS,4,0,0 -BSK-E0011|BSK-E0101|BSK-E0112|BSK-E0113,narrowing_typeis.py,narrowing,PASS,9,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-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-E0011|BSK-E0036|BSK-E0097|BSK-E0121,protocols_definition.py,protocols,FAIL,21,0,6 +BSK-E0036|BSK-E0097|BSK-E0121,protocols_definition.py,protocols,PASS,21,0,0 BSK-E0099|BSK-E0118|BSK-E0123|BSK-E0124,protocols_explicit.py,protocols,PASS,6,0,0 BSK-E0130|BSK-E0137,protocols_generic.py,protocols,PASS,9,0,0 BSK-E0098|BSK-E0099|BSK-E0121,protocols_merging.py,protocols,PASS,6,0,0 @@ -121,14 +121,14 @@ 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-E0025|BSK-E0034,qualifiers_final_decorator.py,qualifiers,FAIL,3,3,1 +BSK-E0010|BSK-E0034,qualifiers_final_decorator.py,qualifiers,FAIL,3,3,1 ,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-E0011|BSK-E0014|BSK-E0049|BSK-E0090,tuples_type_form.py,tuples,FAIL,11,0,1 +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 diff --git a/conformance/score.py b/conformance/score.py index a1834572..2d56af79 100644 --- a/conformance/score.py +++ b/conformance/score.py @@ -229,6 +229,7 @@ def ensure_fixtures(conf_dir: Path, force: bool) -> None: "BSK-E0001": "disabled", # missing parameter type annotation "BSK-E0002": "disabled", # missing return type annotation "BSK-E0004": "disabled", # missing *args/**kwargs annotation + "BSK-E0025": "disabled", # missing @override decorator (PEP 698 is opt-in) "BSK-W0014": "disabled", # explicit-`Any` nudge (style, not a spec error) "BSK-W0050": "disabled", # redundant type annotation (house style) } diff --git a/coverage-thresholds.json b/coverage-thresholds.json index f4ed6473..c25267a7 100644 --- a/coverage-thresholds.json +++ b/coverage-thresholds.json @@ -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 -> /basilisk.json) that disables basilisk's opinionated, non-spec house-style rules (E0001/E0002/E0004 require-annotations, 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: 121/146 = 82.9% (up from 40.4% measured with house-style rules on), pinned to python/typing@268d0c4e. See [CHKARCH-CONFORMANCE-MODE]. Target is 100%.", - "threshold": 82, - "_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: 24 (down from 285). Drive this DOWN.", - "max_false_positives": 24 + "_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 -> /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 } } diff --git a/crates/basilisk-checker/src/rules/e0005.rs b/crates/basilisk-checker/src/rules/e0005.rs index b2e39baa..6377b868 100644 --- a/crates/basilisk-checker/src/rules/e0005.rs +++ b/crates/basilisk-checker/src/rules/e0005.rs @@ -41,6 +41,16 @@ impl Rule for MissingAttributeAnnotation { .map(|tv| tv.name.as_str()) .collect(); + // Likewise exempt `X = TypeAliasType("X", ...)` alias definitions: these + // are type-system declarations (static type `typing.TypeAliasType`), not + // data attributes, so requiring an annotation is wrong. The resolver + // already collects these call-sites recursively (including class bodies). + let alias_names: std::collections::HashSet<&str> = module + .type_alias_type_calls + .iter() + .map(|c| c.lhs_name.as_str()) + .collect(); + module .classes .iter() @@ -52,6 +62,7 @@ impl Rule for MissingAttributeAnnotation { class, &module.path, &typevar_names, + &alias_names, &module.classes, diagnostics, ); @@ -63,6 +74,7 @@ fn check_class( class: &ClassInfo, path: &str, typevar_names: &std::collections::HashSet<&str>, + alias_names: &std::collections::HashSet<&str>, all_classes: &[ClassInfo], out: &mut Vec, ) { @@ -72,24 +84,34 @@ fn check_class( .filter(|attr| { !attr.has_annotation && !typevar_names.contains(attr.name.as_str()) - && !is_scalar_literal(&attr.rhs_kind) + && !alias_names.contains(attr.name.as_str()) + && !is_inferrable_literal(&attr.rhs_kind) + && !class + .pep695_type_param_names + .iter() + .any(|p| p == &attr.name) && !parent_has_annotated_attr(&attr.name, class, all_classes) }) .for_each(|attr| out.push(make_diagnostic(attr, &class.name, path))); } -/// Returns `true` when the RHS is a scalar literal whose type is trivially -/// inferrable (int, float, str, bool, bytes, None). -fn is_scalar_literal(rhs: &RhsKind) -> bool { - matches!( - rhs, +/// Returns `true` when the RHS is a literal whose type is fully inferrable +/// without an annotation: a scalar (int, float, str, bool, bytes, None) or a +/// tuple literal whose elements are all themselves inferrable (so `()` and +/// `("a", "b")` — e.g. dataclass `__match_args__` — are exempt). Empty +/// list/dict/set are deliberately excluded: their element types are unknown +/// without an annotation, so they still require one. +fn is_inferrable_literal(rhs: &RhsKind) -> bool { + match rhs { RhsKind::IntLiteral - | RhsKind::FloatLiteral - | RhsKind::StrLiteral - | RhsKind::BoolLiteral - | RhsKind::BytesLiteral - | RhsKind::NoneValue - ) + | RhsKind::FloatLiteral + | RhsKind::StrLiteral + | RhsKind::BoolLiteral + | RhsKind::BytesLiteral + | RhsKind::NoneValue => true, + RhsKind::Tuple(elems) => elems.iter().all(is_inferrable_literal), + _ => false, + } } /// Returns `true` when any ancestor class declares an attribute with the same diff --git a/crates/basilisk-checker/src/rules/e0011.rs b/crates/basilisk-checker/src/rules/e0011.rs index 4023c770..b3b41716 100644 --- a/crates/basilisk-checker/src/rules/e0011.rs +++ b/crates/basilisk-checker/src/rules/e0011.rs @@ -26,7 +26,10 @@ use basilisk_resolver::{FunctionInfo, ResolvedModule}; use crate::diagnostic::{error_diagnostic_owned, Diagnostic, ErrorCode}; -use super::{guards::is_stub_context, Rule}; +use super::{ + guards::{is_no_type_check, is_stub_context}, + Rule, +}; const CODE: ErrorCode = ErrorCode { code: "BSK-E0011", @@ -44,7 +47,8 @@ impl Rule for ReturnTypeMismatch { diagnostics: &mut Vec, ) { for func in &module.functions { - if !is_stub_context(func, &module.classes) { + // @no_type_check suppresses body checks (E0011); E0041 arity still applies. + if !is_stub_context(func, &module.classes) && !is_no_type_check(func) { check_return_type_mismatch(func, module, diagnostics); } } @@ -100,6 +104,17 @@ fn check_return_type_mismatch( // Parse annotation text to InferredType let declared_type = InferredType::from_annotation(ann_text); + // Skip targets the kind-only return inference cannot reliably verify — + // quoted forward references (`"int | Meta2"` → a union of `Named` + // fragments), structural `Named` types (`Sequence[int]`), and + // `Literal[...]` targets (`return True` infers `Bool`, not + // `Literal[True]`). Shared with E0013 so the two sibling return-mismatch + // rules stay in lock-step. Concrete primitive/None/container mismatches + // (e.g. `-> str: return 42`) are NOT unverifiable and still fire. + if super::shared::is_unverifiable_return_type(&declared_type) { + continue; + } + // Check assignability using inference system if !inferred_type.is_assignable_to(&declared_type) { out.push(error_diagnostic_owned( diff --git a/crates/basilisk-checker/src/rules/e0013.rs b/crates/basilisk-checker/src/rules/e0013.rs index 01a53396..a60e127c 100644 --- a/crates/basilisk-checker/src/rules/e0013.rs +++ b/crates/basilisk-checker/src/rules/e0013.rs @@ -37,44 +37,6 @@ impl Rule for ReturnTypeMismatch { } } -/// Returns true when E0013 cannot reliably verify a return against `ty` — at the -/// top level or nested inside a union, container, optional, callable, or type-form. -/// -/// Two kinds defeat E0013's value-less return inference: -/// - `Named`: protocols/classes/aliases (and quote-mangled forward references) -/// need class-hierarchy/structural analysis E0013 cannot perform. -/// - `Literal`: verifying a `Literal[v]` target requires the *value* of the -/// returned expression, but `infer_rhs` only knows the kind (`return True` -/// infers `Bool`, not `Literal[True]`). Any `Literal`-target check is therefore -/// unreliable, so it is skipped. -/// -/// See `check_function` for the rationale. -fn is_unverifiable_return_type(ty: &InferredType) -> bool { - match ty { - InferredType::Named(_) | InferredType::Literal(_) => true, - InferredType::Optional(inner) - | InferredType::List(inner) - | InferredType::Set(inner) - | InferredType::TypeForm(inner) => is_unverifiable_return_type(inner), - InferredType::Dict(key, value) => { - is_unverifiable_return_type(key) || is_unverifiable_return_type(value) - } - InferredType::Union(types) => types.iter().any(is_unverifiable_return_type), - // The variable-length form `tuple[X, ...]` parses the `...` terminator to - // `Named("...")`; that is a structural marker handled by `is_assignable_to`, - // not an unresolvable type, so it must not trigger the skip. - InferredType::Tuple(types) => types.iter().any(|elem| { - !matches!(elem, InferredType::Named(name) if name == "...") - && is_unverifiable_return_type(elem) - }), - InferredType::Callable(info) => { - is_unverifiable_return_type(&info.return_type) - || info.param_types.iter().any(is_unverifiable_return_type) - } - _ => false, - } -} - fn check_function(func: &FunctionInfo, module: &ResolvedModule, out: &mut Vec) { // Generator functions have their own return type validation (E0120). // Return values in generators go through Generator[Y, S, R]'s ReturnType, @@ -106,7 +68,7 @@ fn check_function(func: &FunctionInfo, module: &ResolvedModule, out: &mut Vec boo }) } +/// Returns `true` when a function is decorated with `@no_type_check`. +/// +/// PEP 484 / `typing.no_type_check` directs checkers to suppress *body* type +/// checks for the function, so return-value/assignment diagnostics (E0011) must +/// not fire. Argument-count (E0041) and similar signature checks still apply. +pub(crate) fn is_no_type_check(func: &FunctionInfo) -> bool { + func.decorators.iter().any(|d| d == "no_type_check") +} + /// Returns `true` when a class is an Enum subclass. /// /// Enum members are unannotated by design — their type is `Literal[EnumClass.member]`, diff --git a/crates/basilisk-checker/src/rules/shared.rs b/crates/basilisk-checker/src/rules/shared.rs index 95416214..c2b7472a 100644 --- a/crates/basilisk-checker/src/rules/shared.rs +++ b/crates/basilisk-checker/src/rules/shared.rs @@ -6,6 +6,7 @@ use std::collections::{HashMap, HashSet}; +use crate::types::InferredType; use basilisk_parser::ParsedModule; use basilisk_resolver::{ClassInfo, FunctionInfo, ResolvedModule, Span, TypeVarCallInfo}; use ruff_python_ast::{self as ast, Expr}; @@ -440,3 +441,48 @@ impl StarParam { annotation.map_or(StarParam::Untyped, StarParam::Typed) } } + +// --------------------------------------------------------------------------- +// Return-type verifiability (shared by E0011 and E0013) +// --------------------------------------------------------------------------- + +/// Returns true when a return annotation cannot be reliably verified against a +/// *value-less* inferred return type — at the top level or nested inside a +/// union, container, optional, callable, or type-form. +/// +/// Two kinds defeat kind-only return inference (`infer_rhs` knows the *kind* of +/// a returned expression, never its value): +/// - `Named`: protocols/classes/aliases (and quote-mangled forward references +/// like `"int | Meta2"`) need class-hierarchy/structural analysis the return +/// rules cannot perform. +/// - `Literal`: verifying a `Literal[v]` target requires the *value* of the +/// returned expression, but `return True` infers `Bool`, not `Literal[True]`. +/// Any `Literal`-target check is therefore unreliable, so it is skipped. +/// +/// Both E0011 and E0013 gate their assignability check on this to avoid false +/// positives (consolidated here so the two sibling rules stay in lock-step). +pub(crate) fn is_unverifiable_return_type(ty: &InferredType) -> bool { + match ty { + InferredType::Named(_) | InferredType::Literal(_) => true, + InferredType::Optional(inner) + | InferredType::List(inner) + | InferredType::Set(inner) + | InferredType::TypeForm(inner) => is_unverifiable_return_type(inner), + InferredType::Dict(key, value) => { + is_unverifiable_return_type(key) || is_unverifiable_return_type(value) + } + InferredType::Union(types) => types.iter().any(is_unverifiable_return_type), + // The variable-length form `tuple[X, ...]` parses the `...` terminator to + // `Named("...")`; that is a structural marker handled by `is_assignable_to`, + // not an unresolvable type, so it must not trigger the skip. + InferredType::Tuple(types) => types.iter().any(|elem| { + !matches!(elem, InferredType::Named(name) if name == "...") + && is_unverifiable_return_type(elem) + }), + InferredType::Callable(info) => { + is_unverifiable_return_type(&info.return_type) + || info.param_types.iter().any(is_unverifiable_return_type) + } + _ => false, + } +} diff --git a/crates/basilisk-checker/src/rules/w0040.rs b/crates/basilisk-checker/src/rules/w0040.rs index 951f1d79..dca6c80d 100644 --- a/crates/basilisk-checker/src/rules/w0040.rs +++ b/crates/basilisk-checker/src/rules/w0040.rs @@ -53,6 +53,13 @@ impl Rule for LambdaMissingAnnotations { // Class attributes: `converter = lambda x: str(x)` without annotation for class in &module.classes { + // Enum bodies legitimately assign bare lambdas as non-member + // callables (e.g. a `converter`); the typing spec discourages + // annotating them, so a missing-annotation nudge here is a false + // positive (conformance enums_members.py). + if class.is_enum { + continue; + } for attr in &class.attributes { if attr.rhs_is_lambda && !attr.has_annotation { diagnostics.push(warning_diagnostic_owned( diff --git a/crates/basilisk-checker/tests/checker/e0005_tests.rs b/crates/basilisk-checker/tests/checker/e0005_tests.rs index 80357aa7..c0eaeb4d 100644 --- a/crates/basilisk-checker/tests/checker/e0005_tests.rs +++ b/crates/basilisk-checker/tests/checker/e0005_tests.rs @@ -339,3 +339,60 @@ class Mixed: ); Ok(()) } + +#[test] +fn e0005_type_alias_type_in_class_exempt() -> Result<(), Box> { + // A class-body `X = TypeAliasType("X", ...)` is a type-alias definition, not + // a data attribute, so it must not require an annotation + // (conformance aliases_typealiastype.py). + let source = "from typing import TypeAliasType\nclass A:\n GoodAlias = TypeAliasType(\"GoodAlias\", list[int])\n"; + let diags = run(source)?; + assert!( + !codes(&diags).contains(&"BSK-E0005"), + "TypeAliasType alias in a class body must not fire E0005, got: {:?}", + codes(&diags) + ); + Ok(()) +} + +#[test] +fn e0005_tuple_literal_match_args_exempt() -> Result<(), Box> { + // A tuple literal of inferrable elements is fully inferrable; `__match_args__` + // must not require an annotation (conformance dataclasses_match_args.py). + let source = "class DC:\n __match_args__ = (\"a\", \"b\")\n empty = ()\n"; + let diags = run(source)?; + assert!( + !codes(&diags).contains(&"BSK-E0005"), + "tuple-literal class attrs must not fire E0005, got: {:?}", + codes(&diags) + ); + Ok(()) +} + +#[test] +fn e0005_pep695_type_param_attr_exempt() -> Result<(), Box> { + // An attribute whose name matches one of the class's PEP 695 type parameters + // is the type variable in scope, not a data attribute + // (conformance generics_syntax_scoping.py). + let source = "class C[T]:\n T = 0\n"; + let diags = run(source)?; + assert!( + !codes(&diags).contains(&"BSK-E0005"), + "PEP 695 type-param-named attr must not fire E0005, got: {:?}", + codes(&diags) + ); + Ok(()) +} + +#[test] +fn e0005_empty_collection_still_fires() -> Result<(), Box> { + // The tuple exemption must NOT leak to empty list/dict (element types unknown). + let source = "class Foo:\n data = []\n"; + let diags = run(source)?; + assert!( + codes(&diags).contains(&"BSK-E0005"), + "empty-list class attr must still fire E0005 (element type unknown), got: {:?}", + codes(&diags) + ); + Ok(()) +} diff --git a/crates/basilisk-checker/tests/checker/e0011_tests.rs b/crates/basilisk-checker/tests/checker/e0011_tests.rs index d7b0b2f5..71dbf35d 100644 --- a/crates/basilisk-checker/tests/checker/e0011_tests.rs +++ b/crates/basilisk-checker/tests/checker/e0011_tests.rs @@ -49,3 +49,46 @@ fn e0011_return_mismatch_stub_exempt() -> Result<(), Box> ); Ok(()) } + +#[test] +fn e0011_literal_target_not_flagged() -> Result<(), Box> { + // `return True` infers `Bool`, not `Literal[True]`; the kind-only return + // inference cannot verify a `Literal[...]` target, so E0011 must NOT fire + // (matches __exit__ -> Literal[True] in conformance exceptions_context_managers.py). + let source = "from typing import Literal\ndef ok() -> Literal[True]:\n return True\n"; + let diags = run(source)?; + assert!( + !codes(&diags).contains(&"BSK-E0011"), + "Literal[True] target must not fire E0011 (value-less inference is unverifiable), got: {:?}", + codes(&diags) + ); + Ok(()) +} + +#[test] +fn e0011_quoted_forward_ref_union_not_flagged() -> Result<(), Box> { + // A quoted forward-ref union annotation parses into `Named` fragments with no + // concrete member; E0011 must skip it rather than flag a valid `return 1` + // (conformance constructors_call_metaclass.py). + let source = "class Meta2: ...\ndef f() -> \"int | Meta2\":\n return 1\n"; + let diags = run(source)?; + assert!( + !codes(&diags).contains(&"BSK-E0011"), + "quoted forward-ref union target must not fire E0011, got: {:?}", + codes(&diags) + ); + Ok(()) +} + +#[test] +fn e0011_concrete_mismatch_still_fires_after_guard() -> Result<(), Box> { + // The unverifiability guard must NOT suppress genuine, concrete mismatches. + let source = "def f() -> None:\n return 42\n"; + let diags = run(source)?; + assert!( + codes(&diags).contains(&"BSK-E0011"), + "returning a value from -> None must still fire E0011, got: {:?}", + codes(&diags) + ); + Ok(()) +} diff --git a/crates/basilisk-checker/tests/checker/w0040_tests.rs b/crates/basilisk-checker/tests/checker/w0040_tests.rs index 162e59ad..b5ad094d 100644 --- a/crates/basilisk-checker/tests/checker/w0040_tests.rs +++ b/crates/basilisk-checker/tests/checker/w0040_tests.rs @@ -42,3 +42,31 @@ fn w0040_lambda_is_warning_not_error() -> Result<(), Box> ); Ok(()) } + +#[test] +fn w0040_enum_body_lambda_exempt() -> Result<(), Box> { + // Enum bodies legitimately assign bare lambdas as non-member callables; + // annotating them is discouraged, so W0040 must not fire + // (conformance enums_members.py). + let source = "from enum import Enum\nclass Color(Enum):\n RED = 1\n converter = lambda x: str(x)\n"; + let diags = run(source)?; + assert!( + !codes(&diags).contains(&"BSK-W0040"), + "lambda in an enum body must not fire W0040, got: {:?}", + codes(&diags) + ); + Ok(()) +} + +#[test] +fn w0040_non_enum_class_lambda_still_fires() -> Result<(), Box> { + // The enum exemption must NOT leak to ordinary classes. + let source = "class Plain:\n converter = lambda x: str(x)\n"; + let diags = run(source)?; + assert!( + codes(&diags).contains(&"BSK-W0040"), + "lambda in a non-enum class must still fire W0040, got: {:?}", + codes(&diags) + ); + Ok(()) +} diff --git a/docs/specs/CHECKER-ARCHITECTURE-SPEC.md b/docs/specs/CHECKER-ARCHITECTURE-SPEC.md index 39d5dd03..4ac64ba9 100644 --- a/docs/specs/CHECKER-ARCHITECTURE-SPEC.md +++ b/docs/specs/CHECKER-ARCHITECTURE-SPEC.md @@ -58,7 +58,7 @@ See the project README for competitive analysis. | Implementation | TypeScript | Python/C | Rust | Rust | Rust | Rust | **Rust** | | License | MIT | MIT | MIT | MIT | AGPL | MIT | **MIT** | | Default strictness | Gradual | Gradual | Gradual | Gradual | Gradual | N/A | **Strict only** | -| PEP conformance (current) | ~95% | ~85% | ~15% | ~58% | ~69% | N/A | **82.9%** | +| PEP conformance (current) | ~95% | ~85% | ~15% | ~58% | ~69% | N/A | **90.4%** | | PEP conformance target | — | — | — | — | — | N/A | **100%** | | LSP server | Yes | No | Yes | Yes | Yes | No | **Yes** | | Incremental computation | Lazy eval | Daemon | Salsa | Module-level | No | N/A | **Salsa** | @@ -286,7 +286,7 @@ The `# type:` prefix ensures compatibility with editors and tools that already r ### Python Typing PEP Coverage {#CHKARCH-PEPS} -Basilisk targets **100% conformance** with the Python typing specification. This is a target, not a present-day achievement: the official `python/typing` conformance scorer (pinned commit, run unmodified in CI) currently reports **121 of 146 files passing (82.9%, counting errors and warnings — the strictest grading)**, with 24 false positives and 36 missed required errors still to clear, running the binary in spec-conformance mode ([CHKARCH-CONFORMANCE-MODE](#CHKARCH-CONFORMANCE-MODE)). We run that suite in CI on every change and ratchet the pass rate up. +Basilisk targets **100% conformance** with the Python typing specification. This is a target, not a present-day achievement: the official `python/typing` conformance scorer (pinned commit, run unmodified in CI) currently reports **132 of 146 files passing (90.4%, counting errors and warnings — the strictest grading)**, with 3 false positives and 36 missed required errors still to clear, running the binary in spec-conformance mode ([CHKARCH-CONFORMANCE-MODE](#CHKARCH-CONFORMANCE-MODE)). We run that suite in CI on every change and ratchet the pass rate up. #### Foundation PEPs {#CHKARCH-PEPS-FOUNDATION} @@ -1405,13 +1405,13 @@ checkers (pyright, mypy, pyrefly, ty, zuban, pycroscope) are graded with. in `coverage-thresholds.json` (`conformance.threshold`, `conformance.max_false_positives`); the former ratchets **up**, the latter **down**. Per-file results are written to `conformance/conformance_status.csv`. -- **Current score**: **121 / 146 = 82.9%** (strictest grading: every diagnostic, - errors AND warnings, counted — as pyright is graded), 24 false positives, 36 +- **Current score**: **132 / 146 = 90.4%** (strictest grading: every diagnostic, + errors AND warnings, counted — as pyright is graded), 3 false positives, 36 missed required errors, binary in spec-conformance mode. This replaces an earlier in-repo harness that *excluded codes from the scorer itself* (a rig that reported 100%); the honest number with house-style rules **on** was 40.4%, and configuring the binary to its type-system mode (§ below) — not touching the - scorer — lifts it to 82.9%. Target: 100%. + scorer — plus eliminating false positives lifts it to 90.4%. Target: 100%. #### Spec-conformance mode {#CHKARCH-CONFORMANCE-MODE} @@ -1423,6 +1423,7 @@ Basilisk is **strict-by-default**: on top of the type system it ships opinionate | `BSK-E0001` | Missing parameter type annotation | unannotated params have an inferred/`Any` type, not an error | | `BSK-E0002` | Missing return type annotation | an unannotated return type is **inferred**, not an error | | `BSK-E0004` | Missing `*args`/`**kwargs` annotation | same — inference, not a requirement | +| `BSK-E0025` | Missing `@override` decorator | PEP 698 `@override` is **opt-in**; a checker is not required to demand it | | `BSK-W0050` | Redundant type annotation | redundancy is a style smell, never a type error | | `BSK-W0014` | Explicit `Any` nudge | `Any` is a fully valid type per the spec | diff --git a/website/src/docs/conformance.md b/website/src/docs/conformance.md index 0ef63d3a..87450caa 100644 --- a/website/src/docs/conformance.md +++ b/website/src/docs/conformance.md @@ -64,7 +64,7 @@ The adapter and gate live in a separate, auditable file, so the calculator stays ## What the checker runs in — spec-conformance mode -The suite tests the **type system** — generics, protocols, overloads, `TypedDict`, and the rest. Basilisk is strict by default and layers on house-style rules the typing spec doesn't define: chiefly *require an annotation* on every parameter, return, and `*args`/`**kwargs`, a redundant-annotation warning, and an explicit-`Any` nudge. Those are the right defaults for day-to-day Basilisk, but the spec treats an unannotated type as **inferred**, not an error — so firing them on the suite would be a false positive on nearly every file. +The suite tests the **type system** — generics, protocols, overloads, `TypedDict`, and the rest. Basilisk is strict by default and layers on house-style rules the typing spec doesn't define: chiefly *require an annotation* on every parameter, return, and `*args`/`**kwargs`, a redundant-annotation warning, a missing-`@override` nudge, and an explicit-`Any` nudge. Those are the right defaults for day-to-day Basilisk, but the spec treats an unannotated type as **inferred**, not an error — so firing them on the suite would be a false positive on nearly every file. So, exactly as pyright's conformance run leaves `reportMissingParameterType` and its siblings off, we run the binary in a **spec-conformance mode** that disables those house-style rules (a committed `basilisk.json` the scorer drops beside the test files). This sets **what the binary emits** — the same lever every checker on the results page pulls — not how the result is scored. The pinned calculator above still counts every diagnostic the binary does emit, with nothing excluded. diff --git a/website/src/zh/docs/conformance.md b/website/src/zh/docs/conformance.md index 4f82e5a9..241f293e 100644 --- a/website/src/zh/docs/conformance.md +++ b/website/src/zh/docs/conformance.md @@ -59,7 +59,7 @@ Basilisk 由**官方 `python/typing` 符合性套件**评分——也就是类 ## 检查器以何种模式运行——符合性模式 -该套件测试的是**类型系统**——泛型、协议、重载、`TypedDict` 等。Basilisk 默认严格,并在其上叠加了类型规范未定义的内部风格规则:主要是要求每个参数、返回值以及 `*args`/`**kwargs` 都带注解、一条冗余注解警告,以及一个显式 `Any` 提示。这些是 Basilisk 日常使用的合理默认,但规范将未注解的类型视为**推断**而非错误——因此在套件上触发它们会让几乎每个文件都产生误报。 +该套件测试的是**类型系统**——泛型、协议、重载、`TypedDict` 等。Basilisk 默认严格,并在其上叠加了类型规范未定义的内部风格规则:主要是要求每个参数、返回值以及 `*args`/`**kwargs` 都带注解、一条冗余注解警告、一个缺失 `@override` 的提示,以及一个显式 `Any` 提示。这些是 Basilisk 日常使用的合理默认,但规范将未注解的类型视为**推断**而非错误——因此在套件上触发它们会让几乎每个文件都产生误报。 因此,正如 pyright 的符合性运行不启用 `reportMissingParameterType` 及其同类规则一样,我们以一种**符合性模式**运行二进制文件,关闭这些内部风格规则(评分器会在测试文件旁放置一份 committed 的 `basilisk.json`)。这设定的是**二进制文件发出什么**——也是结果页上每个检查器所用的同一杠杆——而非结果如何评分。上面那个固定的计算器仍会计入二进制文件实际发出的每一个诊断,不排除任何内容。