Skip to content

Conversation

@11happy
Copy link
Contributor

@11happy 11happy commented Sep 15, 2025

Summary

This PR implements a new semantic syntax error where name is parameter & global.

Test Plan

I have written inline test as directed in #17412

@github-actions
Copy link
Contributor

github-actions bot commented Sep 15, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

///
/// [#111123]: https://github.com/python/cpython/issues/111123
LoadBeforeGlobalDeclaration { name: String, start: TextSize },
LoadBeforeGlobalDeclaration {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer not to reformat unrelated sections of code, as I mentioned on your other PR

Copy link
Contributor Author

@11happy 11happy Sep 29, 2025

Choose a reason for hiding this comment

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

sure , actually I am not changing these , these are automatically added by pre-commit jobs

@github-actions
Copy link
Contributor

github-actions bot commented Sep 29, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

@github-actions
Copy link
Contributor

github-actions bot commented Sep 29, 2025

mypy_primer results

No ecosystem changes detected ✅
No memory usage changes detected ✅

Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thank you, this is looking good. We just need to be a bit more careful with our scope handling and revert the unnecessary Violation changes.

}
)
}
fn is_binded_parameter(&self, name: &str) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I think a name like is_parameter or is_bound_parameter would be a bit better.

More importantly, and related to my comment on the test case above, I don't think this is quite right. We need to limit our parameter check to the nearest-enclosing function scope. In other words, we need to search through self.semantic.current_scopes more like we do in some of the other context methods:

fn in_async_context(&self) -> bool {
for scope in self.semantic.current_scopes() {
match scope.kind {
ScopeKind::Class(_) | ScopeKind::Lambda(_) => return false,
ScopeKind::Function(ast::StmtFunctionDef { is_async, .. }) => return *is_async,
ScopeKind::Generator { .. }
| ScopeKind::Module
| ScopeKind::Type
| ScopeKind::DunderClassCell => {}
}
}
false
}

until we find the first function scope, and then only check for shadowed bindings within that scope. We can also return early if we find a class scope, for example, because this is also valid, like the nested function case I showed above:

>>> def h(a):
...     class C:
...         global a
...
>>>

I think the checks on the binding below look roughly correct, but we need to be more careful about identifying the scope holding the relevant binding.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thank you, this is looking good for the tests currently implemented. I think we should a few more test cases, for example:

def f(a):
    a = 1
    global a  # error

This tests the shadowing code. I'm actually not sure I was correct to recommend checking shadowed_bindings. Rereading the docstring, it sounds like this only catches shadowing from different scopes. We might actually need to check for rebinding_scopes. In any case, a test like the one above, and possibly one with multiple rebindings, would be a good idea:

def f(a):
    a = 1
    a = 2
    global a  # error

I see that is_bound_parameter also has special handling for classes, so it seems worth including a test case with an intervening class scope, probably more than one case showing some of the interleavings like function-class-function, class-function-class, etc, or at least the interesting ones. I think something like this would be interesting at a minimum:

def f(a):
    class Inner:
        global a

I also had a couple of minor nits about spaces between functions.

@11happy
Copy link
Contributor Author

11happy commented Oct 11, 2025

So the shadowed_bindings in SemanticModel and Scope are different, respectively. As per the example given in the docstring of Scope

/// A map from binding ID to binding ID that it shadows.
///
/// For example:
/// ```python
/// def f():
///     x = 1
///     x = 2
/// ```
///
/// In this case, the binding created by `x = 2` shadows the binding created by `x = 1`.
shadowed_bindings: FxHashMap<BindingId, BindingId>,

We can utilize this to trace back whether a global declaration is originally an argument or not — and I’ve done that accordingly. I also think we don’t really need to interact with rebindings_scope here. I manually checked with an example, and it was coming out to be empty in the following case:

def f(a):
    a = 1
    a = 2
    global a

Also, while trying to solve this issue, I read a lot about bindings, shadowed bindings, etc., and I’m genuinely thankful for the opportunity to work on this. The tests are working as expected now

Thank you : )

@11happy
Copy link
Contributor Author

11happy commented Oct 12, 2025

is the CI/fuzz parser error related to this PR changes ?

Comment on lines 58 to 64
invalid-syntax: name `a` is parameter and global
--> <filename>:30:16
|
28 | def f(a):
29 | class Inner:
30 | global a
| ^
Copy link
Contributor

Choose a reason for hiding this comment

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

This is what the fuzzer seeds that are failing look like. This is not supposed to be an error:

>>> def f(a):
...     class Inner:
...         global a
...
>>> f
<function f at 0x7fbec085be20>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done , have fixed it & fixed the merge conflict as well (rebased)
Thank you : )

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently we are doing an early return for class scope , please let me know if there are any more edge cases you would like me to test , would be happy to add them.

