Fix NG subsystem mypy plugin#23334
Conversation
…dditional_deps The previous implementation piggybacked on get_additional_deps to modify abstract_status on FuncDef nodes for @option-decorated methods. This hook fires during import resolution, before mypy's semantic analysis passes run. Because mypy's semantic analyzer processes FuncDef nodes after this hook, any modifications made there do not persist into the type-checking phase, so the [empty-body] errors were never suppressed. Switch to get_base_class_hook, which fires during semantic analysis (after the class's own semanal pass), so setting abstract_status = IS_ABSTRACT on the underlying FuncDef sticks when the type checker runs. Since get_base_class_hook matches only direct base classes, enumerate the known levels of the SubsystemNg hierarchy explicitly: SubsystemNg itself, UniversalSubsystem, ContextualSubsystem, and GoalSubsystemNg. Any new intermediate base class added to the hierarchy will need to be listed here.
|
AI Disclosure: Claude discovered the cause, suggested a fix (which I simplified a bit) and generated a test (which I simplified a lot). |
|
|
||
|
|
||
| # NB: If we create more intermediate subclasses, they must be added here. | ||
| _SUBSYSTEM_BASE_CLASSES = frozenset( |
There was a problem hiding this comment.
Can these be discovered by the plugin from semantic analysis in a performant manner?
There was a problem hiding this comment.
I don't think that can be done reliably, let alone performantly?
There was a problem hiding this comment.
I asked Codex and it suggests looking at ctx.cls.info.mro to access the MRO for the class:
def _is_subsystem_ng(ctx: ClassDefContext) -> bool:
return any(info.fullname == _SUBSYSTEM_ROOT for info in ctx.cls.info.mro)
Open question whether that is performant enough.
There was a problem hiding this comment.
Yes, but not with the current get_base_class_hook(fullname) shape alone.
get_base_class_hook()only receives the direct base class fullname being analyzed, so for:class FooSubsystem(GoalSubsystemNg):
...mypy asks the plugin about pants.ng.goal.GoalSubsystemNg, not about its transitive base
SubsystemNg. That is why_SUBSYSTEM_BASE_CLASSEScurrently has to include intermediate
classes likeGoalSubsystemNg,UniversalSubsystem, andContextualSubsystem.A dynamic version is possible by making the hook run more broadly, then filtering inside the
callback using the class MRO:_SUBSYSTEM_ROOT = "pants.ng.subsystem.SubsystemNg" def _is_subsystem_ng(ctx: ClassDefContext) -> bool: return any(info.fullname == _SUBSYSTEM_ROOT for info in ctx.cls.info.mro) def _process_subclass_hook(ctx: ClassDefContext) -> None: if _is_subsystem_ng(ctx): _process_defns(ctx.cls.defs.body) class SubsystemPlugin(Plugin): def get_base_class_hook(self, fullname: str): return _process_subclass_hookThat would infer intermediate subclasses dynamically from mypy’s semantic model, avoiding
the manually maintained_SUBSYSTEM_BASE_CLASSES.Tradeoff: returning a hook for every base class means the callback runs for many non-
subsystem classes, and possibly more than once for classes with multiple bases. The callback
is cheap and idempotent enough here, but I’d add a guard such as metadata on ctx.cls.info if
duplicate processing becomes a concern.
There was a problem hiding this comment.
Maybe add a conformance check in the plugin for all direct subclasses of pants.ng.subsystem.SubsystemNg that they are listed in _SUBSYSTEM_BASE_CLASSES?
Basically, it would be better if the plugin either discovers the contents of _SUBSYSTEM_BASE_CLASSES or otherwise flags an error when items are missing from it.
The previous implementation failed if the subsystem
extended an intermediate SubsystemNg subclass
instead of SubsystemNg directly.
This change switches to get_base_class_hook, so that
setting abstract_status = IS_ABSTRACT on the
underlying FuncDef sticks when the type checker runs.