Skip to content

aliases_implicit: ParamSpec argument check is a shape guess ("probably wrong") that never identifies the ParamSpec position #409

Description

@MelbourneDeveloper

Split out of #408. Confirmed live on main @ da74283 and on the bidirectionaltype-inference branch (that branch's only change to this file is routing is_assignable_to_bound through a SubtypingContext; this code is untouched).

The code

crates/basilisk-checker/src/rules/aliases_implicit.rs:693-698

// If all args are simple types (no `[...]` or `...`),
// the ParamSpec arg is probably wrong
let all_simple = args.iter().all(|arg| {
    let trimmed = arg.trim();
    !trimmed.contains('[') && trimmed != "..."
});
if all_simple && args.len() > 1 {
    // → error: "Invalid type argument for `ParamSpec` parameter in `{base}`"
}

This emits a hard error on a shape guess. Its own comment says "probably".

Why it looks correct

The alias positions come from collect_typevar_bounds, which records TypeVar names in order of first textual appearance in the RHS. For the conformance suite's

GoodTypeAlias9 = Callable[Concatenate[int, P], R]

that yields [P, R], so GoodTypeAlias9[int, int] gives arg_count == typevar_count == 2, both args "simple", len > 1 → fires. That is aliases_implicit.py:80, and it is the only case the branch was ever exercised against.

It never identifies the ParamSpec position

The check never asks which argument corresponds to the ParamSpec. It asks whether all arguments look bracket-free. Consequences:

False negative — invalid ParamSpec argument accepted:

def f(x: GoodTypeAlias9[int, [str]]): ...

Position 0 is P (a ParamSpec) and receives int — invalid, it must be a parameter list or .... Position 1 is R (a TypeVar) and receives [str] — also invalid, that is not a type. Because "[str]" contains [, all_simple is false and the entire branch is skipped. Two errors, zero diagnostics.

False negative — nested annotations are never reached:

annotation_base_name splits on the first [ and check_single_annotation is called only from the two top-level loops (:606, :627) — it never recurses into type arguments. So:

def f(x: list[GoodTypeAlias2[int]]): ...

resolves the base name to list, finds no alias, and returns. The inner misuse — the same error the suite checks at top level on line 76 — is invisible at any nesting depth.

Fix

Determine parameter kinds from the alias's declared type parameters, match each argument to its position, and validate a ParamSpec argument as a parameter list, Concatenate[...], ..., or another ParamSpec. Recurse into type arguments so nested annotations are checked. Until that exists, this diagnostic should not be emitted — a check whose comment says "probably" must not produce an error.

Needs off-suite regression tests: both cases above are invisible to the conformance suite.

Metadata

Metadata

Assignees

No one assigned

    Labels

    spec-violationCode functionality/logic does not match its spec (see SPEC-CONFORMANCE-AUDIT-PLAN.md)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions