Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pylint] Implement global-variable-undefined (W0601) #10820

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

tibor-reiss
Copy link
Contributor

Add pylint rule global-variable-undefined (W0601)

See #970 for rules

Test Plan: cargo test

@tibor-reiss tibor-reiss force-pushed the add-W0601 branch 5 times, most recently from 181ccaa to 4ea1948 Compare April 7, 2024 19:36
Copy link
Contributor

github-actions bot commented Apr 7, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+4 -0 violations, +0 -0 fixes in 2 projects; 52 projects unchanged)

apache/airflow (+2 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --output-format concise --preview --select ALL

+ tests/listeners/class_listener.py:39:13: PLW0601 Global variable `stopped_component` is undefined at the module
+ tests/listeners/class_listener.py:71:13: PLW0601 Global variable `stopped_component` is undefined at the module

bokeh/bokeh (+2 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --output-format concise --preview --select ALL

+ examples/output/jupyter/push_notebook/Numba Image Example.ipynb:cell 18:2:5: PLW0601 Global variable `_last_kname` is undefined at the module
+ examples/output/jupyter/push_notebook/Numba Image Example.ipynb:cell 18:3:5: PLW0601 Global variable `_last_out` is undefined at the module

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
PLW0601 4 4 0 0 0

@tusharsadhwani
Copy link
Contributor

Does this break on wildcard imports?

from os import *

def foo():
    global system  # W0601 should not be raised
    system = lambda _: None

@tibor-reiss
Copy link
Contributor Author

Does this break on wildcard imports?

from os import *

def foo():
    global system  # W0601 should not be raised
    system = lambda _: None

Yes, it does. I have not found a function in ruff which resolves star-imports, e.g. there is F403 and F405 which are raised for star-imports and warn about "...unable to detect..." and "...may be undefined...".

@MichaReiser
Copy link
Member

I think there's a way to detect if there's a star import in which case you could avoid creating any diagnostics (to avoid false-positives).

The main open question to me are

  • how this fits into ruff long term when Ruff catches more static analyzable errors (undefined variables in general)
  • What's the motivation that this rule is specific to globals and doesn't apply to all variables.

@MichaReiser MichaReiser added the rule Implementing or modifying a lint rule label Apr 15, 2024
@tibor-reiss
Copy link
Contributor Author

@MichaReiser
Regarding star imports: when I do from os import *, pylint resolves this and e.g. has system in the namespace. Ruff is not able to do this (yet). For now, I simply left this out in this PR. What is possible though is to add something similar to F403/F405, i.e. if there is a start import, then raise "global variable undefined but maybe imported via star import". What's your preference?

@MichaReiser
Copy link
Member

@tibor-reiss my preference would be that we don't flag any variables if a file contains a star import. I think there's already precedence for this in Ruff but I would need to search it to. @charliermarsh should know where to find it.

@tibor-reiss tibor-reiss force-pushed the add-W0601 branch 2 times, most recently from 6d1d4ca to 68bb046 Compare May 16, 2024 09:37
@tibor-reiss
Copy link
Contributor Author

@MichaReiser thanks for feedback, I added the check for star imports together with a new test file.

@tibor-reiss
Copy link
Contributor Author

Hi @MichaReiser, this got lost in translation. I have updated now to the latest master, could you please have a look and let me know if something is still missing?

@MichaReiser
Copy link
Member

Thanks @tibor-reiss

I took a quick look at the ecosystem result and it's not clear why the rule raises

Is it because there's only a declaration of Session

https://github.com/apache/airflow/blob/ee87fa0cba4d83084b4bc617d63d117101d9e069/airflow/settings.py#L99

Comment on lines 64 to 70
let Some(module_scope) = checker
.semantic()
.current_scopes()
.find(|scope| scope.kind.is_module())
else {
return;
};
Copy link
Member

Choose a reason for hiding this comment

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

Do you think we could use global_scope here instead?

Suggested change
let Some(module_scope) = checker
.semantic()
.current_scopes()
.find(|scope| scope.kind.is_module())
else {
return;
};
let module_scope = checker
.semantic()
.global_scope();

}

fn get_imports<'a>(checker: &'a Checker) -> Vec<&'a str> {
// Get all names imported in the current scope
Copy link
Member