Thank you

@11happy 11happy force-pushed the name_is_param_and_global branch from 568cb96 to 9ab9411 Compare October 14, 2025 16:43
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thanks, this is looking good. I had one idea to simplify is_bound_parameter and a suggestion for a follow-up.

ScopeKind::Class(_) => {
return false;
}
ScopeKind::Function(_) | ScopeKind::Lambda(_) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I overlooked this initially, but the ScopeKinds here actually contain these nodes' definitions. We should be able to extract the parameters from that and avoid this binding stuff entirely. I always get a little nervous when I see a loop, so I looked a bit harder at this today.

    fn is_bound_parameter(&self, name: &str) -> bool {
        for scope in self.semantic.current_scopes() {
            match scope.kind {
                ScopeKind::Class(_) => return false,
                ScopeKind::Function(ast::StmtFunctionDef { parameters, .. })
                | ScopeKind::Lambda(ast::ExprLambda {
                    parameters: Some(parameters),
                    ..
                }) => return parameters.includes(name),
                ScopeKind::Lambda(_)
                | ScopeKind::Generator { .. }
                | ScopeKind::Module
                | ScopeKind::Type
                | ScopeKind::DunderClassCell => {}
            }
        }

        false
    }

I believe it's actually impossible for a lambda to contain a global since it can only contain a single expression and not a statement, so we could probably eliminate the lambda branch here too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now that I’m looking at this and the previous iterations, I feel like if we’re starting to make the code more complex, something’s probably off with the approach. From now on, I’ll be more careful and focus on keeping things simpler.

Thank you : )

Signed-off-by: 11happy <soni5happy@gmail.com>
Signed-off-by: 11happy <soni5happy@gmail.com>
Signed-off-by: 11happy <soni5happy@gmail.com>
Signed-off-by: 11happy <soni5happy@gmail.com>
Signed-off-by: 11happy <soni5happy@gmail.com>
Signed-off-by: 11happy <soni5happy@gmail.com>
Signed-off-by: 11happy <soni5happy@gmail.com>
Signed-off-by: 11happy <soni5happy@gmail.com>
Signed-off-by: 11happy <soni5happy@gmail.com>
Signed-off-by: 11happy <soni5happy@gmail.com>
Signed-off-by: 11happy <soni5happy@gmail.com>
@11happy 11happy force-pushed the name_is_param_and_global branch from 81a26be to 327c8af Compare October 16, 2025 16:55
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thank you!

@ntBre ntBre changed the title [Syntax-errors] : Implement semantic syntax error where name is parameter and global [syntax-errors] Name is parameter and global Oct 21, 2025
@ntBre ntBre added the rule Implementing or modifying a lint rule label Oct 21, 2025
@ntBre ntBre enabled auto-merge (squash) October 21, 2025 16:46
@ntBre ntBre merged commit 3dd78e7 into astral-sh:main Oct 21, 2025
41 checks passed
dcreager added a commit that referenced this pull request Oct 22, 2025
* main: (65 commits)
  [ty] Some more simplifications when rendering constraint sets (#21009)
  [ty] Make `attributes.md` mdtests faster (#21030)
  [ty] Set `INSTA_FORCE_PASS` and `INSTA_OUTPUT` environment variables from mdtest.py (#21029)
  [ty] Fall back to `Divergent` for deeply nested specializations (#20988)
  [`ruff`] Autogenerate TypeParam nodes (#21028)
  [ty] Add assertions to ensure that we never call `KnownClass::Tuple.to_instance()` or similar (#21027)
  [`ruff`] Auto generate ast Pattern nodes (#21024)
  [`flake8-simplify`] Skip `SIM911` when unknown arguments are present (#20697)
  Render a diagnostic for syntax errors introduced in formatter tests (#21021)
  [ty] Support goto-definition on vendored typeshed stubs (#21020)
  [ty] Implement go-to for binary and unary operators (#21001)
  [ty] Avoid ever-growing default types (#20991)
  [syntax-errors] Name is parameter and global (#20426)
  [ty] Disable panicking mdtest (#21016)
  [ty] Fix completions at end of file (#20993)
  [ty] Fix out-of-order semantic token for function with regular argument after kwargs (#21013)
  [ty] Fix auto import for files with `from __future__` import (#20987)
  [`fastapi`] Handle ellipsis defaults in FAST002 autofix (`FAST002`) (#20810)
  [`ruff`] Skip autofix for keyword and `__debug__` path params (#20960)
  [`flake8-bugbear`] Skip `B905` and `B912` if <2 iterables and no starred arguments (#20998)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants