Skip to content

Fix false negative for disallowed-name on non-constant module-level names - #11241

Open
ArockiaRajamanickam wants to merge 4 commits into
pylint-dev:mainfrom
ArockiaRajamanickam:fix-10679-disallowed-name-false-negative
Open

Fix false negative for disallowed-name on non-constant module-level names#11241
ArockiaRajamanickam wants to merge 4 commits into
pylint-dev:mainfrom
ArockiaRajamanickam:fix-10679-disallowed-name-false-negative

Conversation

@ArockiaRajamanickam

Copy link
Copy Markdown

Closes #10679.

visit_assignname wrapped the _check_name() calls for module-scope names in if not self._meets_exception_for_non_consts(...). That guard exists to suppress an invalid-name false positive, but because it short-circuits before _check_name() is entered, it suppresses disallowed-name along with it.

So a module-level name that is explicitly listed in bad-names is silently not reported whenever its value is a non-Const expression:

foo = {}.keys()   # disallowed-name not emitted
bar = 42          # disallowed-name emitted

On main only line 3 is reported. This is the case marked by the TODO left in tests/functional/d/disallowed_name.py by #10677:

foo = {}.keys()  # Should raise disallowed-name once _check_name() is refactored.

The change

_check_name() already takes disallowed_check_only, which does exactly this job and is already used for the redefines_import case. Both guards now pass that flag instead of skipping the call, so invalid-name stays suppressed and disallowed-name is still emitted.

The part worth pushing back on

There is a third change, and it is arguably beyond the minimal fix.

Once _check_name() is entered for these names, they also reach _is_multi_naming_match(), which records them for the multi-naming-style report. That would make a name start influencing the C0103 naming-group heuristic purely because it is on the bad-names list, which is not what the guard was suppressing it for. I gated that call on not disallowed_check_only and added a functional test for it.

If you would rather keep this PR to the two call sites and treat the multi-naming interaction separately, say so and I will drop that hunk and its test.

Tests

tests/functional/d/disallowed_name.py line 9 changes from the TODO comment to an actual # [disallowed-name] expectation, with the .txt regenerated via --update-functional-output. A new disallowed_name_multi_naming_style functional test covers the third change.

Reverting only checker.py and keeping the tests fails them. Full tests/test_functional.py: 875 passed on a clean checkout, 876 with this branch, with an identical set of 12 pre-existing failures in both runs (wrong_import_order, typing_broken_noreturn, typevar_naming_style_default and others, all Python 3.14 / astroid 4.3.0 issues unrelated to this change). Changelog fragment added under doc/whatsnew/fragments/.

I used an AI assistant while working on this. The reproduction, the baseline comparison and the decision to flag the multi-naming hunk separately are mine.

… names

`NameChecker.visit_assignname` short-circuited on
`_meets_exception_for_non_consts()` before reaching `_check_name()`. That
guard exists to avoid an `invalid-name` false positive on module-level
names whose assigned value is not a `nodes.Const`, but skipping the call
entirely also skipped the `disallowed-name` check, so a name explicitly
listed in `bad-names` went unreported.

Pass the guard through as `disallowed_check_only` instead, the parameter
`_check_name()` already provides for this situation, so `disallowed-name`
is still emitted while `invalid-name` stays suppressed.

`disallowed_check_only` now also skips registering the name for multiple
naming style detection. That registration happens before the existing
`disallowed_check_only` guard and reports `invalid-name` from
`leave_module()`, which would otherwise have reintroduced the false
positive for users configuring `name-group` with named-group patterns.

Closes pylint-dev#10679

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Pierre-Sassoulas Pierre-Sassoulas added the False Negative 🦋 No message is emitted but something is wrong with the code label Aug 9, 2026
@Pierre-Sassoulas Pierre-Sassoulas added this to the 4.1.0 milestone Aug 9, 2026
@codecov

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.35%. Comparing base (7f0d9a7) to head (52d9764).
⚠️ Report is 23 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main   #11241   +/-   ##
=======================================
  Coverage   96.35%   96.35%           
=======================================
  Files         178      178           
  Lines       19932    19940    +8     
=======================================
+ Hits        19205    19213    +8     
  Misses        727      727           
Files with missing lines Coverage Δ
pylint/checkers/base/name_checker/checker.py 98.81% <100.00%> (+0.01%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

This comment has been minimized.

Pin the current behavior of the name-group census for module-level
names: a name redefining an import in an except ImportError handler
still takes part in it, while a name bound to a non-constant inside a
module-level loop does not.

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on pylint. This change the behavior of un unrelated check, I added a regression test and pushed it on your branch, because explaining what to do is harder. (pull your branch before working on it please :) )


for _ in range(3):
tata = {}.keys() # [disallowed-name]
some_other_name = {}.keys()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It matches variable-rgx, so it stays silent with or without your change mabe I didn't understand the intent of the test ?

@@ -0,0 +1,5 @@
Fix a false negative for ``disallowed-name`` on module-level names assigned a
value that is not a constant. The control flow that suppresses ``invalid-name``

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module-level names whose value cannot be inferred still emit nothing.

@@ -0,0 +1,5 @@
Fix a false negative for ``disallowed-name`` on module-level names assigned a
value that is not a constant. The control flow that suppresses ``invalid-name``
for these names no longer skips the ``disallowed-name`` check as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd state the user-visible change instead of describing it ("foo = {}.keys() at module level is now reported").

The previous revision gated _is_multi_naming_match on
disallowed_check_only, which conflated two different reasons for
suppressing invalid-name. A name redefining an import should still take
part in the multiple naming style census; only a name that previously
never reached _check_name at all should stay out of it.

Split them: skip_name_group_census is now set solely by
_meets_exception_for_non_consts, leaving redefines_import to suppress
the message while remaining in the census, as before.

Both functional tests added in b20bde9 now pass.
@ArockiaRajamanickam

Copy link
Copy Markdown
Author

Thanks, and the test was much clearer than an explanation would have been. Pulled and fixed.

You were right about what broke. I had gated _is_multi_naming_match on disallowed_check_only, which quietly conflated two different reasons for suppressing invalid-name:

  • redefines_import: the message is suppressed, but the name should still take part in the naming style census.
  • _meets_exception_for_non_consts: the name never reached _check_name at all before this PR, so it should stay out of the census.

My guard treated both the same and dropped the import case out of the census, which is exactly what your disallowed_name_multi_naming_style_import test catches.

Split them with a separate skip_name_group_census parameter, set only by the non-const path. redefines_import keeps its old behaviour.

Both of your tests pass now, and the loop one passes for the right reason rather than by accident. Full tests/test_functional.py on your merge commit: 878 passed, 12 failed. Reverting just my checker.py change gives 877 passed, 13 failed, the extra failure being the disallowed_name case this PR is for. The 12 are pre-existing on main here (wrong_import_order, typing_broken_noreturn and similar, all Python 3.14 / astroid 4.3.0).

Worth flagging since it is a real question rather than a rhetorical one: the census behaviour for the non-const case is now pinned by your loop test, but it is still a behaviour change relative to main in the sense that those names reach _check_name where they did not before. If you would rather this PR only rewire the two call sites and leave the census question to a separate change, I am happy to split it.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

🤖 According to the primer, this change has no effect on the checked open source code. 🤖🎉

This comment was generated for commit 52d9764

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

False Negative 🦋 No message is emitted but something is wrong with the code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

False negatives for disallowed-name due to control flow in NameChecker

3 participants