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
32 changes: 16 additions & 16 deletions conformance/conformance_status.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions conformance/score.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
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, 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 -> <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
}
}
46 changes: 34 additions & 12 deletions crates/basilisk-checker/src/rules/e0005.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -52,6 +62,7 @@ impl Rule for MissingAttributeAnnotation {
class,
&module.path,
&typevar_names,
&alias_names,
&module.classes,
diagnostics,
);
Expand All @@ -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<Diagnostic>,
) {
Expand All @@ -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
Expand Down
19 changes: 17 additions & 2 deletions crates/basilisk-checker/src/rules/e0011.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -44,7 +47,8 @@ impl Rule for ReturnTypeMismatch {
diagnostics: &mut Vec<Diagnostic>,
) {
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);
}
}
Expand Down Expand Up @@ -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(
Expand Down
40 changes: 1 addition & 39 deletions crates/basilisk-checker/src/rules/e0013.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Diagnostic>) {
// Generator functions have their own return type validation (E0120).
// Return values in generators go through Generator[Y, S, R]'s ReturnType,
Expand Down Expand Up @@ -106,7 +68,7 @@ fn check_function(func: &FunctionInfo, module: &ResolvedModule, out: &mut Vec<Di
// be flagged. Empty-tuple forms like `tuple[()]` parse the `()` to
// `Named("()")`. `Literal[...]` targets need the returned value, which the
// kind-only return inference does not have. All are skipped.
if is_unverifiable_return_type(&declared_type) {
if super::shared::is_unverifiable_return_type(&declared_type) {
return;
}

Expand Down
9 changes: 9 additions & 0 deletions crates/basilisk-checker/src/rules/guards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ pub(crate) fn is_stub_context(func: &FunctionInfo, classes: &[ClassInfo]) -> 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]`,
Expand Down
Loading
Loading