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

New linter for complex conditional expressions #2676

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

Conversation

IndrajeetPatil
Copy link
Collaborator

@IndrajeetPatil IndrajeetPatil commented Oct 26, 2024

closes #1830

Draft PR for early feedback.


Examples

library(lintr)

lint(
  text = "if (a && b) NULL",
  linters = complex_conditional_linter()
)
#> ℹ No lints found.

lint(
  text = "if (a && b || c) NULL",
  linters = complex_conditional_linter()
)
#> ℹ No lints found.

lint(
  text = "if (a && b || c) NULL",
  linters = complex_conditional_linter(1L)
)
#> <text>:1:5: warning: [complex_conditional_linter] Complex conditional with more than 1 logical operator(s). Consider extracting into a boolean function or variable for readability and reusability.
#> if (a && b || c) NULL
#>     ^~~~~~~~~~~

Created on 2024-11-01 with reprex v2.1.1

@MichaelChirico
Copy link
Collaborator

I don't think a && b || c is complex at all TBH, besides the mixing of precedences (but presumably (a && b) || c would also throw a lint here?), which I think is a separate issue.

I'm more comfortable calling nontrivial_expr(a) && nontrivial_expr(b) || nontrivial_expr(c) complex, but just plain names don't seem to add complexity IMO.

Defining the metric for "complexity" here is going to be quite hard -- are there other languages that have implemented something similar, or is there some CS literature we can turn to for some more generic definitions?

@IndrajeetPatil
Copy link
Collaborator Author

Defining the metric for "complexity" here is going to be quite hard -- are there other languages that have implemented something similar, or is there some CS literature we can turn to for some more generic definitions?

You are correct that this is a difficult question, but I don't think we need to have a consensus definition of what counts as "complex" conditional because that's an inherently subjective notion. The only thing we need to figure out is a good default (the same way we did for cyclocomp linter). And, since this is a configurable linter, even if we adopt a threshold that users feel is too restrictive, they can easily change this in config.

I think the current default of threshold = 1L is too aggressive, but we can easily change it to threshold = 2L (or higher):

lint(
  text = "if (a && b || c) NULL",
  linters = complex_conditional_linter(2)
)
#> ℹ No lints found.

So the question for us to resolve is what should be the default threshold?
How did we decide on 15L as the default threshold for McCabe complexity linter?


I would vote for threshold = 2L; any conditional expression with more than 2 operators (and 3 logical operands) can benefit from simplification, IMO.

Here is an example from our codebase:

if (inherits(e, "lint") && (is.na(e$line) || !nzchar(e$line) || e$message == "unexpected end of input")) {
  ...
}

which lints with this new linter and can be modified something along these lines:

is_expression_valid <- (is.na(e$line) || !nzchar(e$line) || e$message == "unexpected end of input")
if (inherits(e, "lint") && is_expression_valid) {
  ...
}

P.S. As for linters in other programming languages, I can't think of any (neither ruff nor eslint have anything similar; the closest ones are about complex conditional in type stubs or mixing operators).

@IndrajeetPatil IndrajeetPatil marked this pull request as ready for review November 5, 2024 22:09
@IndrajeetPatil IndrajeetPatil changed the title Draft: New linter for complex conditional expressions New linter for complex conditional expressions Nov 5, 2024
@IndrajeetPatil IndrajeetPatil requested review from AshesITR and MichaelChirico and removed request for AshesITR November 21, 2024 19:06
@IndrajeetPatil
Copy link
Collaborator Author

I don't understand why the repo-meta-tests workflow is failing. There are already metadata tests for the new linter.

@AshesITR
Copy link
Collaborator

AshesITR commented Nov 24, 2024

What happens when you run .dev/lint_metadata_tests.R locally?

From my understanding the script replaces all line numbers by 2^31 - 1 during Lint() construction and assumes the test file for each linter the_linter is named test-the_linter.R.

--- edit: Ah, the file name is the problem. Rename the test file and it should work.

@IndrajeetPatil
Copy link
Collaborator Author

@AshesITR Thanks!! That was it. Changing the filename fixed the issue.

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

Successfully merging this pull request may close these issues.

New linter to suggest moving complex conditional expressions to boolean function/variable?
3 participants