Choose a reason for hiding this comment

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

Why do we only look at imports of the current scope? What about imports in the enclosing scope?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This covers the case when the global statement is before the import statement - see tests. I additionally added the check for imports in is_global_binding.

Co-authored-by: Micha Reiser <[email protected]>
@tibor-reiss
Copy link
Contributor Author

tibor-reiss commented Sep 29, 2024

Thanks @tibor-reiss

I took a quick look at the ecosystem result and it's not clear why the rule raises

* [airflow/settings.py:441:5:](https://github.com/apache/airflow/blob/ee87fa0cba4d83084b4bc617d63d117101d9e069/airflow/settings.py#L441) PLW0601 Global variable `Session` is undefined at the module

Is it because there's only a declaration of Session

https://github.com/apache/airflow/blob/ee87fa0cba4d83084b4bc617d63d117101d9e069/airflow/settings.py#L99

You are right: there was a case missing, i.e. the global variable was just declared (with type annotation). I have added this case now (with test).

@MichaReiser what do you think about this case? I have it now covered. However, the rule reads undefined, so I think there is a strong case for raising the warning if the global was only declared.

@tibor-reiss tibor-reiss force-pushed the add-W0601 branch 5 times, most recently from 56b56c6 to 7b9de3e Compare September 29, 2024 16:30
@dhruvmanila dhruvmanila added the preview Related to preview mode features label Oct 1, 2024
Comment on lines +102 to +111
let Some(fs) = checker
.semantic()
.current_scopes()
.find(|scope| scope.kind.is_function())
else {
return vec![];
};
let ScopeKind::Function(ast::StmtFunctionDef { body, .. }) = fs.kind else {
return vec![];
};
Copy link
Member

Choose a reason for hiding this comment

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

Nit:

Suggested change
let Some(fs) = checker
.semantic()
.current_scopes()
.find(|scope| scope.kind.is_function())
else {
return vec![];
};
let ScopeKind::Function(ast::StmtFunctionDef { body, .. }) = fs.kind else {
return vec![];
};
let Some(ast::StmtFunctionDef { body, .. }) = checker
.semantic()
.current_scopes()
.find_map(|scope| scope.kind.as_function())
else {
return vec![];
};

Comment on lines +113 to +123
for stmt in body {
match stmt {
Stmt::Import(ast::StmtImport { names, .. })
| Stmt::ImportFrom(ast::StmtImportFrom { names, .. }) => {
for name in names {
import_names.push(name.name.as_str());
}
}
_ => (),
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Should this also respect imports in conditional branches. E.g. what about:

def test(a):
	if a:
		from b import bar

	globals bar

Comment on lines +1 to +4
# pylint: disable=invalid-name, import-outside-toplevel, too-few-public-methods, unused-import
# pylint: disable=missing-module-docstring, missing-function-docstring, missing-class-docstring
# pylint: disable=global-at-module-level, global-statement, global-variable-not-assigned
# pylint: diable=redefined-outer-name
Copy link
Member

Choose a reason for hiding this comment

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

We can remove those comments because our tests run in isolation


for name in names {
// Skip if imported names
if imported_names.contains(&name.as_str()) {
Copy link
Member

Choose a reason for hiding this comment

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

My python knowledge is too limited to understand why it is okay to ignore imports from the enclosing function's scope. Could you help me understand why it is okay to ignore imports?

I read https://docs.python.org/3/reference/simple_stmts.html#the-global-statement and couldn't find any special handling of imports in the enclosing scope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
preview Related to preview mode features rule Implementing or modifying a lint rule
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants