Do not infer a shadowed builtin as the builtin - #3211
Conversation
Merging this PR will not alter performance
Comparing Footnotes
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3211 +/- ##
=======================================
Coverage 93.65% 93.66%
=======================================
Files 93 93
Lines 11613 11660 +47
=======================================
+ Hits 10876 10921 +45
- Misses 737 739 +2
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Pierre-Sassoulas
left a comment
There was a problem hiding this comment.
Thanks, this look promising !
I ran the pylint primer on the branch and found no new pylint false positives. But there's a set of cases where lookup() resolves differently from Python, so the tip now bails where it used to infer.
from builtins import str
str(42)main infers Const('42'), this branch infers a plain Instance of builtins.str.
class Fruit:
size = len("apple") # Python: builtin, `len` is not bound in the class namespace yet
def len(self):
return 0main infers 5, this branch raises InferenceError. Same at module level:
size = len([1, 2, 3])
def len(x):
return "shadowed"main infers 3, this branch raises InferenceError.
def h(x, len=len([1, 2, 3])): # the default is evaluated in the *enclosing* scope
return xmain infers 3, this branch infers Uninferable.
| if not _is_builtin_call(node): | ||
| raise UseInferenceDefault |
There was a problem hiding this comment.
This prevents the use of _inference_tip_cached later, not sure if it's worth resturcturing for.
There was a problem hiding this comment.
Yeah, fair point. I left the check in the tip so the cheap name filter stays as-is. Pulling it into the predicate (or otherwise making misses cacheable) felt like a bigger reshuffle than this bug needs. Easy to revisit if it shows up hot.
|
Thanks for the careful look. Pushed a follow-up that should cover the cases you hit:
Full suite is green on my side. Happy to tweak further if anything still looks off. |
|
You were right that Name.lookup already handles statement order. I think there's still a a scoped_nodes.py bug. _name_lookup_frame duplicates Lambda/FunctionDef.scope_lookup's default branch and calls the private _scope_lookup. Those test def h(x, len=[len(y) for y in ([1, 2],)]):
def h(x, len=lambda: len([1, 2])):But it's pre-existing, feel free to not touch. Either way, annotations are the same family and neither version covers them (def h(x: str("a") = 1, str=None)), decorators likewise. |
|
Went ahead and fixed it in scoped_nodes, you were right that it was the better place.
def h(x, len=[len(y) for y in ([1, 2, 3],)]):
def h(x, len=lambda: len([1, 2]))Both used to come back Uninferable on my branch and now infer 3, same as main. That also let me delete I wrote the helper to walk up from the node rather than call Added tests in test_lookup.py for the lookup change itself and in test_builtin.py for the two inference cases. Full suite green locally. You are right about annotations, they are still not covered. |
7794a9d to
86a414b
Compare
86a414b to
1681b06
Compare
|
Rebased on main. The only conflict was the ChangeLog, which is towncrier now, so the entry moved to The red pylint job was Local suite is 2088 passed, 2 failed, and both failures are my environment (no |
|
Thanks for applying the fragment edit and adding the xfail tests directly, nothing left on my side. Happy to take the two xfail cases in this PR if you would rather not leave them as follow-ups. |
|
Sorry I made #3232 and intended to merge it back here but got drowned in notifications. Fixing the xfail wxould make sense if you don't mind. |
register_builtin_transform selects its inference tip on the identifier
alone, so every one of the transforms it registers (bool, int, len, str,
type, isinstance, dict.fromkeys, ...) also fired on a name that merely
happened to spell a builtin. A parameter named `type` was inferred as
builtins.type, which is what makes pylint report
E1101: Class 'float' has no 'split' member
for `convert_type(12.34, str).split('.')` in pylint-dev/pylint#10994.
Resolve the identifier with lookup() before running the transform and
fall back to the default inference when it does not come from builtins.
brain_type already guards `type[...]` subscripts this way.
The check lives in the inference tip rather than in the transform
predicate, so it runs only when a builtin-named call is actually
inferred and parse time is untouched.
Closes pylint-dev/pylint#10994
Look up from the name node so later defs do not count, treat from-builtins imports as the real builtin, and resolve default-arg values in the enclosing scope. Add the cases Pierre raised plus the missing getattr/hasattr/property/super controls.
for more information, see https://pre-commit.ci
Lambda.scope_lookup and FunctionDef.scope_lookup only sent a node to the enclosing frame when it was one of the default values itself. A name nested inside a default, such as in a comprehension or a lambda, was resolved against the parameters it sits next to instead. Fixing it there lets the builtin shadowing check drop its own copy of the rule and just call lookup().
The tip now bails for every kind of binding, not only parameters, so inference can return Uninferable where it used to return a builtin result. Spell that out, along with the rebound-name case that now infers correctly.
An aliased builtins import binds the name to a different builtin than the one the tip is registered for, and annotations are evaluated in the enclosing scope just like defaults. Neither is handled yet; record both so they are tracked rather than rediscovered.
338ba45 to
2ecc442
Compare
An aliased import bound the wrong builtin: `from builtins import int as str` left `str(42)` inferring as a string, since the check only looked at the name the import binds, not the one it binds it to. Ask `real_name` instead. Annotations went to the function frame, so a parameter shadowing a builtin stole the name from the annotation next to it. They are evaluated where the function is defined, same as defaults, with one difference: annotations see the type parameters and defaults do not, which is why `_signature_part` now says which of the two a node sits in. Return annotations go the same way.
|
Rebased on main and fixed both xfails. The import one was checking the alias instead of what was imported under it, so The annotation one needed a small correction to the plan. Type parameters live in The xfail for the import case asserted Ran pylint's suite against this branch, same results as against main. |
A bare annotation is not a binding: ``len: int`` says what ``len`` should hold, it does not put anything there, so the builtin is still the one being called. ``lookup()`` hands the annotated name back anyway and the tip steps aside. ``brain_type.infer_type_sub`` has the same shape of bug this branch fixes elsewhere: it looks ``type`` up from the scope rather than from the name node, so a parameter called ``type`` hides the builtin from the annotation next to it and ``type[int]`` stops being subscriptable. That one only shows up now that the annotation resolves to the builtin at all; it costs a false positive on sentry.
There was a problem hiding this comment.
Thank you for fixing the two test case. I added two other test cases, The issue seems to be thatr brain_type.infer_type_sub lookup from scope, not name node:
- node_scope, _ = node.scope().lookup("type")
+ node_scope, _ = node.lookup("type")And for the other one we need to check is something is a bare annotation, there's probably a helper function for that.
|
The CodSpeed red looks like noise. The gate passed on 2ecc442, and the only commit since is your test-only one, which can't cost 10% on cold lint. main picked up the CodSpeedHQ action bump in between, and the report itself says it compared across different runtime environments. codecov is down to the Still on the two xfails. The bare annotation one is a bigger family than the single case: |
Type of Changes
Description
register_builtin_transformpicks its tip by name text alone. A parameter calledtypeis treated asbuiltins.type:astroid turns that into
ClassDef.float, and pylint reportsE1101: Class 'float' has no 'split' member(pylint-dev/pylint#10994). Same hole for every transform registered this way (len,str,list, ...).The fix is to resolve the name before running the transform, and fall back to normal inference when it is not the real builtin. Lookup goes through the name node (so later defs in the same scope do not count), with a small extra step for default args, which run in the enclosing scope.
from builtins import stris treated as the builtin too.Review follow-up
Addressed the cases Pierre flagged where the first version was too strict:
from builtins import str/from builtins import *defin the same module or class bodydef h(x, len=len([...])))Also filled in the missing unshadowed controls for
getattr,hasattr,property, andsuper.On the cache note: the check stays inside the tip (same place as before) so the cheap name filter is unchanged. Restructuring so the tip could still be cached on a miss did not seem worth the churn for this fix; happy to revisit if it shows up in profiles.
Verification
Closes pylint-dev/pylint#10994