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

enforce required imports even with useless alias #14287

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

Conversation

dylwil3
Copy link
Collaborator

@dylwil3 dylwil3 commented Nov 11, 2024

This PR handles a panic that occurs when applying unsafe fixes if a user inserts a required import (I002) that has a "useless alias" in it, like import numpy as numpy, and also selects PLC0414 (useless-import-alias)

In this case, the fixes alternate between adding the required import statement, then removing the alias, until the recursion limit is reached. See linked issue for an example.

Tests to confirm panic resolved:

$ echo 1 | cargo run --quiet -p ruff -- check --no-cache --isolated --select I002,PLC0414 --config 'lint.isort.required-imports = ["from module import this as this"]' - --unsafe-fixes --fix
from module import this as this
1
Found 1 error (1 fixed, 0 remaining).
$ echo 1 | cargo run --quiet -p ruff -- check --no-cache --isolated --select I002,PLC0414 --config 'lint.isort.required-imports = ["import numpy as numpy"]' - --unsafe-fixes --fix
import numpy as numpy
1
Found 1 error (1 fixed, 0 remaining).

Closes #14283

@dylwil3
Copy link
Collaborator Author

dylwil3 commented Nov 11, 2024

Not so happy about the big diff in such a simple rule for an edge case... I welcome alternative suggestions!

Copy link
Contributor

github-actions bot commented Nov 11, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

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

Could you expand the PR summary with details about what was causing the infinite loop or panic? It would help me review this PR and possibly suggest alternatives.

@dylwil3
Copy link
Collaborator Author

dylwil3 commented Nov 12, 2024

Done! It also occurred to me that it might be better to keep the diagnostic but elide the fix for useless-import-alias. That would serve the purpose of warning the user that something was amiss, and they could override the warning by ignoring the rule.

Edit: Made this change. I did not change the "Fix availability" or documentation because it seems like this is a very rare (nonexistent?) exception, but let me know what you think.

let Some(asname) = &alias.asname else {
return;
};
if alias.name.contains('.') {
Copy link
Member

Choose a reason for hiding this comment

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

Why this part?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't know... This seems to have been there since the original implementation: https://github.com/harupy/ruff/blob/d765fbe5ddca1fa7b735256ebb303799a2e7aa95/src/pylint/plugins.rs#L16

I don't think it's possible to have an alias with a dot in it... maybe it's supposed to be an early return? Is checking for a dot faster than comparing two strings? Certainly can't be that much of a speed up... Anyway, happy to remove it.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks. Do you mind removing it (here and in the linked location) in a separate PR?

Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to implement helper methods on the isort settings to test if it is a required import to avoid duplicating the logic?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok! Refactored to use new helper methods on isort settings, and I will follow up after this PR is merged to get rid of the check for "."

@charliermarsh
Copy link
Member

Do you mind adding a dedicated test for this? Sorry!

@MichaReiser MichaReiser added bug Something isn't working fixes Related to suggested fixes for violations labels Nov 13, 2024
Comment on lines 57 to 61
// While the fix is specified as always available,
// the presence of a user-specified required import (I002)
// with a useless alias causes an infinite loop. So
// in this case we keep the diagnostic but omit the fix.
// See https://github.com/astral-sh/ruff/issues/14283
Copy link
Member

Choose a reason for hiding this comment

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

I don't think our testing framework will accept that an always fixable fix isn't always fixable, which makes sense.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Of course you're right! Changed to "Sometimes" with special message/fix title emitted in case of conflict.

@dylwil3
Copy link
Collaborator Author

dylwil3 commented Nov 14, 2024

Do you mind adding a dedicated test for this? Sorry!

Added!

Comment on lines +70 to +95
if checker
.settings
.isort
.requires_module_import(alias.name.to_string(), Some(asname.to_string()))
{
let diagnostic = Diagnostic::new(
UselessImportAlias {
required_import_conflict: true,
},
alias.range(),
);
checker.diagnostics.push(diagnostic);
return;
}

let mut diagnostic = Diagnostic::new(
UselessImportAlias {
required_import_conflict: false,
},
alias.range(),
);
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
asname.to_string(),
alias.range(),
)));
checker.diagnostics.push(diagnostic);
Copy link
Member

Choose a reason for hiding this comment

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

Nit: To reduce the amount of repeated code

Suggested change
if checker
.settings
.isort
.requires_module_import(alias.name.to_string(), Some(asname.to_string()))
{
let diagnostic = Diagnostic::new(
UselessImportAlias {
required_import_conflict: true,
},
alias.range(),
);
checker.diagnostics.push(diagnostic);
return;
}
let mut diagnostic = Diagnostic::new(
UselessImportAlias {
required_import_conflict: false,
},
alias.range(),
);
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
asname.to_string(),
alias.range(),
)));
checker.diagnostics.push(diagnostic);
let mut diagnostic = Diagnostic::new(
UselessImportAlias {
required_import_conflict: true,
},
alias.range(),
);
if !checker
.settings
.isort
.requires_module_import(alias.name.to_string(), Some(asname.to_string()))
{
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
asname.to_string(),
alias.range(),
)));
}
checker.diagnostics.push(diagnostic);

Comment on lines +105 to +113
let Some(asname) = &alias.asname else {
return;
};
if alias.name.contains('.') {
return;
}
if alias.name.as_str() != asname.as_str() {
return;
}
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible to move this as well into requires_member_import?

Comment on lines +116 to +137
if checker.settings.isort.requires_member_import(
module.map(str::to_string),
alias.name.to_string(),
Some(asname.to_string()),
level,
) {
let diagnostic = Diagnostic::new(
UselessImportAlias {
required_import_conflict: true,
},
alias.range(),
);
checker.diagnostics.push(diagnostic);
return;
}

let mut diagnostic = Diagnostic::new(UselessImportAlias, alias.range());
let mut diagnostic = Diagnostic::new(
UselessImportAlias {
required_import_conflict: false,
},
alias.range(),
);
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Same as above, let's invert the check so that we don't need to duplicate the diagnostic creation

@@ -75,6 +75,29 @@ pub struct Settings {
pub length_sort_straight: bool,
}

impl Settings {
pub fn requires_module_import(&self, name: String, as_name: Option<String>) -> bool {
Copy link
Member

Choose a reason for hiding this comment

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

Can we use these new methods in the isort implementation as well? It would help to ensure that both implementations stay in sync

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working fixes Related to suggested fixes for violations
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Infinite loop] PLC0414 conflicts with I002 if a useless alias is required
3 participants