Skip to content

Fix Python PEP 695 generic functions being reported with the name ] - #492

Merged
terryyin merged 1 commit into
terryyin:masterfrom
Gronoxx:fix/python-pep695-type-params
Aug 27, 2026
Merged

Fix Python PEP 695 generic functions being reported with the name ]#492
terryyin merged 1 commit into
terryyin:masterfrom
Gronoxx:fix/python-pep695-type-params

Conversation

@Gronoxx

@Gronoxx Gronoxx commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Problem

Functions declared with a PEP 695 type parameter list (Python 3.12+) are reported with the
name ] instead of their real name:

def soma[T](a: T, b: T) -> T:
    return a

python -m lizard repro.py reports this as ]@1-2@repro.py. Through the API,
function_list[0].name is ']' and long_name is ']( a : T , b : T )'.
nloc, cyclomatic_complexity and parameter_count are correct, so only the name breaks.

Measured on home-assistant/core @ 73898c29e2a: 175 .py files use def name[...], and
242 of the 4,235 functions (5.7%) in those files come out as ]. Everything that keys on
the name sees ]: -w output, CSV/XML/HTML reports, whitelists, duplicate detection.

Cause

PythonStates._function assumes exactly one token sits between def and (:

def _function(self, token):
    if token != '(':
        self.context.restart_new_function(token)   # runs for EVERY token before '('
        self.context.add_to_long_function_name("(")
    else:
        self._state = self._dec

For def soma[T](...) the tokens before ( are soma, [, T, ]. Each one restarts the
function, so the last one wins.

The reader already skips [...] groups, but only inside the parameter list
(_state_parameterized_type_annotation, entered from _dec). A PEP 695 list sits before the
(, so it never reaches that state.

Fix

Enter a dedicated state when _function sees [, and consume the type parameter list without
restarting the function. Bracket depth is tracked so nested bounds work.

The state also recovers on def and class. Neither keyword can appear inside a valid type
parameter list, so an unbalanced [ resumes at the next definition instead of swallowing the
rest of the file.

All of these keep the correct name:

def soma[T](a: T, b: T) -> T: ...
def par[K, V](k: K, v: V): ...
def f[T: int](x: T): ...
def f[T: list[int]](x: T): ...              # nested bound
def f[T: (int, str)](x: T): ...             # constraints
def f[T: Callable[[int], str]](x: T): ...   # nested brackets in bound
def f[T = int](x: T): ...                   # PEP 696 default
def f[*Ts](x): ...                          # TypeVarTuple
def f[**P](x): ...                          # ParamSpec
class C:
    def m[T](self, x: T): ...
async def busca[T](x: T): ...

Notes

  • Type parameters stay out of long_name, which reads soma( a : T , b : T ). Including them
    means deferring the ( that is currently appended as soon as the name is read, which changes
    long_name construction for every function, not just generic ones. This PR restores the name
    with the smallest edit. Adding type parameters to long_name is an easy follow-up.
  • PEP 695 syntax appears only inside test strings, so the new tests run on the Python 3.8 and
    3.10 interpreters in CI without a SyntaxError.

Tests

14 tests in test/test_languages/testPython.py::Test_Python_pep695_type_parameters. They cover
generic functions, methods, async def, multiple type parameters, nested bounds, constraints,
PEP 696 defaults, TypeVarTuple, ParamSpec, multi-line lists, string literals with brackets
inside a bound, long_name integrity, and recovery from an unbalanced [.

Full suite on the base commit: 8 failed, 1364 passed, 7 skipped.
With this change: 8 failed, 1378 passed, 7 skipped.

The 8 failures also fail on the base commit, 2 in testFilesFilter.py and 6 in the HTML output
tests. They do not touch Python function parsing.

PythonStates._function assumed exactly one token between `def` and `(`, so
every token before `(` called restart_new_function and the last one won. For
`def soma[T](a, b)` the tokens are `soma`, `[`, `T`, `]`, leaving the function
named `]`.

Add _state_type_parameters, entered from _function on `[`, which consumes the
PEP 695 type parameter list without restarting the function. Bracket depth is
tracked so nested bounds such as `def f[T: list[int]](x)` work. The state
recovers on `def`/`class`, so an unbalanced `[` in truncated or invalid source
does not swallow the rest of the file.

Measured on home-assistant/core@73898c29e2a: 242 of 4235 functions in the 175
files using `def name[...]` were reported as `]`.

Add 14 regression tests covering generic functions, methods, async, multiple
type parameters, nested bounds, constraints, PEP 696 defaults, TypeVarTuple,
ParamSpec, multi-line lists, bracket-bearing string literals in bounds,
long_name integrity and unbalanced-bracket recovery.
@terryyin

Copy link
Copy Markdown
Owner

thanks. Looks great!

@terryyin
terryyin merged commit 0aadd97 into terryyin:master Aug 27, 2026
@Gronoxx
Gronoxx deleted the fix/python-pep695-type-params branch August 27, 2026 15:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants