├── cran-comments.md ├── .github ├── .gitignore ├── ISSUE_TEMPLATE │ ├── bug--wrong-fix.md │ └── bug--false-positive.md └── workflows │ ├── lint.yml │ ├── check-link-rot.yml │ ├── format.yml │ ├── pkgdown.yaml │ └── R-CMD-check.yaml ├── jarl.toml ├── vignettes ├── .gitignore ├── demo.gif └── automatic_fixes.Rmd ├── inst ├── cache_file_state.rds ├── rules │ └── builtin │ │ ├── todo_comment.yml │ │ ├── length_levels.yml │ │ ├── which_grepl.yml │ │ ├── rep_len.yml │ │ ├── equal_assignment.yml │ │ ├── right_assignment.yml │ │ ├── numeric_leading_zero.yml │ │ ├── function_return.yml │ │ ├── absolute_path.yml │ │ ├── empty_assignment.yml │ │ ├── undesirable_function.yml │ │ ├── expect_length.yml │ │ ├── expect_null.yml │ │ ├── expect_not.yml │ │ ├── condition_message.yml │ │ ├── double_assignment.yml │ │ ├── list_comparison.yml │ │ ├── library_call.yml │ │ ├── for_loop_index.yml │ │ ├── any_is_na.yml │ │ ├── stopifnot_all.yml │ │ ├── expect_true_false.yml │ │ ├── outer_negation.yml │ │ ├── nested_ifelse.yml │ │ ├── is_numeric.yml │ │ ├── redundant_equals.yml │ │ ├── expect_comparison.yml │ │ ├── missing_argument.yml │ │ ├── unnecessary_nesting.yml │ │ ├── undesirable_operator.yml │ │ ├── expect_identical.yml │ │ ├── duplicate_argument.yml │ │ ├── sample_int.yml │ │ ├── equals_na.yml │ │ ├── class_equals.yml │ │ ├── unreachable_code.yml │ │ ├── length_test.yml │ │ ├── redundant_ifelse.yml │ │ ├── paste.yml │ │ ├── implicit_assignment.yml │ │ ├── literal_coercion.yml │ │ ├── lengths.yml │ │ ├── expect_named.yml │ │ ├── expect_s4_class.yml │ │ ├── T_and_F_symbol.yml │ │ ├── sort.yml │ │ ├── any_duplicated.yml │ │ └── seq.yml ├── WORDLIST ├── gha │ └── flir.yaml └── config.yml ├── LICENSE ├── tests ├── testthat │ ├── _snaps │ │ ├── undesirable_function.md │ │ ├── T_and_F_symbol.md │ │ ├── flint_ignore.md │ │ ├── length_levels.md │ │ ├── setup_flir.md │ │ ├── rep_len.md │ │ ├── which_grepl.md │ │ ├── outer_negation.md │ │ ├── fix_several_files.md │ │ ├── stopifnot_all.md │ │ ├── expect_identical.md │ │ ├── numeric_leading_zero.md │ │ ├── expect_not.md │ │ ├── sample_int.md │ │ ├── expect_comparison.md │ │ ├── sort.md │ │ ├── expect_length.md │ │ ├── export_new_rule.md │ │ ├── expect_null.md │ │ ├── add_new_rule.md │ │ ├── length_test.md │ │ ├── lengths.md │ │ ├── print.md │ │ ├── expect_true_false.md │ │ ├── message.md │ │ ├── expect_named.md │ │ ├── class_equals.md │ │ ├── config_yml.md │ │ ├── nested-lints.md │ │ ├── expect_s4_class.md │ │ ├── condition_message.md │ │ ├── redundant_ifelse.md │ │ ├── literal_coercion.md │ │ ├── expect_type.md │ │ └── seq.md │ ├── test-fix_several_files.R │ ├── test-exclude_linters.R │ ├── test-setup_flir_gha.R │ ├── test-length_levels.R │ ├── test-path.R │ ├── test-nested-lints.R │ ├── test-todo_comment.R │ ├── test-undesirable_function.R │ ├── test-undesirable_operator.R │ ├── test-list_linters.R │ ├── test-several_paths.R │ ├── test-list_comparison.R │ ├── test-setup_flir.R │ ├── test-which_grepl.R │ ├── test-for_loop_index.R │ ├── test-stopifnot_all.R │ ├── test-redundant_equals.R │ ├── test-add_new_rule.R │ ├── test-any_is_na.R │ ├── test-print.R │ ├── test-numeric_leading_zero.R │ ├── test-empty_assignment.R │ ├── test-length_test.R │ ├── test-export_new_rule.R │ ├── test-expect_not.R │ ├── test-expect_comparison.R │ ├── test-function_return.R │ ├── test-outer_negation.R │ ├── test-expect_true_false.R │ ├── test-nested_ifelse.R │ ├── test-message.R │ ├── test-expect_s4_class.R │ ├── test-expect_null.R │ ├── test-expect_length.R │ ├── test-rep_len.R │ ├── test-lengths.R │ ├── test-equals_na.R │ └── test-flint_ignore.R ├── spelling.R └── testthat.R ├── .Rbuildignore ├── man ├── seq_linter.Rd ├── empty_assignment_linter.Rd ├── equal_assignment_linter.Rd ├── right_assignment_linter.Rd ├── double_assignment_linter.Rd ├── implicit_assignment_linter.Rd ├── nzchar_linter.Rd ├── sort_linter.Rd ├── equals_na_linter.Rd ├── library_call_linter.Rd ├── todo_comment_linter.Rd ├── lengths_linter.Rd ├── package_hooks_linter.Rd ├── paste_linter.Rd ├── rep_len_linter.Rd ├── which_grepl_linter.Rd ├── class_equals_linter.Rd ├── T_and_F_symbol_linter.Rd ├── any_is_na_linter.Rd ├── missing_argument_linter.Rd ├── stopifnot_all_linter.Rd ├── length_levels_linter.Rd ├── nested_ifelse_linter.Rd ├── duplicate_argument_linter.Rd ├── expect_not_linter.Rd ├── expect_null_linter.Rd ├── expect_s3_class_linter.Rd ├── sample_int_linter.Rd ├── length_test_linter.Rd ├── undesirable_function_linter.Rd ├── undesirable_operator_linter.Rd ├── vector_logic_linter.Rd ├── is_numeric_linter.Rd ├── for_loop_index_linter.Rd ├── redundant_equals_linter.Rd ├── unnecessary_nesting_linter.Rd ├── expect_named_linter.Rd ├── expect_type_linter.Rd ├── any_duplicated_linter.Rd ├── expect_identical_linter.Rd ├── expect_length_linter.Rd ├── function_return_linter.Rd ├── literal_coercion_linter.Rd ├── matrix_apply_linter.Rd ├── list_comparison_linter.Rd ├── expect_s4_class_linter.Rd ├── expect_true_false_linter.Rd ├── outer_negation_linter.Rd ├── numeric_leading_zero_linter.Rd ├── expect_comparison_linter.Rd ├── condition_message_linter.Rd ├── redundant_ifelse_linter.Rd ├── list_linters.Rd ├── setup_flir_gha.Rd ├── export_new_rule.Rd ├── add_new_rule.Rd ├── flir-package.Rd └── setup_flir.Rd ├── R ├── flir-package.R ├── print.R ├── rstudioapi.R ├── github_action.R └── review_app.R ├── flint.Rproj ├── _pkgdown.yml ├── .gitignore ├── LICENSE.md ├── pkgdown └── extra.scss ├── DESCRIPTION └── NAMESPACE /cran-comments.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /jarl.toml: -------------------------------------------------------------------------------- 1 | [lint] 2 | exclude = ["inst/"] -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | 4 | /.quarto/ 5 | -------------------------------------------------------------------------------- /vignettes/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etiennebacher/flir/HEAD/vignettes/demo.gif -------------------------------------------------------------------------------- /inst/cache_file_state.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etiennebacher/flir/HEAD/inst/cache_file_state.rds -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2025 2 | COPYRIGHT HOLDER: flir authors 3 | 4 | YEAR: 2025 5 | COPYRIGHT HOLDER: lintr authors 6 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/undesirable_function.md: -------------------------------------------------------------------------------- 1 | # browser() has no fix 2 | 3 | Code 4 | fix_text("browser()") 5 | 6 | -------------------------------------------------------------------------------- /tests/spelling.R: -------------------------------------------------------------------------------- 1 | if (requireNamespace('spelling', quietly = TRUE)) { 2 | spelling::spell_check_test( 3 | vignettes = TRUE, 4 | error = FALSE, 5 | skip_on_cran = TRUE 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /inst/rules/builtin/todo_comment.yml: -------------------------------------------------------------------------------- 1 | id: todo_comment-1 2 | language: r 3 | severity: warning 4 | rule: 5 | kind: comment 6 | regex: '(?i)#(|\s+)\b(todo|fixme)\b' 7 | message: Remove TODO comments. 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug--wrong-fix.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Bug: wrong fix' 3 | about: The code should be fixed, but the fix is wrong. 4 | title: '' 5 | labels: wrong-fix 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/T_and_F_symbol.md: -------------------------------------------------------------------------------- 1 | # don't replace T/F when they receive the assignment 2 | 3 | Code 4 | fix_text("T <- N/G") 5 | 6 | --- 7 | 8 | Code 9 | fix_text("F <- N/G") 10 | 11 | -------------------------------------------------------------------------------- /inst/rules/builtin/length_levels.yml: -------------------------------------------------------------------------------- 1 | id: length_levels-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: length(levels($VAR)) 6 | fix: nlevels(~~VAR~~) 7 | message: nlevels(x) is better than length(levels(x)). df 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug--false-positive.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Bug: false positive' 3 | about: Some code is marked as "lint" but it shouldn't. 4 | title: '' 5 | labels: false-positive 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /inst/rules/builtin/which_grepl.yml: -------------------------------------------------------------------------------- 1 | id: which_grepl-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: which(grepl($$$ARGS)) 6 | fix: grep(~~ARGS~~) 7 | message: grep(pattern, x) is better than which(grepl(pattern, x)). 8 | -------------------------------------------------------------------------------- /inst/rules/builtin/rep_len.yml: -------------------------------------------------------------------------------- 1 | id: rep_len-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: rep($OBJ, length.out = $LEN) 6 | fix: rep_len(~~OBJ~~, ~~LEN~~) 7 | message: Use rep_len(x, n) instead of rep(x, length.out = n). 8 | -------------------------------------------------------------------------------- /inst/rules/builtin/equal_assignment.yml: -------------------------------------------------------------------------------- 1 | id: equal_assignment 2 | language: r 3 | severity: hint 4 | rule: 5 | pattern: $LHS = $RHS 6 | has: 7 | field: lhs 8 | kind: identifier 9 | fix: ~~LHS~~ <- ~~RHS~~ 10 | message: Use <-, not =, for assignment. 11 | -------------------------------------------------------------------------------- /inst/rules/builtin/right_assignment.yml: -------------------------------------------------------------------------------- 1 | id: right_assignment 2 | language: r 3 | severity: hint 4 | rule: 5 | pattern: $RHS -> $LHS 6 | has: 7 | field: rhs 8 | kind: identifier 9 | fix: ~~LHS~~<- ~~RHS~~ 10 | message: Use <-, not ->, for assignment. 11 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^src/\.cargo$ 4 | ^README\.Rmd$ 5 | ^LICENSE\.md$ 6 | 7 | # flir files 8 | ^inst/flir 9 | ^docs$ 10 | ^pkgdown$ 11 | ^_pkgdown\.yml$ 12 | ^\.github$ 13 | ^cran-comments\.md$ 14 | ^CRAN-SUBMISSION$ 15 | 16 | ^jarl\.toml$ 17 | -------------------------------------------------------------------------------- /tests/testthat/test-fix_several_files.R: -------------------------------------------------------------------------------- 1 | test_that("Ask permission to use fix() on several files if Git is not used", { 2 | create_local_package() 3 | cat("x = 1", file = "R/test.R") 4 | cat("x = 2", file = "R/test2.R") 5 | expect_snapshot( 6 | fix_dir("R"), 7 | error = TRUE 8 | ) 9 | }) 10 | -------------------------------------------------------------------------------- /inst/rules/builtin/numeric_leading_zero.yml: -------------------------------------------------------------------------------- 1 | id: numeric_leading_zero-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: $VALUE 6 | any: 7 | - kind: float 8 | - kind: identifier 9 | regex: ^\.[0-9] 10 | fix: 0~~VALUE~~ 11 | message: Include the leading zero for fractional numeric constants. 12 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/flint_ignore.md: -------------------------------------------------------------------------------- 1 | # flir-ignore-start and end error if mismatch 2 | 3 | Code 4 | lint_text("# flir-ignore-start\nany(duplicated(x))\nany(duplicated(y))") 5 | Condition 6 | Error in `find_lines_to_ignore()`: 7 | ! Mismatch: the number of `start` patterns (1) and of `end` patterns (0) must be equal. 8 | 9 | -------------------------------------------------------------------------------- /inst/WORDLIST: -------------------------------------------------------------------------------- 1 | Acknowledgements 2 | Arel 3 | Bundock 4 | CMD 5 | Github 6 | Jagan 7 | Jarl 8 | Linters 9 | Mikael 10 | ORCID 11 | RStudio 12 | ast 13 | astgrepr 14 | coercions 15 | fleer 16 | formatter 17 | grepl 18 | ints 19 | lapply 20 | len 21 | linter 22 | linters 23 | lintr 24 | nlevels 25 | nzchar 26 | sourceMarkers 27 | stopifnot 28 | unstaged 29 | ’s 30 | -------------------------------------------------------------------------------- /man/seq_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{seq_linter} 4 | \alias{seq_linter} 5 | \title{Sequence linter} 6 | \usage{ 7 | seq_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/seq_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/empty_assignment_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{empty_assignment_linter} 4 | \alias{empty_assignment_linter} 5 | \title{empty_assignment} 6 | \usage{ 7 | empty_assignment_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | empty_assignment 14 | } 15 | -------------------------------------------------------------------------------- /man/equal_assignment_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{equal_assignment_linter} 4 | \alias{equal_assignment_linter} 5 | \title{equal_assignment} 6 | \usage{ 7 | equal_assignment_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | equal_assignment 14 | } 15 | -------------------------------------------------------------------------------- /man/right_assignment_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{right_assignment_linter} 4 | \alias{right_assignment_linter} 5 | \title{right_assignment} 6 | \usage{ 7 | right_assignment_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | right_assignment 14 | } 15 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/length_levels.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("{\n length(levels(x))\n length(levels(y))\n}") 5 | Output 6 | Old code: 7 | { 8 | length(levels(x)) 9 | length(levels(y)) 10 | } 11 | 12 | New code: 13 | { 14 | nlevels(x) 15 | nlevels(y) 16 | } 17 | 18 | -------------------------------------------------------------------------------- /man/double_assignment_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{double_assignment_linter} 4 | \alias{double_assignment_linter} 5 | \title{double_assignment} 6 | \usage{ 7 | double_assignment_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | double_assignment 14 | } 15 | -------------------------------------------------------------------------------- /inst/rules/builtin/function_return.yml: -------------------------------------------------------------------------------- 1 | id: function_return-1 2 | language: r 3 | severity: warning 4 | rule: 5 | any: 6 | - pattern: return($OBJ <- $VAL) 7 | - pattern: return($OBJ <<- $VAL) 8 | - pattern: return($VAL -> $OBJ) 9 | - pattern: return($VAL ->> $OBJ) 10 | message: | 11 | Move the assignment outside of the return() clause, or skip assignment altogether. 12 | -------------------------------------------------------------------------------- /man/implicit_assignment_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{implicit_assignment_linter} 4 | \alias{implicit_assignment_linter} 5 | \title{implicit_assignment} 6 | \usage{ 7 | implicit_assignment_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | implicit_assignment 14 | } 15 | -------------------------------------------------------------------------------- /inst/rules/builtin/absolute_path.yml: -------------------------------------------------------------------------------- 1 | # id: absolute_path-1 2 | # language: r 3 | # severity: warning 4 | # rule: 5 | # kind: string_content 6 | # any: 7 | # - regex: '^~[[:alpha:]]' 8 | # - regex: '^~/[[:alpha:]]' 9 | # - regex: '^[[:alpha:]]:' 10 | # - regex: '^(/|~)$' 11 | # - regex: '^/[[:alpha:]]' 12 | # - regex: '^\\' 13 | # message: Do not use absolute paths. 14 | -------------------------------------------------------------------------------- /man/nzchar_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{nzchar_linter} 4 | \alias{nzchar_linter} 5 | \title{Require usage of nzchar where appropriate} 6 | \usage{ 7 | nzchar_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/nzchar_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/sort_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{sort_linter} 4 | \alias{sort_linter} 5 | \title{Check for common mistakes around sorting vectors} 6 | \usage{ 7 | sort_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/sort_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/equals_na_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{equals_na_linter} 4 | \alias{equals_na_linter} 5 | \title{Equality check with NA linter} 6 | \usage{ 7 | equals_na_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/equals_na_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/setup_flir.md: -------------------------------------------------------------------------------- 1 | # setup_flir works for packages 2 | 3 | Code 4 | fs::dir_tree("flir") 5 | Output 6 | flir 7 | +-- cache_file_state.rds 8 | \-- config.yml 9 | 10 | # setup_flir works for projects 11 | 12 | Code 13 | fs::dir_tree("flir") 14 | Output 15 | flir 16 | +-- cache_file_state.rds 17 | \-- config.yml 18 | 19 | -------------------------------------------------------------------------------- /man/library_call_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{library_call_linter} 4 | \alias{library_call_linter} 5 | \title{Library call linter} 6 | \usage{ 7 | library_call_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/library_call_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/todo_comment_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{todo_comment_linter} 4 | \alias{todo_comment_linter} 5 | \title{TODO comment linter} 6 | \usage{ 7 | todo_comment_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/todo_comment_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/lengths_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{lengths_linter} 4 | \alias{lengths_linter} 5 | \title{Require usage of \code{lengths()} where possible} 6 | \usage{ 7 | lengths_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/lengths_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/package_hooks_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{package_hooks_linter} 4 | \alias{package_hooks_linter} 5 | \title{Package hooks linter} 6 | \usage{ 7 | package_hooks_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/package_hooks_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/paste_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{paste_linter} 4 | \alias{paste_linter} 5 | \title{Raise lints for several common poor usages of \code{paste()}} 6 | \usage{ 7 | paste_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/paste_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/posit-dev/setup-air/tree/main/examples 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | 8 | name: Lint 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | jarl-check: 14 | name: jarl-check 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: etiennebacher/setup-jarl@v0.1.0 -------------------------------------------------------------------------------- /man/rep_len_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{rep_len_linter} 4 | \alias{rep_len_linter} 5 | \title{Require usage of rep_len(x, n) over rep(x, length.out = n)} 6 | \usage{ 7 | rep_len_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/rep_len_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/which_grepl_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{which_grepl_linter} 4 | \alias{which_grepl_linter} 5 | \title{Require usage of grep over which(grepl(.))} 6 | \usage{ 7 | which_grepl_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/which_grepl_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/class_equals_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{class_equals_linter} 4 | \alias{class_equals_linter} 5 | \title{Block comparison of class with \code{==}} 6 | \usage{ 7 | class_equals_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/class_equals_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/rep_len.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("rep(1:3, length.out = 5)", linters = linter) 5 | Output 6 | Old code: rep(1:3, length.out = 5) 7 | New code: rep_len(1:3, 5) 8 | 9 | --- 10 | 11 | Code 12 | fix_text("rep(x, length.out = 5)", linters = linter) 13 | Output 14 | Old code: rep(x, length.out = 5) 15 | New code: rep_len(x, 5) 16 | 17 | -------------------------------------------------------------------------------- /man/T_and_F_symbol_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{T_and_F_symbol_linter} 4 | \alias{T_and_F_symbol_linter} 5 | \title{\code{T} and \code{F} symbol linter} 6 | \usage{ 7 | T_and_F_symbol_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/T_and_F_symbol_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/any_is_na_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{any_is_na_linter} 4 | \alias{any_is_na_linter} 5 | \title{Require usage of \code{anyNA(x)} over \code{any(is.na(x))}} 6 | \usage{ 7 | any_is_na_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/any_is_na_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/missing_argument_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{missing_argument_linter} 4 | \alias{missing_argument_linter} 5 | \title{Missing argument linter} 6 | \usage{ 7 | missing_argument_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/missing_argument_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/stopifnot_all_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{stopifnot_all_linter} 4 | \alias{stopifnot_all_linter} 5 | \title{Block usage of all() within stopifnot()} 6 | \usage{ 7 | stopifnot_all_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/stopifnot_all_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/length_levels_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{length_levels_linter} 4 | \alias{length_levels_linter} 5 | \title{Require usage of nlevels over length(levels(.))} 6 | \usage{ 7 | length_levels_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/length_levels_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/nested_ifelse_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{nested_ifelse_linter} 4 | \alias{nested_ifelse_linter} 5 | \title{Block usage of nested \code{ifelse()} calls} 6 | \usage{ 7 | nested_ifelse_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/nested_ifelse_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /inst/rules/builtin/empty_assignment.yml: -------------------------------------------------------------------------------- 1 | id: empty_assignment-1 2 | language: r 3 | severity: warning 4 | rule: 5 | any: 6 | - pattern: $OBJ <- {} 7 | - pattern: $OBJ <- {$CONTENT} 8 | - pattern: $OBJ = {} 9 | - pattern: $OBJ = {$CONTENT} 10 | constraints: 11 | CONTENT: 12 | regex: ^\s+$ 13 | message: | 14 | Assign NULL explicitly or, whenever possible, allocate the empty object with 15 | the right type and size. 16 | -------------------------------------------------------------------------------- /inst/rules/builtin/undesirable_function.yml: -------------------------------------------------------------------------------- 1 | id: undesirable_function-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: $FUN 6 | kind: identifier 7 | not: 8 | inside: 9 | kind: argument 10 | constraints: 11 | FUN: 12 | regex: ^(\.libPaths|attach|browser|debug|debugcall|debugonce|detach|par|setwd|structure|Sys\.setenv|Sys\.setlocale|trace|undebug|untrace)$ 13 | message: Function "~~FUN~~()" is undesirable. 14 | -------------------------------------------------------------------------------- /man/duplicate_argument_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{duplicate_argument_linter} 4 | \alias{duplicate_argument_linter} 5 | \title{Duplicate argument linter} 6 | \usage{ 7 | duplicate_argument_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/duplicate_argument_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/expect_not_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{expect_not_linter} 4 | \alias{expect_not_linter} 5 | \title{Require usage of \code{expect_false(x)} over \code{expect_true(!x)}} 6 | \usage{ 7 | expect_not_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/expect_not_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/expect_null_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{expect_null_linter} 4 | \alias{expect_null_linter} 5 | \title{Require usage of \code{expect_null} for checking \code{NULL}} 6 | \usage{ 7 | expect_null_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/expect_null_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/expect_s3_class_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{expect_s3_class_linter} 4 | \alias{expect_s3_class_linter} 5 | \title{Require usage of \code{expect_s3_class()}} 6 | \usage{ 7 | expect_s3_class_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/expect_s3_class_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/sample_int_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{sample_int_linter} 4 | \alias{sample_int_linter} 5 | \title{Require usage of sample.int(n, m, ...) over sample(1:n, m, ...)} 6 | \usage{ 7 | sample_int_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/sample_int_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /tests/testthat/test-exclude_linters.R: -------------------------------------------------------------------------------- 1 | test_that("lint: argument 'exclude_linters' works", { 2 | expect_no_lint( 3 | "any(duplicated(x))", 4 | linter = NULL, 5 | exclude_linters = "any_duplicated" 6 | ) 7 | }) 8 | 9 | test_that("fix: argument 'exclude_linters' works", { 10 | expect_fix( 11 | "any(duplicated(x))", 12 | character(0), 13 | linter = NULL, 14 | exclude_linters = "any_duplicated" 15 | ) 16 | }) 17 | -------------------------------------------------------------------------------- /R/flir-package.R: -------------------------------------------------------------------------------- 1 | #' @keywords internal 2 | "_PACKAGE" 3 | 4 | ## usethis namespace: start 5 | #' @importFrom data.table .BY 6 | #' @importFrom data.table .EACHI 7 | #' @importFrom data.table .GRP 8 | #' @importFrom data.table .I 9 | #' @importFrom data.table .N 10 | #' @importFrom data.table .NGRP 11 | #' @importFrom data.table .SD 12 | #' @importFrom data.table := 13 | #' @importFrom data.table data.table 14 | ## usethis namespace: end 15 | NULL 16 | -------------------------------------------------------------------------------- /man/length_test_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{length_test_linter} 4 | \alias{length_test_linter} 5 | \title{Check for a common mistake where length is applied in the wrong place} 6 | \usage{ 7 | length_test_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/length_test_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/undesirable_function_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{undesirable_function_linter} 4 | \alias{undesirable_function_linter} 5 | \title{Undesirable function linter} 6 | \usage{ 7 | undesirable_function_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/undesirable_function_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/undesirable_operator_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{undesirable_operator_linter} 4 | \alias{undesirable_operator_linter} 5 | \title{Undesirable operator linter} 6 | \usage{ 7 | undesirable_operator_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/undesirable_operator_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/vector_logic_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{vector_logic_linter} 4 | \alias{vector_logic_linter} 5 | \title{Enforce usage of scalar logical operators in conditional statements} 6 | \usage{ 7 | vector_logic_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/vector_logic_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | # This file is part of the standard setup for testthat. 2 | # It is recommended that you do not modify it. 3 | # 4 | # Where should you do additional test configuration? 5 | # Learn more about the roles of various files in: 6 | # * https://r-pkgs.org/testing-design.html#sec-tests-files-overview 7 | # * https://testthat.r-lib.org/articles/special-files.html 8 | 9 | library(testthat) 10 | library(flir) 11 | 12 | test_check("flir") 13 | -------------------------------------------------------------------------------- /inst/rules/builtin/expect_length.yml: -------------------------------------------------------------------------------- 1 | id: expect_length-1 2 | language: r 3 | severity: warning 4 | rule: 5 | any: 6 | - pattern: $FUN(length($OBJ), $VALUE) 7 | - pattern: $FUN($VALUE, length($OBJ)) 8 | constraints: 9 | FUN: 10 | regex: ^(expect_identical|expect_equal)$ 11 | VALUE: 12 | not: 13 | regex: length\( 14 | fix: expect_length(~~OBJ~~, ~~VALUE~~) 15 | message: expect_length(x, n) is better than ~~FUN~~(length(x), n). 16 | -------------------------------------------------------------------------------- /man/is_numeric_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{is_numeric_linter} 4 | \alias{is_numeric_linter} 5 | \title{Redirect \code{is.numeric(x) || is.integer(x)} to just use \code{is.numeric(x)}} 6 | \usage{ 7 | is_numeric_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/is_numeric_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /tests/testthat/test-setup_flir_gha.R: -------------------------------------------------------------------------------- 1 | test_that("setup_flir_gha basic use works", { 2 | create_local_package() 3 | expect_no_error(suppressMessages(setup_flir_gha())) 4 | expect_true(file.exists(".github/workflows/flir.yaml")) 5 | 6 | # Do not use snapshot here since error message includes path to installed 7 | # "flir" 8 | expect_error(setup_flir_gha(), "file already exists") 9 | expect_no_error(setup_flir_gha(overwrite = TRUE)) 10 | }) 11 | -------------------------------------------------------------------------------- /man/for_loop_index_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{for_loop_index_linter} 4 | \alias{for_loop_index_linter} 5 | \title{Block usage of for loops directly overwriting the indexing variable} 6 | \usage{ 7 | for_loop_index_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/for_loop_index_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/redundant_equals_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{redundant_equals_linter} 4 | \alias{redundant_equals_linter} 5 | \title{Block usage of \code{==}, \code{!=} on logical vectors} 6 | \usage{ 7 | redundant_equals_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/redundant_equals_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/unnecessary_nesting_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{unnecessary_nesting_linter} 4 | \alias{unnecessary_nesting_linter} 5 | \title{Block instances of unnecessary nesting} 6 | \usage{ 7 | unnecessary_nesting_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/unnecessary_nesting_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/expect_named_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{expect_named_linter} 4 | \alias{expect_named_linter} 5 | \title{Require usage of \code{expect_named(x, n)} over \code{expect_equal(names(x), n)}} 6 | \usage{ 7 | expect_named_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/expect_named_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/expect_type_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{expect_type_linter} 4 | \alias{expect_type_linter} 5 | \title{Require usage of \code{expect_type(x, type)} over \code{expect_equal(typeof(x), type)}} 6 | \usage{ 7 | expect_type_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/expect_type_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/any_duplicated_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{any_duplicated_linter} 4 | \alias{any_duplicated_linter} 5 | \title{Require usage of \code{anyDuplicated(x) > 0} over \code{any(duplicated(x))}} 6 | \usage{ 7 | any_duplicated_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/any_duplicated_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/expect_identical_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{expect_identical_linter} 4 | \alias{expect_identical_linter} 5 | \title{Require usage of \code{expect_identical(x, y)} where appropriate} 6 | \usage{ 7 | expect_identical_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/expect_identical_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/expect_length_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{expect_length_linter} 4 | \alias{expect_length_linter} 5 | \title{Require usage of \code{expect_length(x, n)} over \code{expect_equal(length(x), n)}} 6 | \usage{ 7 | expect_length_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/expect_length_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/function_return_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{function_return_linter} 4 | \alias{function_return_linter} 5 | \title{Lint common mistakes/style issues cropping up from return statements} 6 | \usage{ 7 | function_return_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/function_return_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/literal_coercion_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{literal_coercion_linter} 4 | \alias{literal_coercion_linter} 5 | \title{Require usage of correctly-typed literals over literal coercions} 6 | \usage{ 7 | literal_coercion_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/literal_coercion_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/matrix_apply_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{matrix_apply_linter} 4 | \alias{matrix_apply_linter} 5 | \title{Require usage of \code{colSums(x)} or \code{rowSums(x)} over \code{apply(x, ., sum)}} 6 | \usage{ 7 | matrix_apply_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/matrix_apply_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/list_comparison_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{list_comparison_linter} 4 | \alias{list_comparison_linter} 5 | \title{Block usage of comparison operators with known-list() functions like lapply} 6 | \usage{ 7 | list_comparison_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/list_comparison_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/which_grepl.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("which(grepl('^a', x))", linters = linter) 5 | Output 6 | Old code: which(grepl('^a', x)) 7 | New code: grep('^a', x) 8 | 9 | --- 10 | 11 | Code 12 | fix_text("which(grepl('^a', x, ignore.case = TRUE))", linters = linter) 13 | Output 14 | Old code: which(grepl('^a', x, ignore.case = TRUE)) 15 | New code: grep('^a', x, ignore.case = TRUE) 16 | 17 | -------------------------------------------------------------------------------- /flint.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | 22 | ProjectName: flir 23 | -------------------------------------------------------------------------------- /man/expect_s4_class_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{expect_s4_class_linter} 4 | \alias{expect_s4_class_linter} 5 | \title{Require usage of \code{expect_s4_class(x, k)} over \code{expect_true(is(x, k))}} 6 | \usage{ 7 | expect_s4_class_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/expect_s4_class_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/expect_true_false_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{expect_true_false_linter} 4 | \alias{expect_true_false_linter} 5 | \title{Require usage of \code{expect_true(x)} over \code{expect_equal(x, TRUE)}} 6 | \usage{ 7 | expect_true_false_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/expect_true_false_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/outer_negation_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{outer_negation_linter} 4 | \alias{outer_negation_linter} 5 | \title{Require usage of \code{!any(x)} over \code{all(!x)}, \code{!all(x)} over \code{any(!x)}} 6 | \usage{ 7 | outer_negation_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/outer_negation_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/numeric_leading_zero_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{numeric_leading_zero_linter} 4 | \alias{numeric_leading_zero_linter} 5 | \title{Require usage of a leading zero in all fractional numerics} 6 | \usage{ 7 | numeric_leading_zero_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/numeric_leading_zero_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/outer_negation.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("any(!x)\nall(!y)") 5 | Output 6 | Old code: 7 | any(!x) 8 | all(!y) 9 | 10 | New code: 11 | !all(x) 12 | !any(y) 13 | 14 | --- 15 | 16 | Code 17 | fix_text("any(!f(x))\nall(!f(x))") 18 | Output 19 | Old code: 20 | any(!f(x)) 21 | all(!f(x)) 22 | 23 | New code: 24 | !all(f(x)) 25 | !any(f(x)) 26 | 27 | -------------------------------------------------------------------------------- /man/expect_comparison_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{expect_comparison_linter} 4 | \alias{expect_comparison_linter} 5 | \title{Require usage of \code{expect_gt(x, y)} over \code{expect_true(x > y)} (and similar)} 6 | \usage{ 7 | expect_comparison_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/expect_comparison_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/condition_message_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{condition_message_linter} 4 | \alias{condition_message_linter} 5 | \title{Block usage of \code{paste()} and \code{paste0()} with messaging functions using \code{...}} 6 | \usage{ 7 | condition_message_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/condition_message_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /man/redundant_ifelse_linter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/linters_factory.R 3 | \name{redundant_ifelse_linter} 4 | \alias{redundant_ifelse_linter} 5 | \title{Prevent \code{ifelse()} from being used to produce \code{TRUE}/\code{FALSE} or \code{1}/\code{0}} 6 | \usage{ 7 | redundant_ifelse_linter 8 | } 9 | \value{ 10 | The name of the linter 11 | } 12 | \description{ 13 | See \url{https://lintr.r-lib.org/reference/redundant_ifelse_linter}. 14 | } 15 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/fix_several_files.md: -------------------------------------------------------------------------------- 1 | # Ask permission to use fix() on several files if Git is not used 2 | 3 | Code 4 | fix_dir("R") 5 | Condition 6 | Error in `fix()`: 7 | ! It seems that you are not using Git, but `fix()` will be applied on several R files. 8 | ! This will make it difficult to see the changes in code. 9 | i Therefore, this operation is not allowed by default in a non-interactive setting. 10 | i Use `force = TRUE` to bypass this behavior. 11 | 12 | -------------------------------------------------------------------------------- /man/list_linters.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/list-linters.R 3 | \name{list_linters} 4 | \alias{list_linters} 5 | \title{Get the list of linters in \code{flir}} 6 | \usage{ 7 | list_linters(path = ".") 8 | } 9 | \arguments{ 10 | \item{path}{A valid path to a file or a directory. Relative paths are 11 | accepted. Default is \code{"."}.} 12 | } 13 | \value{ 14 | A character vector 15 | } 16 | \description{ 17 | Get the list of linters in \code{flir} 18 | } 19 | \examples{ 20 | list_linters(".") 21 | } 22 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/stopifnot_all.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("stopifnot(all(x > 0 & y < 1))", linters = linter) 5 | Output 6 | Old code: stopifnot(all(x > 0 & y < 1)) 7 | New code: stopifnot(x > 0 & y < 1) 8 | 9 | --- 10 | 11 | Code 12 | fix_text(lines, linters = linter) 13 | Output 14 | Old code: 15 | stopifnot(exprs = { 16 | all(x > 0 & y < 1) 17 | }) 18 | 19 | New code: 20 | stopifnot(exprs = { 21 | x > 0 & y < 1 22 | }) 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/check-link-rot.yml: -------------------------------------------------------------------------------- 1 | name: check-link-rot 2 | 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | check-link-rot: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Link Checker 20 | id: lychee 21 | uses: lycheeverse/lychee-action@v2 22 | with: 23 | fail: true 24 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/expect_identical.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("expect_true(identical(x + 1, y))", linters = linter) 5 | Output 6 | Old code: expect_true(identical(x + 1, y)) 7 | New code: expect_identical(x + 1, y) 8 | 9 | --- 10 | 11 | Code 12 | fix_text("expect_equal(x + 1, y)", linters = linter) 13 | Output 14 | Old code: expect_equal(x + 1, y) 15 | New code: expect_identical(x + 1, y) 16 | 17 | --- 18 | 19 | Code 20 | fix_text("expect_equal(x + 1.1, y)", linters = linter) 21 | 22 | -------------------------------------------------------------------------------- /tests/testthat/test-length_levels.R: -------------------------------------------------------------------------------- 1 | test_that("length_levels_linter skips allowed usages", { 2 | expect_no_lint("length(c(levels(x), 'a'))", length_levels_linter()) 3 | }) 4 | 5 | test_that("length_levels_linter blocks simple disallowed usages", { 6 | expect_lint( 7 | "2:length(levels(x))", 8 | "nlevels(x) is better than length(levels(x)).", 9 | length_levels_linter() 10 | ) 11 | }) 12 | 13 | test_that("fix works", { 14 | expect_fix("2:length(levels(x))", "2:nlevels(x)") 15 | expect_snapshot(fix_text("{\n length(levels(x))\n length(levels(y))\n}")) 16 | }) 17 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: format 2 | on: 3 | push: 4 | branches: [main, master] 5 | pull_request: 6 | branches: [main, master] 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.head_ref }} 9 | cancel-in-progress: true 10 | jobs: 11 | ci: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Install `air` 16 | run: curl -LsSf https://github.com/posit-dev/air/releases/latest/download/air-installer.sh | sh 17 | - name: Check for changes 18 | run: air format . --check 19 | 20 | -------------------------------------------------------------------------------- /inst/rules/builtin/expect_null.yml: -------------------------------------------------------------------------------- 1 | id: expect_null-1 2 | language: r 3 | severity: warning 4 | rule: 5 | any: 6 | - pattern: $FUN(NULL, $VALUES) 7 | - pattern: $FUN($VALUES, NULL) 8 | constraints: 9 | FUN: 10 | regex: ^(expect_identical|expect_equal)$ 11 | fix: expect_null(~~VALUES~~) 12 | message: expect_null(x) is better than ~~FUN~~(x, NULL). 13 | 14 | --- 15 | 16 | id: expect_null-2 17 | language: r 18 | severity: warning 19 | rule: 20 | pattern: expect_true(is.null($VALUES)) 21 | fix: expect_null(~~VALUES~~) 22 | message: expect_null(x) is better than expect_true(is.null(x)). 23 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/numeric_leading_zero.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("0.1 + .22-0.3-.2") 5 | Output 6 | Old code: 0.1 + .22-0.3-.2 7 | New code: 0.1 + 0.22-0.3-0.2 8 | 9 | --- 10 | 11 | Code 12 | fix_text("d <- 6.7 + .8i") 13 | Output 14 | Old code: d <- 6.7 + .8i 15 | New code: d <- 6.7 + 0.8i 16 | 17 | --- 18 | 19 | Code 20 | fix_text(".7i + .2 + .8i") 21 | Output 22 | Old code: .7i + .2 + .8i 23 | New code: 0.7i + 0.2 + 0.8i 24 | 25 | --- 26 | 27 | Code 28 | fix_text("'some text .7'") 29 | 30 | -------------------------------------------------------------------------------- /inst/rules/builtin/expect_not.yml: -------------------------------------------------------------------------------- 1 | id: expect_not-1 2 | language: r 3 | severity: warning 4 | rule: 5 | all: 6 | - pattern: expect_true(!$COND) 7 | - not: 8 | regex: '^expect_true\(!!' 9 | fix: expect_false(~~COND~~) 10 | message: expect_false(x) is better than expect_true(!x), and vice versa. 11 | 12 | --- 13 | 14 | id: expect_not-2 15 | language: r 16 | severity: warning 17 | rule: 18 | all: 19 | - pattern: expect_false(!$COND) 20 | - not: 21 | regex: '^expect_false\(!!' 22 | fix: expect_true(~~COND~~) 23 | message: expect_false(x) is better than expect_true(!x), and vice versa. 24 | -------------------------------------------------------------------------------- /inst/rules/builtin/condition_message.yml: -------------------------------------------------------------------------------- 1 | id: condition_message-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: $FUN($$$ paste0($$$MSG) $$$) 6 | kind: call 7 | not: 8 | any: 9 | - has: 10 | kind: extract_operator 11 | - has: 12 | stopBy: end 13 | kind: argument 14 | has: 15 | field: name 16 | regex: "^collapse|recycle0$" 17 | stopBy: end 18 | constraints: 19 | FUN: 20 | regex: "^(packageStartupMessage|stop|warning)$" 21 | fix: ~~FUN~~(~~MSG~~) 22 | message: | 23 | ~~FUN~~(paste0(...)) can be rewritten as ~~FUN~~(...). 24 | -------------------------------------------------------------------------------- /inst/rules/builtin/double_assignment.yml: -------------------------------------------------------------------------------- 1 | id: right_double_assignment 2 | language: r 3 | severity: hint 4 | rule: 5 | pattern: $RHS ->> $LHS 6 | has: 7 | field: rhs 8 | kind: identifier 9 | message: ->> can have hard-to-predict behavior; prefer assigning to a 10 | specific environment instead (with assign() or <-). 11 | 12 | --- 13 | 14 | id: left_double_assignment 15 | language: r 16 | severity: hint 17 | rule: 18 | pattern: $LHS <<- $RHS 19 | has: 20 | field: lhs 21 | kind: identifier 22 | message: <<- can have hard-to-predict behavior; prefer assigning to a 23 | specific environment instead (with assign() or <-). 24 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/expect_not.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("expect_true(!anyNA(x))", linters = linter) 5 | Output 6 | Old code: expect_true(!anyNA(x)) 7 | New code: expect_false(anyNA(x)) 8 | 9 | --- 10 | 11 | Code 12 | fix_text("expect_false(!anyNA(x))", linters = linter) 13 | Output 14 | Old code: expect_false(!anyNA(x)) 15 | New code: expect_true(anyNA(x)) 16 | 17 | --- 18 | 19 | Code 20 | fix_text("expect_true(!anyNA(x) & TRUE)", linters = linter) 21 | 22 | --- 23 | 24 | Code 25 | fix_text("expect_false(!anyNA(x) & TRUE)", linters = linter) 26 | 27 | -------------------------------------------------------------------------------- /inst/rules/builtin/list_comparison.yml: -------------------------------------------------------------------------------- 1 | id: list_comparison-1 2 | language: r 3 | severity: warning 4 | rule: 5 | any: 6 | - pattern: $FUN($$$) > $$$ 7 | - pattern: $FUN($$$) >= $$$ 8 | - pattern: $FUN($$$) < $$$ 9 | - pattern: $FUN($$$) <= $$$ 10 | - pattern: $FUN($$$) == $$$ 11 | - pattern: $FUN($$$) != $$$ 12 | constraints: 13 | FUN: 14 | regex: ^(lapply|map|Map|\.mapply)$ 15 | message: | 16 | The output of ~~FUN~~(), a list, is being coerced for comparison. 17 | Instead, use a mapper that generates a vector with the correct type directly, 18 | for example vapply(x, FUN, character(1L)) if the output is a string. 19 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/sample_int.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("sample(1:2, 3)", linters = linter) 5 | Output 6 | Old code: sample(1:2, 3) 7 | New code: sample.int(2, 3) 8 | 9 | --- 10 | 11 | Code 12 | fix_text("sample(seq(2), 3)", linters = linter) 13 | Output 14 | Old code: sample(seq(2), 3) 15 | New code: sample.int(2, 3) 16 | 17 | --- 18 | 19 | Code 20 | fix_text("sample(seq_len(2), 3)", linters = linter) 21 | Output 22 | Old code: sample(seq_len(2), 3) 23 | New code: sample.int(2, 3) 24 | 25 | --- 26 | 27 | Code 28 | fix_text("sample(2:3, 3)", linters = linter) 29 | 30 | -------------------------------------------------------------------------------- /inst/rules/builtin/library_call.yml: -------------------------------------------------------------------------------- 1 | id: library_call 2 | language: r 3 | severity: warning 4 | rule: 5 | kind: call 6 | has: 7 | regex: ^library|require$ 8 | kind: identifier 9 | follows: 10 | not: 11 | any: 12 | - kind: call 13 | has: 14 | regex: ^library|require$ 15 | kind: identifier 16 | - kind: comment 17 | not: 18 | inside: 19 | stopBy: end 20 | any: 21 | - kind: function_definition 22 | - kind: call 23 | has: 24 | pattern: suppressPackageStartupMessages 25 | kind: identifier 26 | message: Move all library/require calls to the top of the script. 27 | -------------------------------------------------------------------------------- /inst/rules/builtin/for_loop_index.yml: -------------------------------------------------------------------------------- 1 | id: for_loop_index-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: for ($IDX in $IDX) 6 | message: Don't re-use any sequence symbols as the index symbol in a for loop. 7 | 8 | --- 9 | 10 | id: for_loop_index-2 11 | language: r 12 | severity: warning 13 | rule: 14 | pattern: for ($IDX in $SEQ) 15 | constraints: 16 | SEQ: 17 | kind: call 18 | has: 19 | kind: arguments 20 | has: 21 | kind: argument 22 | stopBy: end 23 | has: 24 | kind: identifier 25 | field: value 26 | pattern: $IDX 27 | message: Don't re-use any sequence symbols as the index symbol in a for loop. 28 | -------------------------------------------------------------------------------- /man/setup_flir_gha.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/github_action.R 3 | \name{setup_flir_gha} 4 | \alias{setup_flir_gha} 5 | \title{Create a Github Actions workflow for \code{flir}} 6 | \usage{ 7 | setup_flir_gha(path, overwrite = FALSE) 8 | } 9 | \arguments{ 10 | \item{path}{Path to package or project root. If \code{NULL} (default), uses \code{"."}.} 11 | 12 | \item{overwrite}{Whether to overwrite \code{.github/workflows/flir.yaml} if it 13 | already exists.} 14 | } 15 | \value{ 16 | Creates \code{.github/workflows/flir.yaml} but doesn't return any value. 17 | } 18 | \description{ 19 | Create a Github Actions workflow for \code{flir} 20 | } 21 | -------------------------------------------------------------------------------- /inst/rules/builtin/any_is_na.yml: -------------------------------------------------------------------------------- 1 | id: any_na-1 2 | language: r 3 | severity: warning 4 | rule: 5 | any: 6 | - pattern: any(is.na($MYVAR)) 7 | - pattern: any(na.rm = $NARM, is.na($MYVAR)) 8 | - pattern: any(is.na($MYVAR), na.rm = $NARM) 9 | fix: anyNA(~~MYVAR~~) 10 | message: anyNA(x) is better than any(is.na(x)). 11 | 12 | --- 13 | 14 | id: any_na-2 15 | language: r 16 | severity: warning 17 | rule: 18 | any: 19 | - pattern: NA %in% $ELEM 20 | - pattern: NA_real_ %in% $ELEM 21 | - pattern: NA_logical_ %in% $ELEM 22 | - pattern: NA_character_ %in% $ELEM 23 | - pattern: NA_complex_ %in% $ELEM 24 | fix: anyNA(~~ELEM~~) 25 | message: anyNA(x) is better than NA %in% x. 26 | -------------------------------------------------------------------------------- /tests/testthat/test-path.R: -------------------------------------------------------------------------------- 1 | test_that("arg exclude_path works", { 2 | create_local_package() 3 | expect_no_error(setup_flir()) 4 | 5 | dir.create("inst") 6 | cat("any(duplicated(x))", file = "inst/foo.R") 7 | cat("any(duplicated(x))", file = "R/foo.R") 8 | withr::with_envvar( 9 | new = c("TESTTHAT" = FALSE, "GITHUB_ACTIONS" = FALSE), 10 | { 11 | expect_equal(nrow(lint(exclude_path = "inst", verbose = FALSE)), 1) 12 | expect_equal( 13 | nrow(lint_dir("R", exclude_path = "inst", verbose = FALSE)), 14 | 1 15 | ) 16 | expect_equal( 17 | nrow(lint_package(exclude_path = "inst", verbose = FALSE)), 18 | 1 19 | ) 20 | } 21 | ) 22 | }) 23 | -------------------------------------------------------------------------------- /inst/rules/builtin/stopifnot_all.yml: -------------------------------------------------------------------------------- 1 | id: stopifnot_all-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: stopifnot(all($$$CODE)) 6 | fix: stopifnot(~~CODE~~) 7 | message: | 8 | Use stopifnot(x) instead of stopifnot(all(x)). stopifnot(x) runs all() 'under 9 | the hood' and provides a better error message in case of failure. 10 | 11 | --- 12 | 13 | id: stopifnot_all-2 14 | language: r 15 | severity: warning 16 | rule: 17 | pattern: stopifnot(exprs = { all($$$CODE) }) 18 | fix: | 19 | stopifnot(exprs = { 20 | ~~CODE~~ 21 | }) 22 | message: | 23 | Use stopifnot(x) instead of stopifnot(all(x)). stopifnot(x) runs all() 'under 24 | the hood' and provides a better error message in case of failure. 25 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://flir.etiennebacher.com/ 2 | template: 3 | bootstrap: 5 4 | bslib: 5 | primary: '#0B7189' 6 | line-height-base: 1.7 7 | 8 | authors: 9 | Etienne Bacher: 10 | href: https://www.etiennebacher.com 11 | 12 | navbar: 13 | structure: 14 | left: 15 | - intro 16 | - articles 17 | - reference 18 | - news 19 | right: 20 | - search 21 | - github 22 | 23 | reference: 24 | - title: Find and fix lints 25 | contents: 26 | - fix 27 | - lint 28 | - list_linters 29 | 30 | - title: Setup `flir` 31 | contents: 32 | - add_new_rule 33 | - export_new_rule 34 | - setup_flir 35 | - setup_flir_gha 36 | 37 | - title: Linters 38 | contents: 39 | - ends_with("linter") 40 | 41 | -------------------------------------------------------------------------------- /inst/rules/builtin/expect_true_false.yml: -------------------------------------------------------------------------------- 1 | id: expect_true_false-1 2 | language: r 3 | severity: warning 4 | rule: 5 | any: 6 | - pattern: $FUN(TRUE, $VALUES) 7 | - pattern: $FUN($VALUES, TRUE) 8 | constraints: 9 | FUN: 10 | regex: ^(expect_identical|expect_equal)$ 11 | fix: expect_true(~~VALUES~~) 12 | message: expect_true(x) is better than ~~FUN~~(x, TRUE). 13 | 14 | --- 15 | 16 | id: expect_true_false-2 17 | language: r 18 | severity: warning 19 | rule: 20 | any: 21 | - pattern: $FUN(FALSE, $VALUES) 22 | - pattern: $FUN($VALUES, FALSE) 23 | constraints: 24 | FUN: 25 | regex: ^(expect_identical|expect_equal)$ 26 | fix: expect_false(~~VALUES~~) 27 | message: expect_false(x) is better than ~~FUN~~(x, FALSE). 28 | 29 | -------------------------------------------------------------------------------- /tests/testthat/test-nested-lints.R: -------------------------------------------------------------------------------- 1 | test_that("nested lints", { 2 | expect_snapshot(fix_text("any(duplicated(any(is.na(x))))")) 3 | expect_snapshot(fix_text("any(duplicated(any(is.na(x <- T))))")) 4 | 5 | # Examples from #60 6 | expect_snapshot( 7 | fix_text( 8 | " 9 | expect_equal( 10 | anyDuplicated(x) > 0, 11 | FALSE 12 | )" 13 | ) 14 | ) 15 | expect_snapshot( 16 | fix_text( 17 | " 18 | test_that( 19 | 'Formalist works', 20 | { 21 | expect_equal( 22 | anyDuplicated(x) > 0, 23 | FALSE 24 | ) 25 | } 26 | )" 27 | ) 28 | ) 29 | expect_snapshot( 30 | fix_text( 31 | "test_that('Formalist works',{expect_equal(anyDuplicated(x) > 0,FALSE)})" 32 | ) 33 | ) 34 | }) 35 | -------------------------------------------------------------------------------- /inst/rules/builtin/outer_negation.yml: -------------------------------------------------------------------------------- 1 | id: outer_negation-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: all(!$VAR) 6 | constraints: 7 | VAR: 8 | not: 9 | regex: '^!' 10 | fix: '!any(~~VAR~~)' 11 | message: | 12 | !any(x) is better than all(!x). The former applies negation only once after 13 | aggregation instead of many times for each element of x. 14 | 15 | --- 16 | 17 | id: outer_negation-2 18 | language: r 19 | severity: warning 20 | rule: 21 | pattern: any(! $VAR) 22 | constraints: 23 | VAR: 24 | not: 25 | regex: '^!' 26 | fix: '!all(~~VAR~~)' 27 | message: | 28 | !all(x) is better than any(!x). The former applies negation only once after 29 | aggregation instead of many times for each element of x. 30 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/expect_comparison.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("expect_true(x > y)", linters = linter) 5 | Output 6 | Old code: expect_true(x > y) 7 | New code: expect_gt(x, y) 8 | 9 | --- 10 | 11 | Code 12 | fix_text("expect_true(x >= y)", linters = linter) 13 | Output 14 | Old code: expect_true(x >= y) 15 | New code: expect_gte(x, y) 16 | 17 | --- 18 | 19 | Code 20 | fix_text("expect_true(x < y)", linters = linter) 21 | Output 22 | Old code: expect_true(x < y) 23 | New code: expect_lt(x, y) 24 | 25 | --- 26 | 27 | Code 28 | fix_text("expect_true(x <= y)", linters = linter) 29 | Output 30 | Old code: expect_true(x <= y) 31 | New code: expect_lte(x, y) 32 | 33 | -------------------------------------------------------------------------------- /tests/testthat/test-todo_comment.R: -------------------------------------------------------------------------------- 1 | test_that("returns the correct linting", { 2 | linter <- todo_comment_linter() 3 | lint_msg <- "Remove TODO comments." 4 | 5 | expect_no_lint('a <- "you#need#to#fixme"', linter) 6 | expect_no_lint("# tadatodo", linter) 7 | expect_no_lint("# something todo", linter) 8 | expect_lint("#todo", lint_msg, linter) 9 | expect_lint("cat(x) ### fixme", lint_msg, linter) 10 | expect_lint( 11 | "x <- \"1.0\n2.0 #FIXME\n3 #TODO 4\"; y <- 2; z <- 3 # todo later", 12 | lint_msg, 13 | linter 14 | ) 15 | expect_lint( 16 | trim_some( 17 | " 18 | function() { 19 | # TODO 20 | function() { 21 | # fixme 22 | } 23 | } 24 | " 25 | ), 26 | lint_msg, 27 | linter 28 | ) 29 | }) 30 | -------------------------------------------------------------------------------- /inst/rules/builtin/nested_ifelse.yml: -------------------------------------------------------------------------------- 1 | id: nested_ifelse-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: $FUN($COND, $TRUE, $FALSE) 6 | constraints: 7 | FALSE: 8 | regex: ^(ifelse|if_else|fifelse) 9 | FUN: 10 | regex: ^(ifelse|if_else|fifelse) 11 | message: | 12 | Don't use nested ~~FUN~~() calls; instead, try (1) data.table::fcase; 13 | (2) dplyr::case_when; or (3) using a lookup table. 14 | 15 | --- 16 | 17 | id: nested_ifelse-2 18 | language: r 19 | severity: warning 20 | rule: 21 | pattern: $FUN($COND, $TRUE, $FALSE) 22 | constraints: 23 | TRUE: 24 | regex: ^(ifelse|if_else|fifelse) 25 | FUN: 26 | regex: ^(ifelse|if_else|fifelse) 27 | message: | 28 | Don't use nested ~~FUN~~() calls; instead, try (1) data.table::fcase; 29 | (2) dplyr::case_when; or (3) using a lookup table. 30 | -------------------------------------------------------------------------------- /inst/rules/builtin/is_numeric.yml: -------------------------------------------------------------------------------- 1 | id: is_numeric-1 2 | language: r 3 | severity: warning 4 | rule: 5 | any: 6 | - pattern: is.numeric($VAR) || is.integer($VAR) 7 | - pattern: is.integer($VAR) || is.numeric($VAR) 8 | message: is.numeric(x) || is.integer(x) can be simplified to is.numeric(x). Use 9 | is.double(x) to test for objects stored as 64-bit floating point. 10 | 11 | --- 12 | 13 | id: is_numeric-2 14 | language: r 15 | severity: warning 16 | rule: 17 | any: 18 | - pattern: 19 | context: class($VAR) %in% c("numeric", "integer") 20 | strictness: ast 21 | - pattern: 22 | context: class($VAR) %in% c("integer", "numeric") 23 | strictness: ast 24 | message: class(x) %in% c("numeric", "integer") can be simplified to is.numeric(x). Use 25 | is.double(x) to test for objects stored as 64-bit floating point. 26 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/sort.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("y[order(y)]", linters = linter) 5 | Output 6 | Old code: y[order(y)] 7 | New code: sort(y, na.last = TRUE) 8 | 9 | --- 10 | 11 | Code 12 | fix_text("y[order(y, decreasing = FALSE)]", linters = linter) 13 | Output 14 | Old code: y[order(y, decreasing = FALSE)] 15 | New code: sort(y, decreasing = FALSE, na.last = TRUE) 16 | 17 | --- 18 | 19 | Code 20 | fix_text("y[order(y, na.last = FALSE)]", linters = linter) 21 | Output 22 | Old code: y[order(y, na.last = FALSE)] 23 | New code: sort(y, na.last = FALSE, na.last = TRUE) 24 | 25 | --- 26 | 27 | Code 28 | fix_text("sort(x + y) == x + y", linters = linter) 29 | 30 | --- 31 | 32 | Code 33 | fix_text("sort(x + y) != x + y", linters = linter) 34 | 35 | -------------------------------------------------------------------------------- /man/export_new_rule.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/new_rule.R 3 | \name{export_new_rule} 4 | \alias{export_new_rule} 5 | \title{Create a custom rule for external use} 6 | \usage{ 7 | export_new_rule(name, path) 8 | } 9 | \arguments{ 10 | \item{name}{Name(s) of the rule. Cannot contain white space.} 11 | 12 | \item{path}{Path to package or project root. If \code{NULL} (default), uses \code{"."}.} 13 | } 14 | \value{ 15 | Create new file(s) but doesn't return anything 16 | } 17 | \description{ 18 | This function creates a YAML file with the placeholder text to define a new 19 | rule. The file is stored in \code{inst/flir/rules} and will be available to users 20 | of your package if they use \code{flir}. 21 | 22 | To create a new rule that you can use in the current project only, use 23 | \code{add_new_rule()} instead. 24 | } 25 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/expect_length.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("expect_equal(length(x), 2L)", linters = linter) 5 | Output 6 | Old code: expect_equal(length(x), 2L) 7 | New code: expect_length(x, 2L) 8 | 9 | --- 10 | 11 | Code 12 | fix_text("expect_identical(length(x), 2L)", linters = linter) 13 | Output 14 | Old code: expect_identical(length(x), 2L) 15 | New code: expect_length(x, 2L) 16 | 17 | --- 18 | 19 | Code 20 | fix_text("expect_equal(2L, length(x))", linters = linter) 21 | Output 22 | Old code: expect_equal(2L, length(x)) 23 | New code: expect_length(x, 2L) 24 | 25 | --- 26 | 27 | Code 28 | fix_text("expect_identical(2L, length(x))", linters = linter) 29 | Output 30 | Old code: expect_identical(2L, length(x)) 31 | New code: expect_length(x, 2L) 32 | 33 | -------------------------------------------------------------------------------- /tests/testthat/test-undesirable_function.R: -------------------------------------------------------------------------------- 1 | test_that("browser() call detected", { 2 | linter <- undesirable_function_linter() 3 | expect_lint("browser()", "Function \"browser()\" is undesirable", linter) 4 | expect_no_lint("#browser()", linter) 5 | }) 6 | 7 | test_that("browser() has no fix", { 8 | expect_snapshot(fix_text("browser()")) 9 | }) 10 | 11 | test_that("debugonce() call detected", { 12 | linter <- undesirable_function_linter() 13 | expect_lint("debugonce()", "Function \"debugonce()\" is undesirable", linter) 14 | expect_no_lint("#debugonce()", linter) 15 | }) 16 | 17 | test_that("those names can be used as argument names, not as values", { 18 | linter <- undesirable_function_linter() 19 | expect_no_lint("foo(browser = TRUE)", linter) 20 | expect_lint( 21 | "foo(x = browser())", 22 | "Function \"browser()\" is undesirable", 23 | linter 24 | ) 25 | }) 26 | -------------------------------------------------------------------------------- /man/add_new_rule.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/new_rule.R 3 | \name{add_new_rule} 4 | \alias{add_new_rule} 5 | \title{Create a custom rule for internal use} 6 | \usage{ 7 | add_new_rule(name, path) 8 | } 9 | \arguments{ 10 | \item{name}{Name(s) of the rule. Cannot contain white space.} 11 | 12 | \item{path}{Path to package or project root. If \code{NULL} (default), uses \code{"."}.} 13 | } 14 | \value{ 15 | Create new file(s) but doesn't return anything 16 | } 17 | \description{ 18 | This function creates a YAML file with the placeholder text to define a new 19 | rule. The file is stored in \code{flir/rules/custom}. You need to create the 20 | \code{flir} folder with \code{setup_flir()} if it doesn't exist. 21 | 22 | If you want to create a rule that users of your package will be able to 23 | access, use \code{export_new_rule()} instead. 24 | } 25 | -------------------------------------------------------------------------------- /tests/testthat/test-undesirable_operator.R: -------------------------------------------------------------------------------- 1 | test_that("linter returns correct linting", { 2 | linter <- undesirable_operator_linter() 3 | msg_assign <- "Avoid undesirable operators `<<-` and `->>`" 4 | 5 | expect_no_lint("cat(\"10$\")", linter) 6 | expect_lint( 7 | "a <<- log(10)", 8 | msg_assign, 9 | linter 10 | ) 11 | expect_lint( 12 | "log(10) ->> a", 13 | msg_assign, 14 | linter 15 | ) 16 | }) 17 | 18 | test_that(":: is ok, ::: is not", { 19 | linter <- undesirable_operator_linter() 20 | expect_lint("a:::b", "Operator `:::` is undesirable", linter) 21 | expect_no_lint("a::b", linter) 22 | }) 23 | 24 | test_that("undesirable_operator_linter vectorizes messages", { 25 | expect_equal( 26 | nrow( 27 | lint_text( 28 | "x <<- c(pkg:::foo, bar = baz)", 29 | linters = undesirable_operator_linter() 30 | ) 31 | ), 32 | 2 33 | ) 34 | }) 35 | -------------------------------------------------------------------------------- /inst/rules/builtin/redundant_equals.yml: -------------------------------------------------------------------------------- 1 | id: redundant_equals-1 2 | language: r 3 | severity: warning 4 | rule: 5 | any: 6 | - pattern: $VAR == TRUE 7 | - pattern: TRUE == $VAR 8 | - pattern: $VAR == FALSE 9 | - pattern: FALSE == $VAR 10 | message: | 11 | Using == on a logical vector is redundant. Well-named logical vectors can be 12 | used directly in filtering. For data.table's `i` argument, wrap the column 13 | name in (), like `DT[(is_treatment)]`. 14 | 15 | --- 16 | 17 | id: redundant_equals-2 18 | language: r 19 | severity: warning 20 | rule: 21 | any: 22 | - pattern: $VAR != TRUE 23 | - pattern: TRUE != $VAR 24 | - pattern: $VAR != FALSE 25 | - pattern: FALSE != $VAR 26 | message: | 27 | Using != on a logical vector is redundant. Well-named logical vectors can be 28 | used directly in filtering. For data.table's `i` argument, wrap the column 29 | name in (), like `DT[(is_treatment)]`. 30 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/export_new_rule.md: -------------------------------------------------------------------------------- 1 | # export_new_rule() only works in packages 2 | 3 | Code 4 | export_new_rule("foobar") 5 | Condition 6 | Error in `export_new_rule()`: 7 | ! `export_new_rule()` only works when the project is an R package. 8 | 9 | # export_new_rule() errors on wrong names 10 | 11 | Code 12 | export_new_rule(1) 13 | Condition 14 | Error in `export_new_rule()`: 15 | ! `name` must be a character vector. 16 | 17 | # export_new_rule() cannot overwrite files 18 | 19 | Code 20 | export_new_rule("foobar") 21 | Condition 22 | Error in `export_new_rule()`: 23 | ! './inst/flir/rules/foobar.yml' already exists. 24 | 25 | # export_new_rule() cannot create file with whitespace 26 | 27 | Code 28 | export_new_rule("hi there") 29 | Condition 30 | Error in `export_new_rule()`: 31 | ! `name` must not contain white space. 32 | 33 | -------------------------------------------------------------------------------- /inst/rules/builtin/expect_comparison.yml: -------------------------------------------------------------------------------- 1 | id: expect_comparison-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: expect_true($X > $Y) 6 | fix: expect_gt(~~X~~, ~~Y~~) 7 | message: expect_gt(x, y) is better than expect_true(x > y). 8 | 9 | --- 10 | 11 | id: expect_comparison-2 12 | language: r 13 | severity: warning 14 | rule: 15 | pattern: expect_true($X >= $Y) 16 | fix: expect_gte(~~X~~, ~~Y~~) 17 | message: expect_gte(x, y) is better than expect_true(x >= y). 18 | 19 | --- 20 | 21 | id: expect_comparison-3 22 | language: r 23 | severity: warning 24 | rule: 25 | pattern: expect_true($X < $Y) 26 | fix: expect_lt(~~X~~, ~~Y~~) 27 | message: expect_lt(x, y) is better than expect_true(x < y). 28 | 29 | --- 30 | 31 | id: expect_comparison-4 32 | language: r 33 | severity: warning 34 | rule: 35 | pattern: expect_true($X <= $Y) 36 | fix: expect_lte(~~X~~, ~~Y~~) 37 | message: expect_lte(x, y) is better than expect_true(x <= y). 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Direnv files 2 | .direnv 3 | .envrc 4 | # History files 5 | .Rhistory 6 | .Rapp.history 7 | # Session Data files 8 | .RData 9 | # User-specific files 10 | .Ruserdata 11 | # Example code in package build process 12 | *-Ex.R 13 | # Output files from R CMD build 14 | /*.tar.gz 15 | # Output files from R CMD check 16 | /*.Rcheck/ 17 | # RStudio files 18 | .Rproj.user/ 19 | # produced vignettes 20 | vignettes/*.html 21 | vignettes/*.pdf 22 | # OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 23 | .httr-oauth 24 | # knitr and R markdown default cache directories 25 | *_cache/ 26 | /cache/ 27 | # Temporary files created by R markdown 28 | *.utf8.md 29 | *.knit.md 30 | # R Environment Variables 31 | .Renviron 32 | .Rproj.user 33 | my.csv 34 | test.csv 35 | .Rprofile 36 | check 37 | inst/doc 38 | .DS_Store 39 | 40 | # Python 41 | .venv 42 | .Rdata 43 | .quarto 44 | ^docs$ 45 | docs 46 | 47 | # Vim 48 | *.swp 49 | 50 | /.quarto/ 51 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/expect_null.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("expect_identical(x, NULL)", linters = linter) 5 | Output 6 | Old code: expect_identical(x, NULL) 7 | New code: expect_null(x) 8 | 9 | --- 10 | 11 | Code 12 | fix_text("expect_equal(x, NULL)", linters = linter) 13 | Output 14 | Old code: expect_equal(x, NULL) 15 | New code: expect_null(x) 16 | 17 | --- 18 | 19 | Code 20 | fix_text("expect_true(is.null(x))", linters = linter) 21 | Output 22 | Old code: expect_true(is.null(x)) 23 | New code: expect_null(x) 24 | 25 | --- 26 | 27 | Code 28 | fix_text("expect_identical(NULL, x)", linters = linter) 29 | Output 30 | Old code: expect_identical(NULL, x) 31 | New code: expect_null(x) 32 | 33 | --- 34 | 35 | Code 36 | fix_text("expect_equal(NULL, x)", linters = linter) 37 | Output 38 | Old code: expect_equal(NULL, x) 39 | New code: expect_null(x) 40 | 41 | -------------------------------------------------------------------------------- /inst/rules/builtin/missing_argument.yml: -------------------------------------------------------------------------------- 1 | id: missing_argument-1 2 | language: r 3 | severity: warning 4 | rule: 5 | kind: arguments 6 | has: 7 | kind: comma 8 | any: 9 | - precedes: 10 | stopBy: neighbor 11 | any: 12 | - regex: '^\)$' 13 | - kind: comma 14 | - follows: 15 | any: 16 | - regex: '^\($' 17 | - kind: argument 18 | regex: '=$' 19 | follows: 20 | kind: identifier 21 | not: 22 | regex: '^(quote|switch|alist)$' 23 | inside: 24 | kind: call 25 | message: Missing argument in function call. 26 | 27 | --- 28 | 29 | id: missing_argument-2 30 | language: r 31 | severity: warning 32 | rule: 33 | kind: arguments 34 | regex: '=(\s+|)\)$' 35 | follows: 36 | any: 37 | - kind: identifier 38 | - kind: extract_operator 39 | - kind: namespace_operator 40 | not: 41 | regex: '^(quote|switch|alist)$' 42 | inside: 43 | kind: call 44 | message: Missing argument in function call. 45 | -------------------------------------------------------------------------------- /inst/rules/builtin/unnecessary_nesting.yml: -------------------------------------------------------------------------------- 1 | id: unnecessary_nesting-1 2 | language: r 3 | severity: warning 4 | rule: 5 | kind: if_statement 6 | any: 7 | - has: 8 | kind: 'braced_expression' 9 | field: consequence 10 | has: 11 | kind: if_statement 12 | stopBy: neighbor 13 | not: 14 | has: 15 | kind: 'braced_expression' 16 | field: alternative 17 | stopBy: end 18 | not: 19 | any: 20 | - has: 21 | nthChild: 2 22 | - precedes: 23 | regex: "^else$" 24 | - has: 25 | kind: if_statement 26 | field: consequence 27 | stopBy: neighbor 28 | # Can be in if(), but not else if() 29 | not: 30 | inside: 31 | field: alternative 32 | kind: if_statement 33 | message: | 34 | Don't use nested `if` statements, where a single `if` with the combined 35 | conditional expression will do. For example, instead of `if (x) { if (y) { ... }}`, 36 | use `if (x && y) { ... }`. 37 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/add_new_rule.md: -------------------------------------------------------------------------------- 1 | # add_new_rule() errors 2 | 3 | Code 4 | add_new_rule("foo bar") 5 | Condition 6 | Error in `add_new_rule()`: 7 | ! `name` must not contain white space. 8 | 9 | --- 10 | 11 | Code 12 | add_new_rule(1) 13 | Condition 14 | Error in `add_new_rule()`: 15 | ! `name` must be a character vector. 16 | 17 | # export_new_rule() cannot overwrite files 18 | 19 | Code 20 | add_new_rule("foobar") 21 | Condition 22 | Error in `add_new_rule()`: 23 | ! './flir/rules/custom/foobar.yml' already exists. 24 | 25 | # create template for new custom rule 26 | 27 | Code 28 | add_new_rule("foobar") 29 | Condition 30 | Error in `add_new_rule()`: 31 | ! Folder `flir` doesn't exist. 32 | i Create it with `setup_flir()` first. 33 | 34 | --- 35 | 36 | Code 37 | fs::dir_tree("flir") 38 | Output 39 | flir 40 | +-- cache_file_state.rds 41 | +-- config.yml 42 | \-- rules 43 | \-- custom 44 | \-- foobar.yml 45 | 46 | -------------------------------------------------------------------------------- /tests/testthat/test-list_linters.R: -------------------------------------------------------------------------------- 1 | test_that("expect_*() rules are only present if the package uses testthat", { 2 | create_local_package() 3 | expect_rules <- grep("^expect\\_", list_linters(), value = TRUE) 4 | expect_length(expect_rules, 0) 5 | 6 | suppressMessages(usethis::use_testthat()) 7 | expect_rules <- grep("^expect\\_", list_linters(), value = TRUE) 8 | expect_true(length(expect_rules) > 0) 9 | }) 10 | 11 | test_that("expect_*() rules are only used if the package uses testthat", { 12 | create_local_package() 13 | fs::dir_create("inst/tinytest") 14 | 15 | cat( 16 | "expect_equal(names(x), c('a', 'b'))\n", 17 | file = "inst/tinytest/test-foo.R" 18 | ) 19 | 20 | # lint() skips expect_* linters for tinytest 21 | expect_true(nrow(lint_package(open = FALSE)) == 0) 22 | expect_true(nrow(lint("inst/tinytest/test-foo.R", open = FALSE)) == 0) 23 | 24 | # fix() leaves that unchanged 25 | fix("inst/tinytest/test-foo.R") 26 | expect_equal( 27 | readLines("inst/tinytest/test-foo.R"), 28 | "expect_equal(names(x), c('a', 'b'))" 29 | ) 30 | }) 31 | -------------------------------------------------------------------------------- /inst/rules/builtin/undesirable_operator.yml: -------------------------------------------------------------------------------- 1 | id: undesirable_operator-1 2 | language: r 3 | severity: warning 4 | rule: 5 | any: 6 | - pattern: $X <<- $Y 7 | - pattern: $X ->> $Y 8 | message: | 9 | Avoid undesirable operators `<<-` and `->>`. They assign outside the current 10 | environment in a way that can be hard to reason about. Prefer fully-encapsulated 11 | functions wherever possible, or, if necessary, assign to a specific environment 12 | with assign(). Recall that you can create an environment at the desired scope 13 | with new.env(). 14 | 15 | --- 16 | 17 | id: undesirable_operator-2 18 | language: r 19 | severity: warning 20 | rule: 21 | kind: namespace_operator 22 | has: 23 | pattern: ':::' 24 | message: | 25 | Operator `:::` is undesirable. It accesses non-exported functions inside 26 | packages. Code relying on these is likely to break in future versions of the 27 | package because the functions are not part of the public interface and may be 28 | changed or removed by the maintainers without notice. Use public functions 29 | via :: instead. 30 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/length_test.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("length(x == 0)", linters = linter) 5 | Output 6 | Old code: length(x == 0) 7 | New code: length(x) == 0 8 | 9 | --- 10 | 11 | Code 12 | fix_text("length(x != 0)", linters = linter) 13 | Output 14 | Old code: length(x != 0) 15 | New code: length(x) != 0 16 | 17 | --- 18 | 19 | Code 20 | fix_text("length(x >= 0)", linters = linter) 21 | Output 22 | Old code: length(x >= 0) 23 | New code: length(x) >= 0 24 | 25 | --- 26 | 27 | Code 28 | fix_text("length(x <= 0)", linters = linter) 29 | Output 30 | Old code: length(x <= 0) 31 | New code: length(x) <= 0 32 | 33 | --- 34 | 35 | Code 36 | fix_text("length(x > 0)", linters = linter) 37 | Output 38 | Old code: length(x > 0) 39 | New code: length(x) > 0 40 | 41 | --- 42 | 43 | Code 44 | fix_text("length(x < 0)", linters = linter) 45 | Output 46 | Old code: length(x < 0) 47 | New code: length(x) < 0 48 | 49 | -------------------------------------------------------------------------------- /inst/rules/builtin/expect_identical.yml: -------------------------------------------------------------------------------- 1 | id: expect_identical-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: expect_true(identical($VAL1, $VAL2)) 6 | fix: expect_identical(~~VAL1~~, ~~VAL2~~) 7 | message: Use expect_identical(x, y) instead of expect_true(identical(x, y)). 8 | 9 | --- 10 | 11 | id: expect_identical-2 12 | language: r 13 | severity: warning 14 | rule: 15 | pattern: expect_equal($VAL1, $VAL2) 16 | fix: expect_identical(~~VAL1~~, ~~VAL2~~) 17 | constraints: 18 | VAL1: 19 | all: 20 | - not: 21 | has: 22 | stopBy: end 23 | kind: float 24 | regex: \. 25 | - not: 26 | regex: ^typeof 27 | - not: 28 | pattern: NULL 29 | VAL2: 30 | all: 31 | - not: 32 | has: 33 | stopBy: end 34 | kind: float 35 | regex: \. 36 | - not: 37 | regex: ^typeof 38 | - not: 39 | pattern: NULL 40 | message: | 41 | Use expect_identical(x, y) by default; resort to expect_equal() only when 42 | needed, e.g. when setting ignore_attr= or tolerance=. 43 | -------------------------------------------------------------------------------- /tests/testthat/test-several_paths.R: -------------------------------------------------------------------------------- 1 | test_that("lint() works with multiple paths", { 2 | create_local_package() 3 | cat("x = 1", file = "R/foo.R") 4 | cat("x = 1", file = "R/foo2.R") 5 | expect_equal(nrow(lint(c("R/foo.R", "R/foo2.R"))), 2) 6 | }) 7 | 8 | test_that("fix() works with multiple paths", { 9 | create_local_package() 10 | cat("x = 1", file = "R/foo.R") 11 | cat("x = 1", file = "R/foo2.R") 12 | fix(c("R/foo.R", "R/foo2.R"), force = TRUE) 13 | expect_true(grepl("x <- 1", readLines("R/foo.R"), fixed = TRUE)) 14 | expect_true(grepl("x <- 1", readLines("R/foo2.R"), fixed = TRUE)) 15 | }) 16 | 17 | test_that("lint_package() works", { 18 | create_local_package() 19 | cat("x = 1", file = "R/foo.R") 20 | cat("x = 1", file = "R/foo2.R") 21 | expect_no_error(lint_package()) 22 | }) 23 | 24 | test_that("fix_package() works", { 25 | create_local_package() 26 | cat("x = 1", file = "R/foo.R") 27 | cat("x = 1", file = "R/foo2.R") 28 | fix_package(force = TRUE) 29 | expect_true(grepl("x <- 1", readLines("R/foo.R"), fixed = TRUE)) 30 | expect_true(grepl("x <- 1", readLines("R/foo2.R"), fixed = TRUE)) 31 | }) 32 | -------------------------------------------------------------------------------- /inst/gha/flir.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | name: flir 13 | 14 | jobs: 15 | flir: 16 | runs-on: macOS-latest 17 | # Only restrict concurrency for non-PR jobs 18 | concurrency: 19 | group: flir-${{ github.event_name != 'pull_request' || github.run_id }} 20 | env: 21 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 22 | permissions: 23 | contents: write 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - uses: r-lib/actions/setup-r@v2 28 | 29 | - name: Install flir 30 | run: install.packages("flir", repos = c("https://etiennebacher.r-universe.dev/", getOption("repos"))) 31 | shell: Rscript {0} 32 | 33 | - name: Run flir 34 | run: flir::lint() 35 | shell: Rscript {0} 36 | env: 37 | FLIR_ERROR_ON_LINT: true 38 | -------------------------------------------------------------------------------- /tests/testthat/test-list_comparison.R: -------------------------------------------------------------------------------- 1 | test_that("list_comparison_linter skips allowed usages", { 2 | expect_no_lint("sapply(x, sum) > 10", list_comparison_linter()) 3 | }) 4 | 5 | local({ 6 | linter <- list_comparison_linter() 7 | lint_msg <- "a list, is being coerced for comparison" 8 | 9 | cases <- expand.grid( 10 | list_mapper = c("lapply", "map", "Map", ".mapply"), 11 | comparator = c("==", "!=", ">=", "<=", ">", "<") 12 | ) 13 | cases$.test_name <- with(cases, paste(list_mapper, comparator)) 14 | patrick::with_parameters_test_that( 15 | "list_comparison_linter blocks simple disallowed usages", 16 | expect_lint( 17 | sprintf("%s(x, sum) %s 10", list_mapper, comparator), 18 | lint_msg, 19 | linter 20 | ), 21 | .cases = cases 22 | ) 23 | }) 24 | 25 | test_that("list_comparison_linter vectorizes", { 26 | expect_equal( 27 | nrow( 28 | lint_text( 29 | "{ 30 | sapply(x, sum) > 10 31 | .mapply(`+`, list(1:10, 1:10), NULL) == 2 32 | lapply(x, sum) < 5 33 | }", 34 | linters = list_comparison_linter() 35 | ) 36 | ), 37 | 2 38 | ) 39 | }) 40 | -------------------------------------------------------------------------------- /inst/rules/builtin/duplicate_argument.yml: -------------------------------------------------------------------------------- 1 | id: duplicate_argument-1 2 | language: r 3 | severity: warning 4 | rule: 5 | # Look for a function argument... 6 | kind: argument 7 | any: 8 | - has: 9 | kind: identifier 10 | field: name 11 | pattern: $OBJ 12 | - has: 13 | kind: string_content 14 | pattern: $OBJ 15 | stopBy: end 16 | 17 | # ... that follows other argument(s) with the same name... 18 | follows: 19 | kind: argument 20 | stopBy: end 21 | has: 22 | stopBy: end 23 | kind: identifier 24 | field: name 25 | pattern: $OBJ 26 | 27 | # ... inside a function call (or a subset environment for data.table)... 28 | inside: 29 | kind: arguments 30 | follows: 31 | any: 32 | - kind: identifier 33 | pattern: $FUN 34 | - kind: string 35 | inside: 36 | any: 37 | - kind: call 38 | - kind: subset 39 | 40 | # ... that is not a function listed below. 41 | constraints: 42 | FUN: 43 | not: 44 | regex: ^(mutate|transmute)$ 45 | 46 | message: Avoid duplicate arguments in function calls. 47 | -------------------------------------------------------------------------------- /inst/rules/builtin/sample_int.yml: -------------------------------------------------------------------------------- 1 | id: sample_int-1 2 | language: r 3 | severity: warning 4 | rule: 5 | any: 6 | - pattern: sample(1:$N, $$$OTHER) 7 | - pattern: sample(1L:$N, $$$OTHER) 8 | fix: sample.int(~~N~~, ~~OTHER~~) 9 | message: sample.int(n, m, ...) is preferable to sample(1:n, m, ...). 10 | 11 | --- 12 | 13 | id: sample_int-2 14 | language: r 15 | severity: warning 16 | rule: 17 | pattern: sample(seq($N), $$$OTHER) 18 | fix: sample.int(~~N~~, ~~OTHER~~) 19 | message: sample.int(n, m, ...) is preferable to sample(seq(n), m, ...). 20 | 21 | --- 22 | 23 | id: sample_int-3 24 | language: r 25 | severity: warning 26 | rule: 27 | pattern: sample(seq_len($N), $$$OTHER) 28 | fix: sample.int(~~N~~, ~~OTHER~~) 29 | message: sample.int(n, m, ...) is preferable to sample(seq_len(n), m, ...). 30 | 31 | --- 32 | 33 | # Strangely this panicks if I rename FIRST to N 34 | id: sample_int-4 35 | language: r 36 | severity: warning 37 | rule: 38 | pattern: sample($FIRST, $$$OTHER) 39 | constraints: 40 | FIRST: 41 | regex: ^\d+(L|)$ 42 | fix: sample.int(~~FIRST~~, ~~OTHER~~) 43 | message: sample.int(n, m, ...) is preferable to sample(n, m, ...). 44 | -------------------------------------------------------------------------------- /inst/rules/builtin/equals_na.yml: -------------------------------------------------------------------------------- 1 | id: equals_na 2 | language: r 3 | severity: warning 4 | rule: 5 | any: 6 | - pattern: $MYVAR == NA 7 | - pattern: $MYVAR == NA_integer_ 8 | - pattern: $MYVAR == NA_real_ 9 | - pattern: $MYVAR == NA_complex_ 10 | - pattern: $MYVAR == NA_character_ 11 | - pattern: NA == $MYVAR 12 | - pattern: NA_integer_ == $MYVAR 13 | - pattern: NA_real_ == $MYVAR 14 | - pattern: NA_complex_ == $MYVAR 15 | - pattern: NA_character_ == $MYVAR 16 | fix: is.na(~~MYVAR~~) 17 | message: Use is.na for comparisons to NA (not == or !=). 18 | 19 | --- 20 | 21 | id: equals_na-2 22 | language: r 23 | severity: warning 24 | rule: 25 | any: 26 | - pattern: $MYVAR != NA 27 | - pattern: $MYVAR != NA_integer_ 28 | - pattern: $MYVAR != NA_real_ 29 | - pattern: $MYVAR != NA_complex_ 30 | - pattern: $MYVAR != NA_character_ 31 | - pattern: NA != $MYVAR 32 | - pattern: NA_integer_ != $MYVAR 33 | - pattern: NA_real_ != $MYVAR 34 | - pattern: NA_complex_ != $MYVAR 35 | - pattern: NA_character_ != $MYVAR 36 | fix: is.na(~~MYVAR~~) 37 | message: Use is.na for comparisons to NA (not == or !=). 38 | -------------------------------------------------------------------------------- /R/print.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | print.flir_lint <- function(x, ...) { 3 | for (i in seq_along(x$text)) { 4 | x$message[i] <- gsub("\\n", "", x$message[i]) 5 | if (grepl("\\n", x$text[i])) { 6 | cat(paste0("Original code:\n", crayon::red(x$text[i]), "\n")) 7 | cat(paste0("Suggestion: ", crayon::green(x$message[i]), "\n")) 8 | cat(crayon::silver(paste0("Rule ID: ", x$id[i]), "\n\n")) 9 | } else { 10 | cat("Original code:", crayon::red(x$text[i]), "\n") 11 | cat("Suggestion:", crayon::green(x$message[i]), "\n") 12 | cat(crayon::silver(paste0("Rule ID: ", x$id[i]), "\n\n")) 13 | } 14 | } 15 | } 16 | 17 | #' @export 18 | print.flir_fix <- function(x, ...) { 19 | new_code_multilines <- grepl("\\n", attr(x, "original")) | grepl("\\n", x) 20 | if (grepl("\\n", attr(x, "original"))) { 21 | cat(paste0("Old code:\n", crayon::red(attr(x, "original")), "\n\n")) 22 | } else { 23 | cat("Old code:", crayon::red(attr(x, "original")), "\n") 24 | } 25 | if (new_code_multilines) { 26 | cat(paste0("New code:\n", crayon::green(x), "\n")) 27 | } else { 28 | cat("New code:", crayon::green(x), "\n") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2025 flir authors 4 | Copyright (c) 2025 lintr authors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /man/flir-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/flir-package.R 3 | \docType{package} 4 | \name{flir-package} 5 | \alias{flir} 6 | \alias{flir-package} 7 | \title{flir: Find and Fix Lints in R Code} 8 | \description{ 9 | Lints are code patterns that are not optimal because they are inefficient, forget corner cases, or are less readable. 'flir' provides a small set of functions to detect those lints and automatically fix them. It builds on 'astgrepr', which itself uses the 'Rust' crate 'ast-grep' to parse and navigate R code. 10 | } 11 | \seealso{ 12 | Useful links: 13 | \itemize{ 14 | \item \url{https://flir.etiennebacher.com} 15 | \item \url{https://github.com/etiennebacher/flir} 16 | \item Report bugs at \url{https://github.com/etiennebacher/flir/issues} 17 | } 18 | 19 | } 20 | \author{ 21 | \strong{Maintainer}: Etienne Bacher \email{etienne.bacher@protonmail.com} [copyright holder] 22 | 23 | Authors: 24 | \itemize{ 25 | \item lintr authors 26 | } 27 | 28 | Other contributors: 29 | \itemize{ 30 | \item Trevor L. Davis (\href{https://orcid.org/0000-0001-6341-4639}{ORCID}) [contributor] 31 | } 32 | 33 | } 34 | \keyword{internal} 35 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/lengths.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("x |> sapply(length)", linters = linter) 5 | Output 6 | Old code: x |> sapply(length) 7 | New code: x |> lengths() 8 | 9 | --- 10 | 11 | Code 12 | fix_text("x %>% sapply(length)", linters = linter) 13 | Output 14 | Old code: x %>% sapply(length) 15 | New code: x %>% lengths() 16 | 17 | --- 18 | 19 | Code 20 | fix_text("x |> map_int(length)", linters = linter) 21 | Output 22 | Old code: x |> map_int(length) 23 | New code: x |> lengths() 24 | 25 | --- 26 | 27 | Code 28 | fix_text("x %>% map_int(length)", linters = linter) 29 | Output 30 | Old code: x %>% map_int(length) 31 | New code: x %>% lengths() 32 | 33 | --- 34 | 35 | Code 36 | fix_text("x |> purrr::map_int(length)", linters = linter) 37 | Output 38 | Old code: x |> purrr::map_int(length) 39 | New code: x |> lengths() 40 | 41 | --- 42 | 43 | Code 44 | fix_text("x %>% purrr::map_int(length)", linters = linter) 45 | Output 46 | Old code: x %>% purrr::map_int(length) 47 | New code: x %>% lengths() 48 | 49 | -------------------------------------------------------------------------------- /inst/rules/builtin/class_equals.yml: -------------------------------------------------------------------------------- 1 | id: class_equals-1 2 | language: r 3 | severity: warning 4 | rule: 5 | any: 6 | - pattern: class($VAR) == $CLASSNAME 7 | - pattern: $CLASSNAME == class($VAR) 8 | not: 9 | inside: 10 | kind: argument 11 | fix: inherits(~~VAR~~, ~~CLASSNAME~~) 12 | message: Instead of comparing class(x) with ==, use inherits(x, 'class-name') or is. or is(x, 'class') 13 | 14 | --- 15 | 16 | id: class_equals-2 17 | language: r 18 | severity: warning 19 | rule: 20 | any: 21 | - pattern: class($VAR) != $CLASSNAME 22 | - pattern: $CLASSNAME != class($VAR) 23 | not: 24 | inside: 25 | kind: argument 26 | fix: "!inherits(~~VAR~~, ~~CLASSNAME~~)" 27 | message: "Instead of comparing class(x) with !=, use !inherits(x, 'class-name') or is. or is(x, 'class')" 28 | 29 | --- 30 | 31 | id: class_equals-3 32 | language: r 33 | severity: warning 34 | rule: 35 | any: 36 | - pattern: $CLASSNAME %in% class($VAR) 37 | - pattern: class($VAR) %in% $CLASSNAME 38 | constraints: 39 | CLASSNAME: 40 | kind: string 41 | fix: inherits(~~VAR~~, ~~CLASSNAME~~) 42 | message: Instead of comparing class(x) with %in%, use inherits(x, 'class-name') or is. or is(x, 'class') 43 | -------------------------------------------------------------------------------- /tests/testthat/test-setup_flir.R: -------------------------------------------------------------------------------- 1 | test_that("setup_flir works for packages", { 2 | create_local_package() 3 | expect_no_error(setup_flir()) 4 | expect_snapshot(fs::dir_tree("flir")) 5 | 6 | # lint 7 | cat("any(duplicated(x))", file = "R/foo.R") 8 | withr::with_envvar( 9 | new = c("TESTTHAT" = FALSE, "GITHUB_ACTIONS" = FALSE), 10 | expect_equal(nrow(lint(verbose = FALSE)), 1) 11 | ) 12 | 13 | # fix 14 | fix(verbose = FALSE) 15 | expect_equal( 16 | readLines("R/foo.R", warn = FALSE), 17 | "anyDuplicated(x) > 0" 18 | ) 19 | }) 20 | 21 | test_that("setup_flir works for projects", { 22 | create_local_project() 23 | expect_no_error(setup_flir()) 24 | expect_snapshot(fs::dir_tree("flir")) 25 | 26 | # lint 27 | cat("any(duplicated(x))", file = "R/foo.R") 28 | withr::with_envvar( 29 | new = c("TESTTHAT" = FALSE, "GITHUB_ACTIONS" = FALSE), 30 | expect_equal(nrow(lint(verbose = FALSE)), 1) 31 | ) 32 | 33 | # fix 34 | fix(verbose = FALSE) 35 | expect_equal( 36 | readLines("R/foo.R", warn = FALSE), 37 | "anyDuplicated(x) > 0" 38 | ) 39 | }) 40 | 41 | test_that("flir can work without setup", { 42 | create_local_package() 43 | expect_no_error(lint()) 44 | expect_no_error(fix()) 45 | }) 46 | -------------------------------------------------------------------------------- /inst/config.yml: -------------------------------------------------------------------------------- 1 | keep: 2 | - any_duplicated 3 | - any_is_na 4 | - class_equals 5 | - condition_message 6 | - double_assignment 7 | - duplicate_argument 8 | - empty_assignment 9 | - equal_assignment 10 | - equals_na 11 | - expect_comparison 12 | - expect_identical 13 | - expect_length 14 | - expect_named 15 | - expect_not 16 | - expect_null 17 | - expect_s3_class 18 | - expect_s4_class 19 | - expect_true_false 20 | - expect_type 21 | - for_loop_index 22 | - function_return 23 | - implicit_assignment 24 | - is_numeric 25 | - length_levels 26 | - length_test 27 | - lengths 28 | - library_call 29 | - list_comparison 30 | - literal_coercion 31 | - matrix_apply 32 | - missing_argument 33 | - nested_ifelse 34 | - numeric_leading_zero 35 | - nzchar 36 | - outer_negation 37 | - package_hooks 38 | - paste 39 | - redundant_equals 40 | - redundant_ifelse 41 | - rep_len 42 | - right_assignment 43 | - sample_int 44 | - seq 45 | - sort 46 | - stopifnot_all 47 | - T_and_F_symbol 48 | - todo_comment 49 | - undesirable_function 50 | - undesirable_operator 51 | - unnecessary_nesting 52 | # This can be activated, but it is slow. 53 | # - unreachable_code 54 | - vector_logic 55 | - which_grepl 56 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/print.md: -------------------------------------------------------------------------------- 1 | # print for lint works fine with single-line code 2 | 3 | Code 4 | lint_text("suppressPackageStartupMessages(library(dplyr))", linters = temp_rule) 5 | Output 6 | Original code: suppressPackageStartupMessages(library(dplyr)) 7 | Suggestion: foo 8 | Rule ID: rule_1 9 | 10 | 11 | # print for lint works fine with multi-line code 12 | 13 | Code 14 | lint_text( 15 | "suppressPackageStartupMessages({\n library(dplyr)\n library(knitr)\n})", 16 | linters = temp_rule) 17 | Output 18 | Original code: 19 | suppressPackageStartupMessages({ 20 | library(dplyr) 21 | library(knitr) 22 | }) 23 | Suggestion: foo 24 | Rule ID: rule_1 25 | 26 | 27 | --- 28 | 29 | Code 30 | fix_text("unique(length(x))", linters = temp_rule) 31 | Output 32 | Old code: unique(length(x)) 33 | New code: 34 | length( 35 | unique(x) 36 | ) 37 | 38 | --- 39 | 40 | Code 41 | fix_text("unique(\n length(x)\n)", linters = temp_rule) 42 | Output 43 | Old code: 44 | unique( 45 | length(x) 46 | ) 47 | 48 | New code: 49 | length( 50 | unique(x) 51 | ) 52 | 53 | -------------------------------------------------------------------------------- /tests/testthat/test-which_grepl.R: -------------------------------------------------------------------------------- 1 | test_that("which_grepl_linter skips allowed usages", { 2 | # this _could_ be combined as p1|p2, but often it's cleaner to read this way 3 | expect_no_lint("which(grepl(p1, x) | grepl(p2, x))", which_grepl_linter()) 4 | }) 5 | 6 | test_that("which_grepl_linter blocks simple disallowed usages", { 7 | linter <- which_grepl_linter() 8 | lint_msg <- "grep(pattern, x) is better than which(grepl(pattern, x))." 9 | 10 | expect_lint("which(grepl('^a', x))", lint_msg, linter) 11 | # options also don't matter (grep has more arguments: value, invert) 12 | expect_lint( 13 | "which(grepl('^a', x, perl = TRUE, fixed = TRUE))", 14 | lint_msg, 15 | linter 16 | ) 17 | 18 | expect_equal( 19 | nrow( 20 | lint_text( 21 | trim_some( 22 | '{ 23 | which(x) 24 | grepl(y) 25 | which(grepl("pt1", x)) 26 | which(grepl("pt2", y)) 27 | }' 28 | ), 29 | linters = linter 30 | ) 31 | ), 32 | 2 33 | ) 34 | }) 35 | 36 | test_that("fix works", { 37 | linter <- which_grepl_linter() 38 | expect_snapshot(fix_text("which(grepl('^a', x))", linters = linter)) 39 | expect_snapshot( 40 | fix_text("which(grepl('^a', x, ignore.case = TRUE))", linters = linter) 41 | ) 42 | }) 43 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/expect_true_false.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("expect_equal(foo(x), TRUE)", linters = linter) 5 | Output 6 | Old code: expect_equal(foo(x), TRUE) 7 | New code: expect_true(foo(x)) 8 | 9 | --- 10 | 11 | Code 12 | fix_text("expect_equal(foo(x), FALSE)", linters = linter) 13 | Output 14 | Old code: expect_equal(foo(x), FALSE) 15 | New code: expect_false(foo(x)) 16 | 17 | --- 18 | 19 | Code 20 | fix_text("expect_identical(foo(x), TRUE)", linters = linter) 21 | Output 22 | Old code: expect_identical(foo(x), TRUE) 23 | New code: expect_true(foo(x)) 24 | 25 | --- 26 | 27 | Code 28 | fix_text("expect_identical(foo(x), FALSE)", linters = linter) 29 | Output 30 | Old code: expect_identical(foo(x), FALSE) 31 | New code: expect_false(foo(x)) 32 | 33 | --- 34 | 35 | Code 36 | fix_text("expect_equal(TRUE, foo(x))", linters = linter) 37 | Output 38 | Old code: expect_equal(TRUE, foo(x)) 39 | New code: expect_true(foo(x)) 40 | 41 | --- 42 | 43 | Code 44 | fix_text("expect_equal(FALSE, foo(x))", linters = linter) 45 | Output 46 | Old code: expect_equal(FALSE, foo(x)) 47 | New code: expect_false(foo(x)) 48 | 49 | -------------------------------------------------------------------------------- /tests/testthat/test-for_loop_index.R: -------------------------------------------------------------------------------- 1 | test_that("for_loop_index_linter skips allowed usages", { 2 | linter <- for_loop_index_linter() 3 | 4 | expect_no_lint("for (xi in x) {}", linter) 5 | 6 | # this is OK, so not every symbol is problematic 7 | expect_no_lint("for (col in DF$col) {}", linter) 8 | expect_no_lint("for (col in S4@col) {}", linter) 9 | expect_no_lint("for (col in DT[, col]) {}", linter) 10 | 11 | # make sure symbol check is scoped 12 | expect_no_lint( 13 | trim_some( 14 | " 15 | { 16 | for (i in 1:10) { 17 | 42L 18 | } 19 | i <- 7L 20 | } 21 | " 22 | ), 23 | linter 24 | ) 25 | }) 26 | 27 | test_that("for_loop_index_linter blocks simple disallowed usages", { 28 | linter <- for_loop_index_linter() 29 | lint_msg <- "Don't re-use any sequence symbols as the index symbol in a for loop" 30 | 31 | expect_lint("for (x in x) {}", lint_msg, linter) 32 | # these also overwrite a variable in calling scope 33 | expect_lint("for (x in foo(x)) {}", lint_msg, linter) 34 | expect_no_lint("for (x in foo(x = 1)) {}", linter) 35 | # arbitrary nesting 36 | expect_lint("for (x in foo(bar(y, baz(2, x)))) {}", lint_msg, linter) 37 | expect_no_lint("for (x in foo(bar(y, baz(2, x = z)))) {}", linter) 38 | }) 39 | -------------------------------------------------------------------------------- /R/rstudioapi.R: -------------------------------------------------------------------------------- 1 | # Taken from lintr: R/lint.R 2 | # MIT License 3 | 4 | rstudio_source_markers <- function(lints) { 5 | if (nrow(lints) == 0) { 6 | return(invisible()) 7 | } 8 | if (any(startsWith(lints$file, "./"))) { 9 | lints$file <- normalizePath(lints$file) 10 | } 11 | 12 | lints$severity[lints$severity == "hint"] <- "usage" 13 | lints$severity[lints$severity == "info"] <- "info" 14 | 15 | # generate the markers 16 | markers <- lints[, 17 | c("severity", "file", "line_start", "col_start", "message") 18 | ] 19 | names(markers) <- c("type", "file", "line", "column", "message") 20 | markers <- split(markers, seq_len(nrow(markers))) 21 | markers <- lapply(markers, as.list) 22 | markers <- unname(markers) 23 | 24 | # request source markers 25 | out <- rstudioapi::callFun( 26 | "sourceMarkers", 27 | name = "flir", 28 | markers = markers, 29 | basePath = getwd(), 30 | autoSelect = "first" 31 | ) 32 | 33 | # workaround to avoid focusing an empty Markers pane 34 | # when possible, better solution is to delete the "lintr" source marker list 35 | # https://github.com/rstudio/rstudioapi/issues/209 36 | if (length(lints) == 0L) { 37 | Sys.sleep(0.1) 38 | rstudioapi::executeCommand("activateConsole") 39 | } 40 | 41 | out 42 | } 43 | -------------------------------------------------------------------------------- /tests/testthat/test-stopifnot_all.R: -------------------------------------------------------------------------------- 1 | test_that("stopifnot_all_linter skips allowed usages", { 2 | expect_no_lint("stopifnot(all(x) || any(y))", stopifnot_all_linter()) 3 | }) 4 | 5 | # TODO 6 | # test_that("stopifnot_all_linter blocks simple disallowed usages", { 7 | # linter <- stopifnot_all_linter() 8 | # lint_msg <- "Use stopifnot(x) instead of" 9 | 10 | # expect_lint("stopifnot(all(A))", lint_msg, linter) 11 | # expect_lint("stopifnot(x, y, all(z))", lint_msg, linter) 12 | 13 | # expect_lint( 14 | # trim_some("{ 15 | # stopifnot(all(x), all(y), 16 | # all(z) 17 | # ) 18 | # stopifnot(a > 0, b < 0, all(c == 0)) 19 | # }"), 20 | # list( 21 | # list(lint_msg, line_number = 2L, column_number = 13L), 22 | # list(lint_msg, line_number = 2L, column_number = 21L), 23 | # list(lint_msg, line_number = 3L, column_number = 5L), 24 | # list(lint_msg, line_number = 5L, column_number = 27L) 25 | # ), 26 | # linter 27 | # ) 28 | # }) 29 | 30 | test_that("fix works", { 31 | linter <- stopifnot_all_linter() 32 | expect_snapshot( 33 | fix_text("stopifnot(all(x > 0 & y < 1))", linters = linter) 34 | ) 35 | 36 | lines <- " 37 | stopifnot(exprs = { 38 | all(x > 0 & y < 1) 39 | })" 40 | expect_snapshot(fix_text(lines, linters = linter)) 41 | }) 42 | -------------------------------------------------------------------------------- /tests/testthat/test-redundant_equals.R: -------------------------------------------------------------------------------- 1 | test_that("redundant_equals_linter skips allowed usages", { 2 | # comparisons to non-logical constants 3 | expect_no_lint("x == 1", redundant_equals_linter()) 4 | # comparison to TRUE as a string 5 | expect_no_lint("x != 'TRUE'", redundant_equals_linter()) 6 | }) 7 | 8 | test_that("multiple lints return correct custom messages", { 9 | expect_equal( 10 | nrow( 11 | lint_text( 12 | " 13 | list( 14 | x == TRUE, 15 | y != TRUE 16 | ) 17 | ", 18 | linters = redundant_equals_linter() 19 | ) 20 | ), 21 | 2 22 | ) 23 | }) 24 | 25 | test_that("Order doesn't matter", { 26 | expect_lint( 27 | "TRUE == x", 28 | "Using == on a logical vector is redundant.", 29 | redundant_equals_linter() 30 | ) 31 | }) 32 | 33 | patrick::with_parameters_test_that( 34 | "redundant_equals_linter blocks simple disallowed usages", 35 | { 36 | lint_msg <- paste("Using", op, "on a logical vector is redundant.") 37 | code <- paste("x", op, bool) 38 | expect_lint(code, lint_msg, redundant_equals_linter()) 39 | }, 40 | .cases = tibble::tribble( 41 | ~.test_name , ~op , ~bool , 42 | "==, TRUE" , "==" , "TRUE" , 43 | "==, FALSE" , "==" , "FALSE" , 44 | "!=, TRUE" , "!=" , "TRUE" , 45 | "!=, FALSE" , "!=" , "FALSE" 46 | ) 47 | ) 48 | -------------------------------------------------------------------------------- /tests/testthat/test-add_new_rule.R: -------------------------------------------------------------------------------- 1 | test_that("add_new_rule() errors", { 2 | expect_snapshot( 3 | add_new_rule("foo bar"), 4 | error = TRUE 5 | ) 6 | 7 | expect_snapshot( 8 | add_new_rule(1), 9 | error = TRUE 10 | ) 11 | }) 12 | 13 | test_that("export_new_rule() cannot overwrite files", { 14 | create_local_package() 15 | setup_flir() 16 | add_new_rule("foobar") 17 | expect_snapshot( 18 | add_new_rule("foobar"), 19 | error = TRUE 20 | ) 21 | }) 22 | 23 | test_that("create template for new custom rule", { 24 | create_local_project() 25 | expect_snapshot( 26 | add_new_rule("foobar"), 27 | error = TRUE 28 | ) 29 | 30 | setup_flir() 31 | expect_message( 32 | add_new_rule("foobar"), 33 | r"(Add "foobar" to 'flir/config.yml')" 34 | ) 35 | 36 | expect_snapshot(fs::dir_tree("flir")) 37 | }) 38 | 39 | test_that("add_new_rule() can create several rules at once", { 40 | create_local_project() 41 | setup_flir() 42 | add_new_rule(c("foobar", "foobar2")) 43 | expect_true(fs::file_exists("flir/rules/custom/foobar.yml")) 44 | expect_true(fs::file_exists("flir/rules/custom/foobar2.yml")) 45 | expect_true(any(grepl( 46 | "id: foobar$", 47 | readLines("flir/rules/custom/foobar.yml") 48 | ))) 49 | expect_true(any(grepl( 50 | "id: foobar2$", 51 | readLines("flir/rules/custom/foobar2.yml") 52 | ))) 53 | }) 54 | -------------------------------------------------------------------------------- /tests/testthat/test-any_is_na.R: -------------------------------------------------------------------------------- 1 | lint_message <- "anyNA(x) is better than any(is.na(x))." 2 | 3 | test_that("any_is_na_linter skips allowed usages", { 4 | linter <- any_is_na_linter() 5 | 6 | expect_no_lint("x <- any(y)", linter) 7 | expect_no_lint("y <- is.na(z)", linter) 8 | 9 | # negation shouldn't list 10 | expect_no_lint("any(!is.na(x))", linter) 11 | expect_no_lint("any(!is.na(foo(x)))", linter) 12 | 13 | # extended usage of ... arguments to any is not covered 14 | expect_no_lint("any(is.na(y), b)", linter) 15 | expect_no_lint("any(b, is.na(y))", linter) 16 | }) 17 | 18 | test_that("any_is_na_linter blocks disallowed usages", { 19 | linter <- any_is_na_linter() 20 | 21 | expect_lint("any(is.na(x))", lint_message, linter) 22 | expect_lint("any(is.na(foo(x)))", lint_message, linter) 23 | 24 | # na.rm doesn't really matter for this since is.na can't return NA 25 | expect_lint("any(is.na(x), na.rm = TRUE)", lint_message, linter) 26 | 27 | # also catch nested usage 28 | expect_lint("foo(any(is.na(x)))", lint_message, linter) 29 | }) 30 | 31 | test_that("NA %in% x is also found", { 32 | linter <- any_is_na_linter() 33 | lint_message <- "anyNA(x) is better than NA %in% x." 34 | 35 | expect_lint("NA %in% x", lint_message, linter) 36 | expect_lint("NA_real_ %in% x", lint_message, linter) 37 | expect_no_lint("NA_not_a_sentinel_ %in% x", linter) 38 | }) 39 | -------------------------------------------------------------------------------- /tests/testthat/test-print.R: -------------------------------------------------------------------------------- 1 | test_that("print for lint works fine with single-line code", { 2 | temp_rule <- tempfile(fileext = ".yml") 3 | foo <- "rule: 4 | kind: call 5 | has: 6 | regex: ^suppressPackageStartupMessages 7 | message: foo" 8 | cat(foo, file = temp_rule) 9 | expect_snapshot( 10 | lint_text( 11 | "suppressPackageStartupMessages(library(dplyr))", 12 | linters = temp_rule 13 | ) 14 | ) 15 | }) 16 | 17 | test_that("print for lint works fine with multi-line code", { 18 | temp_rule <- tempfile(fileext = ".yml") 19 | foo <- "rule: 20 | kind: call 21 | has: 22 | regex: ^suppressPackageStartupMessages 23 | message: foo" 24 | cat(foo, file = temp_rule) 25 | expect_snapshot( 26 | lint_text( 27 | "suppressPackageStartupMessages({ 28 | library(dplyr) 29 | library(knitr) 30 | })", 31 | linters = temp_rule 32 | ) 33 | ) 34 | }) 35 | 36 | test_that("print for lint works fine with multi-line code", { 37 | temp_rule <- tempfile(fileext = ".yml") 38 | foo <- "rule: 39 | pattern: unique(length($VAR)) 40 | fix: | 41 | length( 42 | unique(~~VAR~~) 43 | )" 44 | cat(foo, file = temp_rule) 45 | expect_snapshot( 46 | fix_text("unique(length(x))", linters = temp_rule) 47 | ) 48 | expect_snapshot( 49 | fix_text( 50 | "unique( 51 | length(x) 52 | )", 53 | linters = temp_rule 54 | ) 55 | ) 56 | }) 57 | -------------------------------------------------------------------------------- /tests/testthat/test-numeric_leading_zero.R: -------------------------------------------------------------------------------- 1 | test_that("numeric_leading_zero_linter skips allowed usages", { 2 | linter <- numeric_leading_zero_linter() 3 | 4 | expect_no_lint("a <- 0.1", linter) 5 | expect_no_lint("b <- -0.2", linter) 6 | expect_no_lint("c <- 3.0", linter) 7 | expect_no_lint("d <- 4L", linter) 8 | expect_no_lint("e <- TRUE", linter) 9 | expect_no_lint("f <- 0.5e6", linter) 10 | expect_no_lint("g <- 0x78", linter) 11 | expect_no_lint("h <- 0.9 + 0.1i", linter) 12 | expect_no_lint("h <- 0.9+0.1i", linter) 13 | expect_no_lint("h <- 0.9 - 0.1i", linter) 14 | expect_no_lint("i <- 2L + 3.4i", linter) 15 | }) 16 | 17 | test_that("numeric_leading_zero_linter blocks simple disallowed usages", { 18 | linter <- numeric_leading_zero_linter() 19 | lint_msg <- "Include the leading zero for fractional numeric constants." 20 | 21 | expect_lint("a <- .1", lint_msg, linter) 22 | expect_lint("b <- -.2", lint_msg, linter) 23 | expect_lint("c <- .3 + 4.5i", lint_msg, linter) 24 | expect_lint("d <- 6.7 + .8i", lint_msg, linter) 25 | expect_lint("d <- 6.7+.8i", lint_msg, linter) 26 | expect_lint("e <- .9e10", lint_msg, linter) 27 | }) 28 | 29 | test_that("fix works", { 30 | expect_snapshot(fix_text("0.1 + .22-0.3-.2")) 31 | expect_snapshot(fix_text("d <- 6.7 + .8i")) 32 | expect_snapshot(fix_text(".7i + .2 + .8i")) 33 | expect_snapshot(fix_text("'some text .7'")) 34 | }) 35 | -------------------------------------------------------------------------------- /pkgdown/extra.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Theme mostly taken from Matthew Kay's "ggblend" package 3 | https://mjskay.github.io/ggblend/ 4 | on 2024-03-16 5 | */ 6 | 7 | .navbar { 8 | border-bottom: solid 0.2rem #0B7189; 9 | background-color: #f2f2f2 !important; 10 | } 11 | 12 | .page-header { 13 | min-height: 1rem !important; 14 | } 15 | 16 | @media (min-width: 1400px) { 17 | body { 18 | font-size: calc(inherit + 0.2rem); 19 | } 20 | } 21 | 22 | h6,.h6,h5,.h5,h4,.h4,h3,.h3,h2,.h2,h1,.h1 { 23 | margin-top: 2rem; 24 | margin-bottom: 1rem; 25 | font-weight: 600; 26 | color: #444444; 27 | } 28 | 29 | pre { 30 | padding: 0.9rem 1rem 1rem; 31 | background-color: #f6f6f6; 32 | border: none; 33 | border-radius: 0.5em; 34 | } 35 | 36 | .navbar { 37 | background-color: white !important; 38 | } 39 | 40 | .navbar-brand { 41 | font-size: var(--bs-nav-link-font-size); 42 | color: #6c757d; 43 | } 44 | 45 | h1, .h1 { 46 | font-size: calc(1.375rem + 1vw); 47 | font-weight: 700; 48 | } 49 | 50 | h2, .h2 { 51 | font-size: calc(1.325rem + 0.5vw); 52 | } 53 | 54 | h3, .h3 { 55 | font-size: calc(1.25rem + 0.5vw); 56 | } 57 | 58 | @media (min-width: 1200px) { 59 | h1, .h1 { 60 | font-size: 2rem; 61 | } 62 | 63 | h2, .h2 { 64 | font-size: 1.75rem; 65 | } 66 | 67 | h3, .h3 { 68 | font-size: 1.3rem; 69 | } 70 | } 71 | 72 | aside h2, aside .h2 { 73 | margin-bottom: 0.5rem; 74 | } 75 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/message.md: -------------------------------------------------------------------------------- 1 | # no lints found message works 2 | 3 | Code 4 | invisible(lint(dest)) 5 | Message 6 | i Going to check 1 file. 7 | v No lints detected. 8 | 9 | # found lint message works when no lint can be fixed 10 | 11 | Code 12 | invisible(lint(temp_dir)) 13 | Message 14 | i Going to check 1 file. 15 | v Found 2 lints in 1 file. 16 | i None of them can be fixed automatically. 17 | 18 | # found lint message works when lint can be fixed 19 | 20 | Code 21 | invisible(lint(temp_dir)) 22 | Message 23 | i Going to check 1 file. 24 | v Found 2 lints in 1 file. 25 | i 2 of them can be fixed automatically. 26 | 27 | --- 28 | 29 | Code 30 | invisible(lint(temp_dir)) 31 | Message 32 | i Going to check 2 files. 33 | v Found 2 lints in 2 files. 34 | i 2 of them can be fixed automatically. 35 | 36 | # no fixes needed message works 37 | 38 | Code 39 | fix(dest) 40 | Message 41 | i Going to check 1 file. 42 | v No fixes needed. 43 | 44 | # fix needed message works 45 | 46 | Code 47 | fix(temp_dir) 48 | Message 49 | i Going to check 1 file. 50 | v Fixed 2 lints in 1 file. 51 | 52 | --- 53 | 54 | Code 55 | fix(temp_dir, force = TRUE) 56 | Message 57 | i Going to check 2 files. 58 | v Fixed 2 lints in 2 files. 59 | 60 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/expect_named.md: -------------------------------------------------------------------------------- 1 | # fix works for expect_named 2 | 3 | Code 4 | fix_text("expect_identical(names(x), c('a', 'b'))", linters = linter) 5 | Output 6 | Old code: expect_identical(names(x), c('a', 'b')) 7 | New code: expect_named(x, c('a', 'b')) 8 | 9 | --- 10 | 11 | Code 12 | fix_text("expect_identical('a', names(x))", linters = linter) 13 | Output 14 | Old code: expect_identical('a', names(x)) 15 | New code: expect_named(x, 'a') 16 | 17 | --- 18 | 19 | Code 20 | fix_text("expect_equal('a', names(x))", linters = linter) 21 | Output 22 | Old code: expect_equal('a', names(x)) 23 | New code: expect_named(x, 'a') 24 | 25 | --- 26 | 27 | Code 28 | fix_text("expect_equal(names(x), c('a', 'b'))", linters = linter) 29 | Output 30 | Old code: expect_equal(names(x), c('a', 'b')) 31 | New code: expect_named(x, c('a', 'b')) 32 | 33 | --- 34 | 35 | Code 36 | fix_text("expect_identical(names(x), c(\"a\", \"b\"))", linters = linter) 37 | Output 38 | Old code: expect_identical(names(x), c("a", "b")) 39 | New code: expect_named(x, c("a", "b")) 40 | 41 | --- 42 | 43 | Code 44 | fix_text("testthat::expect_equal('a', names(x))", linters = linter) 45 | Output 46 | Old code: testthat::expect_equal('a', names(x)) 47 | New code: testthat::expect_named(x, 'a') 48 | 49 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/class_equals.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("class(x) == 'character'") 5 | Output 6 | Old code: class(x) == 'character' 7 | New code: inherits(x, 'character') 8 | 9 | --- 10 | 11 | Code 12 | fix_text("\"class(x) == 'character'\"") 13 | 14 | --- 15 | 16 | Code 17 | fix_text("class(x) != 'character'") 18 | Output 19 | Old code: class(x) != 'character' 20 | New code: !inherits(x, 'character') 21 | 22 | --- 23 | 24 | Code 25 | fix_text("\"class(x) != 'character'\"") 26 | 27 | --- 28 | 29 | Code 30 | fix_text("'character' %in% class(x)") 31 | Output 32 | Old code: 'character' %in% class(x) 33 | New code: inherits(x, 'character') 34 | 35 | --- 36 | 37 | Code 38 | fix_text("\"'character' %in% class(x)\"") 39 | 40 | --- 41 | 42 | Code 43 | fix_text("!'character' %in% class(x)") 44 | Output 45 | Old code: !'character' %in% class(x) 46 | New code: !inherits(x, 'character') 47 | 48 | --- 49 | 50 | Code 51 | fix_text("! 'character' %in% class(x)") 52 | Output 53 | Old code: ! 'character' %in% class(x) 54 | New code: ! inherits(x, 'character') 55 | 56 | --- 57 | 58 | Code 59 | fix_text("!('character' %in% class(x))") 60 | Output 61 | Old code: !('character' %in% class(x)) 62 | New code: !(inherits(x, 'character')) 63 | 64 | -------------------------------------------------------------------------------- /inst/rules/builtin/unreachable_code.yml: -------------------------------------------------------------------------------- 1 | id: unreachable_code-1 2 | language: r 3 | severity: warning 4 | rule: 5 | regex: '[^}]+' 6 | not: 7 | regex: 'else' 8 | follows: 9 | any: 10 | - pattern: return($$$A) 11 | - pattern: stop($$$A) 12 | not: 13 | precedes: 14 | regex: 'else' 15 | stopBy: end 16 | message: Code and comments coming after a return() or stop() should be removed. 17 | 18 | --- 19 | 20 | id: unreachable_code-2 21 | language: r 22 | severity: warning 23 | rule: 24 | regex: '[^}]+' 25 | not: 26 | regex: 'else' 27 | follows: 28 | any: 29 | - pattern: next 30 | - pattern: break 31 | stopBy: end 32 | message: Remove code and comments coming after `next` or `break` 33 | 34 | --- 35 | 36 | id: unreachable_code-3 37 | language: r 38 | severity: warning 39 | rule: 40 | inside: 41 | any: 42 | - kind: if_statement 43 | pattern: if (FALSE) 44 | - kind: while_statement 45 | pattern: while (FALSE) 46 | stopBy: end 47 | message: Remove code inside a conditional loop with a deterministically false condition. 48 | 49 | --- 50 | 51 | id: unreachable_code-4 52 | language: r 53 | severity: warning 54 | rule: 55 | inside: 56 | any: 57 | - kind: if_statement 58 | pattern: if (TRUE) 59 | - kind: while_statement 60 | pattern: while (TRUE) 61 | stopBy: end 62 | message: | 63 | One branch has a a deterministically true condition. The other branches can 64 | be removed. 65 | -------------------------------------------------------------------------------- /inst/rules/builtin/length_test.yml: -------------------------------------------------------------------------------- 1 | # Strangely, having something like pattern: length($VAR $OP $VAR2) doesn't work 2 | 3 | id: length_test-1 4 | language: r 5 | severity: warning 6 | rule: 7 | pattern: length($VAR == $VAR2) 8 | fix: length(~~VAR~~) == ~~VAR2~~ 9 | message: Checking the length of a logical vector is likely a mistake. 10 | 11 | --- 12 | 13 | id: length_test-2 14 | language: r 15 | severity: warning 16 | rule: 17 | pattern: length($VAR != $VAR2) 18 | fix: length(~~VAR~~) != ~~VAR2~~ 19 | message: Checking the length of a logical vector is likely a mistake. 20 | 21 | --- 22 | 23 | id: length_test-3 24 | language: r 25 | severity: warning 26 | rule: 27 | pattern: length($VAR > $VAR2) 28 | fix: length(~~VAR~~) > ~~VAR2~~ 29 | message: Checking the length of a logical vector is likely a mistake. 30 | 31 | --- 32 | 33 | id: length_test-4 34 | language: r 35 | severity: warning 36 | rule: 37 | pattern: length($VAR >= $VAR2) 38 | fix: length(~~VAR~~) >= ~~VAR2~~ 39 | message: Checking the length of a logical vector is likely a mistake. 40 | 41 | --- 42 | 43 | id: length_test-5 44 | language: r 45 | severity: warning 46 | rule: 47 | pattern: length($VAR < $VAR2) 48 | fix: length(~~VAR~~) < ~~VAR2~~ 49 | message: Checking the length of a logical vector is likely a mistake. 50 | 51 | --- 52 | 53 | id: length_test-6 54 | language: r 55 | severity: warning 56 | rule: 57 | pattern: length($VAR <= $VAR2) 58 | fix: length(~~VAR~~) <= ~~VAR2~~ 59 | message: Checking the length of a logical vector is likely a mistake. 60 | -------------------------------------------------------------------------------- /tests/testthat/test-empty_assignment.R: -------------------------------------------------------------------------------- 1 | test_that("empty_assignment_linter skips valid usage", { 2 | expect_no_lint("x <- { 3 + 4 }", empty_assignment_linter()) 3 | expect_no_lint("x <- if (x > 1) { 3 + 4 }", empty_assignment_linter()) 4 | 5 | # also triggers assignment_linter 6 | expect_no_lint("x = { 3 + 4 }", empty_assignment_linter()) 7 | }) 8 | 9 | test_that("empty_assignment_linter blocks disallowed usages", { 10 | linter <- empty_assignment_linter() 11 | lint_msg <- "Assign NULL explicitly or, whenever possible, allocate the empty object" 12 | 13 | expect_lint("xrep <- {}", lint_msg, linter) 14 | 15 | # assignment with equal works as well, and white space doesn't matter 16 | expect_lint("x = { }", lint_msg, linter) 17 | 18 | # ditto right assignments 19 | # expect_lint("{} -> x", lint_msg, linter) 20 | # expect_lint("{} ->> x", lint_msg, linter) 21 | 22 | # ditto data.table-style walrus assignments 23 | # expect_lint("x[, col := {}]", lint_msg, linter) 24 | 25 | # newlines also don't matter 26 | expect_lint("x <- {\n}", lint_msg, linter) 27 | 28 | # LHS of assignment doesn't matter 29 | expect_lint("env$obj <- {}", lint_msg, linter) 30 | }) 31 | 32 | test_that("lints vectorize", { 33 | lint_msg <- "Assign NULL explicitly or, whenever possible, allocate the empty object" 34 | 35 | expect_equal( 36 | nrow( 37 | lint_text( 38 | "{ 39 | x <- {} 40 | y = {} 41 | }", 42 | linters = empty_assignment_linter() 43 | ) 44 | ), 45 | 2 46 | ) 47 | }) 48 | -------------------------------------------------------------------------------- /inst/rules/builtin/redundant_ifelse.yml: -------------------------------------------------------------------------------- 1 | id: redundant_ifelse-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: $FUN($COND, $VAL1, $VAL2) 6 | constraints: 7 | VAL1: 8 | regex: ^TRUE$ 9 | VAL2: 10 | regex: ^FALSE$ 11 | FUN: 12 | regex: ^(ifelse|fifelse|if_else)$ 13 | fix: ~~COND~~ 14 | message: | 15 | Use ~~COND~~ directly instead of calling ~~FUN~~(~~COND~~, TRUE, FALSE). 16 | 17 | --- 18 | 19 | id: redundant_ifelse-2 20 | language: r 21 | severity: warning 22 | rule: 23 | pattern: $FUN($COND, $VAL1, $VAL2) 24 | constraints: 25 | VAL1: 26 | regex: ^FALSE$ 27 | VAL2: 28 | regex: ^TRUE$ 29 | FUN: 30 | regex: ^(ifelse|fifelse|if_else)$ 31 | fix: '!(~~COND~~)' 32 | message: | 33 | Use !(~~COND~~) directly instead of calling ~~FUN~~(~~COND~~, FALSE, TRUE). 34 | 35 | --- 36 | 37 | id: redundant_ifelse-3 38 | language: r 39 | severity: warning 40 | rule: 41 | pattern: $FUN($COND, $VAL1, $VAL2) 42 | constraints: 43 | VAL1: 44 | regex: ^(1|1L)$ 45 | VAL2: 46 | regex: ^(0|0L)$ 47 | FUN: 48 | regex: ^(ifelse|fifelse|if_else)$ 49 | fix: as.integer(~~COND~~) 50 | message: Prefer as.integer(~~COND~~) to ~~FUN~~(~~COND~~, ~~VAL1~~, ~~VAL2~~). 51 | 52 | --- 53 | 54 | id: redundant_ifelse-4 55 | language: r 56 | severity: warning 57 | rule: 58 | pattern: $FUN($COND, $VAL1, $VAL2) 59 | constraints: 60 | VAL1: 61 | regex: ^(0|0L)$ 62 | VAL2: 63 | regex: ^(1|1L)$ 64 | FUN: 65 | regex: ^(ifelse|fifelse|if_else)$ 66 | fix: as.integer(!(~~COND~~)) 67 | message: Prefer as.integer(!(~~COND~~)) to ~~FUN~~(~~COND~~, ~~VAL1~~, ~~VAL2~~). 68 | -------------------------------------------------------------------------------- /tests/testthat/test-length_test.R: -------------------------------------------------------------------------------- 1 | test_that("skips allowed usages", { 2 | linter <- length_test_linter() 3 | 4 | expect_no_lint("length(x) > 0", linter) 5 | expect_no_lint("length(DF[key == val, cols])", linter) 6 | }) 7 | 8 | test_that("blocks simple disallowed usages", { 9 | linter <- length_test_linter() 10 | lint_msg_stub <- "Checking the length of a logical vector is likely a mistake" 11 | 12 | expect_lint("length(x == 0)", lint_msg_stub, linter) 13 | expect_lint("length(x == y)", lint_msg_stub, linter) 14 | expect_lint("length(x + y == 2)", lint_msg_stub, linter) 15 | }) 16 | 17 | test_that("blocks simple disallowed usages", { 18 | linter <- length_test_linter() 19 | lint_msg_stub <- "Checking the length of a logical vector is likely a mistake" 20 | 21 | expect_lint("length(x != 0)", lint_msg_stub, linter) 22 | expect_lint("length(x >= 0)", lint_msg_stub, linter) 23 | expect_lint("length(x <= 0)", lint_msg_stub, linter) 24 | expect_lint("length(x > 0)", lint_msg_stub, linter) 25 | expect_lint("length(x < 0)", lint_msg_stub, linter) 26 | expect_lint("length(x < 0)", lint_msg_stub, linter) 27 | }) 28 | 29 | test_that("fix works", { 30 | linter <- length_test_linter() 31 | 32 | expect_snapshot(fix_text("length(x == 0)", linters = linter)) 33 | expect_snapshot(fix_text("length(x != 0)", linters = linter)) 34 | expect_snapshot(fix_text("length(x >= 0)", linters = linter)) 35 | expect_snapshot(fix_text("length(x <= 0)", linters = linter)) 36 | expect_snapshot(fix_text("length(x > 0)", linters = linter)) 37 | expect_snapshot(fix_text("length(x < 0)", linters = linter)) 38 | }) 39 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/config_yml.md: -------------------------------------------------------------------------------- 1 | # config.yml errors when it doesn't contain any rule 2 | 3 | Code 4 | lint() 5 | Condition 6 | Error in `lint()`: 7 | ! 'flir/config.yml' exists but doesn't contain any rule. 8 | 9 | --- 10 | 11 | Code 12 | lint() 13 | Condition 14 | Error in `lint()`: 15 | ! 'flir/config.yml' exists but doesn't contain any rule. 16 | 17 | # config.yml errors when it contains unknown rules 18 | 19 | Code 20 | lint() 21 | Condition 22 | Error in `lint()`: 23 | ! Unknown linters: foo, bar 24 | 25 | # config.yml errors when it contains duplicated rules 26 | 27 | Code 28 | lint() 29 | Condition 30 | Error in `lint()`: 31 | ! In 'flir/config.yml', the following linters are duplicated: equal_assignment 32 | 33 | # config.yml errors with unknown fields 34 | 35 | Code 36 | lint() 37 | Condition 38 | Error in `lint()`: 39 | ! Unknown field in 'flir/config.yml': some_field 40 | 41 | # config.yml errors with duplicated fields 42 | 43 | Code 44 | lint() 45 | Condition 46 | Error in `yaml.load()`: 47 | ! (flir/config.yml) Duplicate map key: 'keep' 48 | 49 | # config: `from-package` checks duplicated package name 50 | 51 | Code 52 | lint() 53 | Condition 54 | Error in `lint()`: 55 | ! In 'flir/config.yml', the following packages are duplicated: foo 56 | 57 | # config: `from-package` checks that package is installed 58 | 59 | Code 60 | lint() 61 | Condition 62 | Error in `lint()`: 63 | ! The package "foo" is required. 64 | 65 | -------------------------------------------------------------------------------- /tests/testthat/test-export_new_rule.R: -------------------------------------------------------------------------------- 1 | test_that("export_new_rule() only works in packages", { 2 | create_local_project() 3 | expect_snapshot( 4 | export_new_rule("foobar"), 5 | error = TRUE 6 | ) 7 | }) 8 | 9 | test_that("export_new_rule() errors on wrong names", { 10 | create_local_package() 11 | expect_snapshot( 12 | export_new_rule(1), 13 | error = TRUE 14 | ) 15 | }) 16 | 17 | test_that("export_new_rule() can create files", { 18 | create_local_package() 19 | export_new_rule("foobar") 20 | export_new_rule("foobar2") 21 | expect_true(fs::file_exists("inst/flir/rules/foobar.yml")) 22 | expect_true(fs::file_exists("inst/flir/rules/foobar2.yml")) 23 | }) 24 | 25 | test_that("export_new_rule() cannot overwrite files", { 26 | create_local_package() 27 | export_new_rule("foobar") 28 | expect_snapshot( 29 | export_new_rule("foobar"), 30 | error = TRUE 31 | ) 32 | }) 33 | 34 | test_that("export_new_rule() cannot create file with whitespace", { 35 | create_local_package() 36 | expect_snapshot( 37 | export_new_rule("hi there"), 38 | error = TRUE 39 | ) 40 | }) 41 | 42 | test_that("export_new_rule() can create several rules at once", { 43 | create_local_package() 44 | setup_flir() 45 | export_new_rule(c("foobar", "foobar2")) 46 | expect_true(fs::file_exists("inst/flir/rules/foobar.yml")) 47 | expect_true(fs::file_exists("inst/flir/rules/foobar2.yml")) 48 | expect_true(any(grepl( 49 | "id: foobar$", 50 | readLines("inst/flir/rules/foobar.yml") 51 | ))) 52 | expect_true(any(grepl( 53 | "id: foobar2$", 54 | readLines("inst/flir/rules/foobar2.yml") 55 | ))) 56 | }) 57 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | name: pkgdown 13 | 14 | permissions: read-all 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.head_ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | pkgdown: 22 | runs-on: ubuntu-latest 23 | # Only restrict concurrency for non-PR jobs 24 | concurrency: 25 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 26 | env: 27 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 28 | permissions: 29 | contents: write 30 | steps: 31 | - uses: actions/checkout@v4 32 | 33 | - uses: r-lib/actions/setup-pandoc@v2 34 | 35 | - uses: r-lib/actions/setup-r@v2 36 | with: 37 | use-public-rspm: true 38 | 39 | - uses: r-lib/actions/setup-r-dependencies@v2 40 | with: 41 | extra-packages: any::pkgdown, local::. 42 | needs: website 43 | 44 | - name: Build site 45 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 46 | shell: Rscript {0} 47 | 48 | - name: Deploy to GitHub pages 🚀 49 | if: github.event_name != 'pull_request' 50 | uses: JamesIves/github-pages-deploy-action@v4.5.0 51 | with: 52 | clean: false 53 | branch: gh-pages 54 | folder: docs 55 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: flir 2 | Type: Package 3 | Title: Find and Fix Lints in R Code 4 | Version: 0.6.0 5 | Authors@R: 6 | c(person(given = "Etienne", 7 | family = "Bacher", 8 | role = c("aut", "cre", "cph"), 9 | email = "etienne.bacher@protonmail.com"), 10 | person("lintr authors", role = "aut"), 11 | person("Trevor L.", "Davis", role = c("ctb"), 12 | comment = c(ORCID = "0000-0001-6341-4639"))) 13 | Description: Lints are code patterns that are not optimal because they are 14 | inefficient, forget corner cases, or are less readable. 'flir' provides a 15 | small set of functions to detect those lints and automatically fix them. 16 | It builds on 'astgrepr', which itself uses the 'Rust' crate 'ast-grep' to 17 | parse and navigate R code. 18 | Depends: 19 | R (>= 4.2) 20 | Imports: 21 | astgrepr (>= 0.1.0), 22 | cli, 23 | crayon, 24 | data.table, 25 | digest, 26 | fs, 27 | git2r, 28 | rprojroot, 29 | yaml 30 | Suggests: 31 | diffviewer, 32 | glue, 33 | knitr, 34 | patrick, 35 | rlang, 36 | rmarkdown, 37 | rstudioapi, 38 | shiny, 39 | spelling, 40 | testthat (>= 3.0.0), 41 | tibble, 42 | usethis, 43 | utils, 44 | withr 45 | License: MIT + file LICENSE 46 | Encoding: UTF-8 47 | LazyData: true 48 | RoxygenNote: 7.3.2 49 | URL: https://flir.etiennebacher.com, https://github.com/etiennebacher/flir 50 | BugReports: https://github.com/etiennebacher/flir/issues 51 | Config/testthat/edition: 3 52 | Roxygen: list(markdown = TRUE) 53 | VignetteBuilder: knitr 54 | Config/testthat/parallel: true 55 | Language: en-US 56 | -------------------------------------------------------------------------------- /R/github_action.R: -------------------------------------------------------------------------------- 1 | #' Create a Github Actions workflow for `flir` 2 | #' 3 | #' @inheritParams setup_flir 4 | #' @param overwrite Whether to overwrite `.github/workflows/flir.yaml` if it 5 | #' already exists. 6 | #' 7 | #' @return Creates `.github/workflows/flir.yaml` but doesn't return any value. 8 | #' @export 9 | setup_flir_gha <- function(path, overwrite = FALSE) { 10 | if (missing(path) && is_testing()) { 11 | path <- "." 12 | } 13 | src <- system.file("gha/flir.yaml", package = "flir") 14 | tar <- file.path(path, ".github/workflows/flir.yaml") 15 | if (!fs::dir_exists(dirname(tar))) { 16 | fs::dir_create(dirname(tar)) 17 | } 18 | fs::file_copy(src, tar, overwrite = overwrite) 19 | if (!is_testing()) { 20 | cli::cli_inform("Created {.path .github/workflows/flir.yaml}.") 21 | } 22 | } 23 | 24 | ### Taken from https://github.com/r-lib/lintr/blob/main/R/actions.R 25 | ### [MIT License] 26 | 27 | in_github_actions <- function() { 28 | identical(Sys.getenv("GITHUB_ACTIONS"), "true") 29 | } 30 | 31 | # Output logging commands for any lints found 32 | github_actions_log_lints <- function(x, project_dir = "") { 33 | if (nzchar(project_dir)) { 34 | x$filename <- file.path(project_dir, x$filename) 35 | } 36 | file_line_col <- sprintf( 37 | "file=%s,line=%s,col=%s", 38 | x$file, 39 | x$line_start, 40 | x$col_start 41 | ) 42 | # Otherwise highlighting is only applied on the first part of the message 43 | x$message <- gsub("\\\n", " ", x$message) 44 | cat( 45 | sprintf( 46 | "::warning %s::%s,[%s] %s\n", 47 | file_line_col, 48 | file_line_col, 49 | x$text, 50 | x$message 51 | ), 52 | sep = "" 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/nested-lints.md: -------------------------------------------------------------------------------- 1 | # nested lints 2 | 3 | Code 4 | fix_text("any(duplicated(any(is.na(x))))") 5 | Output 6 | Old code: any(duplicated(any(is.na(x)))) 7 | New code: anyDuplicated(anyNA(x)) > 0 8 | 9 | --- 10 | 11 | Code 12 | fix_text("any(duplicated(any(is.na(x <- T))))") 13 | Output 14 | Old code: any(duplicated(any(is.na(x <- T)))) 15 | New code: anyDuplicated(anyNA(x <- TRUE)) > 0 16 | 17 | --- 18 | 19 | Code 20 | fix_text("\nexpect_equal(\n anyDuplicated(x) > 0,\n FALSE\n)") 21 | Output 22 | Old code: 23 | expect_equal( 24 | anyDuplicated(x) > 0, 25 | FALSE 26 | ) 27 | 28 | New code: 29 | expect_false(anyDuplicated(x) > 0) 30 | 31 | --- 32 | 33 | Code 34 | fix_text( 35 | "\ntest_that(\n 'Formalist works',\n {\n expect_equal(\n anyDuplicated(x) > 0,\n FALSE\n )\n }\n)") 36 | Output 37 | Old code: 38 | test_that( 39 | 'Formalist works', 40 | { 41 | expect_equal( 42 | anyDuplicated(x) > 0, 43 | FALSE 44 | ) 45 | } 46 | ) 47 | 48 | New code: 49 | test_that( 50 | 'Formalist works', 51 | { 52 | expect_false(anyDuplicated(x) > 0) 53 | } 54 | ) 55 | 56 | --- 57 | 58 | Code 59 | fix_text( 60 | "test_that('Formalist works',{expect_equal(anyDuplicated(x) > 0,FALSE)})") 61 | Output 62 | Old code: test_that('Formalist works',{expect_equal(anyDuplicated(x) > 0,FALSE)}) 63 | New code: test_that('Formalist works',{expect_false(anyDuplicated(x) > 0)}) 64 | 65 | -------------------------------------------------------------------------------- /tests/testthat/test-expect_not.R: -------------------------------------------------------------------------------- 1 | test_that("expect_not_linter skips allowed usages", { 2 | expect_no_lint("expect_true(x)", expect_not_linter()) 3 | expect_no_lint("expect_false(x)", expect_not_linter()) 4 | 5 | # not a strict ban on ! 6 | ## (expect_false(x && y) is the same, but it's not clear which to prefer) 7 | expect_no_lint("expect_true(!x || !y)", expect_not_linter()) 8 | }) 9 | 10 | test_that("expect_not_linter doesn't block rlang bang-bang", { 11 | expect_no_lint("expect_true(!!x)", expect_not_linter()) 12 | expect_no_lint("expect_true(!!!x)", expect_not_linter()) 13 | expect_no_lint("expect_false(!!x)", expect_not_linter()) 14 | expect_no_lint("expect_false(!!!x)", expect_not_linter()) 15 | }) 16 | 17 | test_that("expect_not_linter blocks simple disallowed usages", { 18 | linter <- expect_not_linter() 19 | lint_msg <- "expect_false(x) is better than expect_true(!x), and vice versa." 20 | 21 | expect_lint("expect_true(!x)", lint_msg, linter) 22 | expect_lint("expect_false(!foo(x))", lint_msg, linter) 23 | expect_lint("expect_true(!(x && y))", lint_msg, linter) 24 | }) 25 | 26 | test_that("lints vectorize", { 27 | expect_equal( 28 | nrow( 29 | lint_text( 30 | "{ 31 | expect_true(!x) 32 | expect_false(!y) 33 | }", 34 | linters = expect_not_linter() 35 | ) 36 | ), 37 | 2 38 | ) 39 | }) 40 | 41 | test_that("fix works", { 42 | linter <- expect_not_linter() 43 | 44 | expect_snapshot(fix_text("expect_true(!anyNA(x))", linters = linter)) 45 | expect_snapshot(fix_text("expect_false(!anyNA(x))", linters = linter)) 46 | 47 | # Do not change 48 | expect_snapshot(fix_text("expect_true(!anyNA(x) & TRUE)", linters = linter)) 49 | expect_snapshot(fix_text("expect_false(!anyNA(x) & TRUE)", linters = linter)) 50 | }) 51 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: R-CMD-check 10 | 11 | permissions: read-all 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.head_ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | R-CMD-check: 19 | runs-on: ${{ matrix.config.os }} 20 | 21 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 22 | 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | config: 27 | - {os: macos-latest, r: 'release'} 28 | - {os: windows-latest, r: 'release'} 29 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 30 | - {os: ubuntu-latest, r: 'release'} 31 | - {os: ubuntu-latest, r: 'oldrel-1'} 32 | - {os: ubuntu-latest, r: 'oldrel-2'} 33 | 34 | env: 35 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 36 | R_KEEP_PKG_SOURCE: yes 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | 41 | - uses: r-lib/actions/setup-pandoc@v2 42 | 43 | - uses: r-lib/actions/setup-r@v2 44 | with: 45 | r-version: ${{ matrix.config.r }} 46 | http-user-agent: ${{ matrix.config.http-user-agent }} 47 | use-public-rspm: true 48 | 49 | - uses: r-lib/actions/setup-r-dependencies@v2 50 | with: 51 | extra-packages: any::rcmdcheck 52 | needs: check 53 | 54 | - uses: r-lib/actions/check-r-package@v2 55 | with: 56 | upload-snapshots: true 57 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' 58 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/expect_s4_class.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("expect_true(is(x, 's4class'))", linters = linter) 5 | Output 6 | Old code: expect_true(is(x, 's4class')) 7 | New code: expect_s4_class(x, 's4class') 8 | 9 | --- 10 | 11 | Code 12 | fix_text("expect_true(methods::is(x, 's4class'))", linters = linter) 13 | Output 14 | Old code: expect_true(methods::is(x, 's4class')) 15 | New code: expect_s4_class(x, 's4class') 16 | 17 | --- 18 | 19 | Code 20 | fix_text("testthat::expect_true(is(x, 's4class'))", linters = linter) 21 | Output 22 | Old code: testthat::expect_true(is(x, 's4class')) 23 | New code: testthat::expect_s4_class(x, 's4class') 24 | 25 | --- 26 | 27 | Code 28 | fix_text("testthat::expect_true(methods::is(x, 's4class'))", linters = linter) 29 | Output 30 | Old code: testthat::expect_true(methods::is(x, 's4class')) 31 | New code: testthat::expect_s4_class(x, 's4class') 32 | 33 | --- 34 | 35 | Code 36 | fix_text("testthat::expect_true(is(object = x, class2 = 's4class'))", linters = linter) 37 | Output 38 | Old code: testthat::expect_true(is(object = x, class2 = 's4class')) 39 | New code: testthat::expect_s4_class(x, 's4class') 40 | 41 | --- 42 | 43 | Code 44 | fix_text("testthat::expect_true(is(class2 = 's4class', object = x))", linters = linter) 45 | Output 46 | Old code: testthat::expect_true(is(class2 = 's4class', object = x)) 47 | New code: testthat::expect_s4_class(x, 's4class') 48 | 49 | --- 50 | 51 | Code 52 | fix_text("testthat::expect_true(is(class2 = 's4class', object = x))", linters = linter) 53 | Output 54 | Old code: testthat::expect_true(is(class2 = 's4class', object = x)) 55 | New code: testthat::expect_s4_class(x, 's4class') 56 | 57 | -------------------------------------------------------------------------------- /inst/rules/builtin/paste.yml: -------------------------------------------------------------------------------- 1 | id: paste-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: 6 | context: paste($$$CONTENT sep = "" $$$CONTENT2) 7 | strictness: ast 8 | # fix: paste0($$$CONTENT) 9 | message: paste0(...) is better than paste(..., sep = ""). 10 | 11 | --- 12 | 13 | id: paste-2 14 | language: r 15 | severity: warning 16 | rule: 17 | any: 18 | - pattern: 19 | context: paste($CONTENT, collapse = ", ") 20 | strictness: ast 21 | - pattern: 22 | context: paste(collapse = ", ", $CONTENT) 23 | strictness: ast 24 | # fix: paste0($$$CONTENT) 25 | message: toString(.) is more expressive than paste(., collapse = ", "). 26 | 27 | --- 28 | 29 | id: paste-3 30 | language: r 31 | severity: warning 32 | rule: 33 | pattern: 34 | context: paste0($$$CONTENT sep = $USELESS $$$CONTENT2) 35 | strictness: ast 36 | # fix: paste0($$$CONTENT) 37 | message: | 38 | sep= is not a formal argument to paste0(); did you mean to use paste(), or 39 | collapse=? 40 | 41 | --- 42 | 43 | id: paste-4 44 | language: r 45 | severity: warning 46 | rule: 47 | any: 48 | - pattern: 49 | context: paste0($CONTENT, collapse = $FOO) 50 | strictness: ast 51 | - pattern: 52 | context: paste0(collapse = $FOO, $CONTENT) 53 | strictness: ast 54 | not: 55 | has: 56 | regex: sep 57 | kind: argument 58 | # fix: paste0($$$CONTENT) 59 | message: | 60 | Use paste(), not paste0(), to collapse a character vector when sep= is not used. 61 | 62 | # --- 63 | # 64 | # id: paste-5 65 | # language: r 66 | # severity: warning 67 | # rule: 68 | # pattern: 69 | # context: paste0(rep($VAR, $TIMES), collapse = "") 70 | # strictness: ast 71 | # constraints: 72 | # VAR: 73 | # kind: string 74 | # fix: strrep(~~VAR~~, ~~TIMES~~) 75 | # message: strrep(x, times) is better than paste0(rep(x, times), collapse = ""). 76 | -------------------------------------------------------------------------------- /tests/testthat/test-expect_comparison.R: -------------------------------------------------------------------------------- 1 | test_that("expect_comparison_linter skips allowed usages", { 2 | linter <- expect_comparison_linter() 3 | 4 | # there's no expect_ne() for this operator 5 | expect_no_lint("expect_true(x != y)", linter) 6 | # NB: also applies to tinytest, but it's sufficient to test testthat 7 | expect_no_lint("testthat::expect_true(x != y)", linter) 8 | 9 | # multiple comparisons are OK 10 | expect_no_lint("expect_true(x > y || x > z)", linter) 11 | 12 | # expect_gt() and friends don't have an info= argument 13 | expect_no_lint("expect_true(x > y, info = 'x is bigger than y yo')", linter) 14 | 15 | # expect_true() used incorrectly, and as executed the first argument is not a lint 16 | expect_no_lint("expect_true(is.count(n_draws), n_draws > 1)", linter) 17 | }) 18 | 19 | test_that("expect_comparison_linter blocks simple disallowed usages", { 20 | linter <- expect_comparison_linter() 21 | 22 | expect_lint( 23 | "expect_true(x > y)", 24 | "expect_gt(x, y) is better than expect_true(x > y).", 25 | linter 26 | ) 27 | 28 | expect_lint( 29 | "expect_true(x < y)", 30 | "expect_lt(x, y) is better than expect_true(x < y).", 31 | linter 32 | ) 33 | 34 | expect_lint( 35 | "expect_true(foo(x) >= y[[2]])", 36 | "expect_gte(x, y) is better than expect_true(x >= y).", 37 | linter 38 | ) 39 | 40 | expect_lint( 41 | "expect_true(x <= y)", 42 | "expect_lte(x, y) is better than expect_true(x <= y).", 43 | linter 44 | ) 45 | }) 46 | 47 | test_that("fix works", { 48 | linter <- expect_comparison_linter() 49 | 50 | expect_snapshot(fix_text("expect_true(x > y)", linters = linter)) 51 | expect_snapshot(fix_text("expect_true(x >= y)", linters = linter)) 52 | expect_snapshot(fix_text("expect_true(x < y)", linters = linter)) 53 | expect_snapshot(fix_text("expect_true(x <= y)", linters = linter)) 54 | }) 55 | -------------------------------------------------------------------------------- /man/setup_flir.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/setup.R 3 | \name{setup_flir} 4 | \alias{setup_flir} 5 | \title{Setup flir} 6 | \usage{ 7 | setup_flir(path) 8 | } 9 | \arguments{ 10 | \item{path}{Path to package or project root. If \code{NULL} (default), uses \code{"."}.} 11 | } 12 | \value{ 13 | Imports files necessary for \code{flir} to work but doesn't return any 14 | value in R. 15 | } 16 | \description{ 17 | This creates a \code{flir} folder that has multiple purposes. It contains: 18 | \itemize{ 19 | \item the file \code{config.yml} where you can define rules to keep or exclude, as 20 | well as rules defined in other packages. More on this below; 21 | \item the file \code{cache_file_state.rds}, which is used when \verb{lint_*()} or \verb{fix_*()} 22 | have \code{cache = TRUE}; 23 | \item an optional folder \code{rules/custom} where you can store your own rules. 24 | } 25 | 26 | This folder must live at the root of the project and cannot be renamed. 27 | } 28 | \details{ 29 | The file \code{flir/config.yml} can contain three fields: \code{keep}, \code{exclude}, 30 | and \code{from-package}. 31 | 32 | \code{keep} and \code{exclude} are used to define the rules to keep or to exclude when 33 | running \verb{lint_*()} or \verb{fix_*()}. 34 | 35 | It is possible for other packages to create their own list of rules, for 36 | instance to detect or replace deprecated functions. In \code{from-package}, you 37 | can list package names where \code{flir} should look for additional rules. By 38 | default, if you list package \code{foobar}, then all rules defined in the package 39 | \code{foobar} will be used. To ignore some of those rules, you can list 40 | \verb{from-foobar-} in the \code{exclude} field. 41 | 42 | See the vignette \href{https://flir.etiennebacher.com/articles/sharing_rules.html}{Sharing rules across packages} for more information. 43 | } 44 | -------------------------------------------------------------------------------- /inst/rules/builtin/implicit_assignment.yml: -------------------------------------------------------------------------------- 1 | id: implicit_assignment-1 2 | language: r 3 | severity: warning 4 | rule: 5 | any: 6 | - pattern: $RECEIVER <- $VALUE 7 | - pattern: $RECEIVER <<- $VALUE 8 | - pattern: $VALUE -> $RECEIVER 9 | - pattern: $VALUE ->> $RECEIVER 10 | inside: 11 | any: 12 | - kind: if_statement 13 | - kind: while_statement 14 | field: condition 15 | stopBy: end 16 | strictness: cst 17 | message: | 18 | Avoid implicit assignments in function calls. For example, instead of 19 | `if (x <- 1L) { ... }`, write `x <- 1L; if (x) { ... }`. 20 | 21 | --- 22 | 23 | id: implicit_assignment-2 24 | language: r 25 | severity: warning 26 | rule: 27 | any: 28 | - pattern: $RECEIVER <- $VALUE 29 | - pattern: $RECEIVER <<- $VALUE 30 | - pattern: $VALUE -> $RECEIVER 31 | - pattern: $VALUE ->> $RECEIVER 32 | inside: 33 | kind: for_statement 34 | field: sequence 35 | stopBy: end 36 | strictness: cst 37 | message: | 38 | Avoid implicit assignments in function calls. For example, instead of 39 | `if (x <- 1L) { ... }`, write `x <- 1L; if (x) { ... }`. 40 | 41 | # --- 42 | # 43 | # id: implicit_assignment-3 44 | # language: r 45 | # severity: warning 46 | # rule: 47 | # any: 48 | # - pattern: $RECEIVER <- $VALUE 49 | # - pattern: $RECEIVER <<- $VALUE 50 | # - pattern: $VALUE -> $RECEIVER 51 | # - pattern: $VALUE ->> $RECEIVER 52 | # inside: 53 | # kind: argument 54 | # field: value 55 | # strictness: cst 56 | # stopBy: end 57 | # not: 58 | # inside: 59 | # kind: call 60 | # field: function 61 | # has: 62 | # kind: identifier 63 | # regex: ^(lapply)$ 64 | # stopBy: end 65 | # strictness: cst 66 | # message: | 67 | # Avoid implicit assignments in function calls. For example, instead of 68 | # `if (x <- 1L) { ... }`, write `x <- 1L; if (x) { ... }`. 69 | 70 | -------------------------------------------------------------------------------- /inst/rules/builtin/literal_coercion.yml: -------------------------------------------------------------------------------- 1 | id: literal_coercion-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: $FUN($VALUE) 6 | constraints: 7 | VALUE: 8 | kind: argument 9 | has: 10 | kind: float 11 | not: 12 | regex: 'e' 13 | FUN: 14 | regex: ^(int|as\.integer)$ 15 | fix: ~~VALUE~~L 16 | message: | 17 | Use ~~VALUE~~L instead of ~~FUN~~(~~VALUE~~), i.e., use literals directly 18 | where possible, instead of coercion. 19 | 20 | --- 21 | 22 | id: literal_coercion-2 23 | language: r 24 | severity: warning 25 | rule: 26 | pattern: as.character(NA) 27 | fix: NA_character_ 28 | message: | 29 | Use NA_character_ instead of as.character(NA), i.e., use literals directly 30 | where possible, instead of coercion. 31 | 32 | --- 33 | 34 | id: literal_coercion-3 35 | language: r 36 | severity: warning 37 | rule: 38 | pattern: as.logical($VAR) 39 | constraints: 40 | VAR: 41 | kind: argument 42 | has: 43 | any: 44 | - regex: ^1L$ 45 | - regex: ^1$ 46 | - regex: 'true' 47 | fix: TRUE 48 | message: Use TRUE instead of as.logical(~~VAR~~). 49 | 50 | --- 51 | 52 | id: literal_coercion-4 53 | language: r 54 | severity: warning 55 | rule: 56 | pattern: $FUN($VAR) 57 | constraints: 58 | VAR: 59 | kind: argument 60 | has: 61 | kind: float 62 | FUN: 63 | regex: ^(as\.numeric|as\.double)$ 64 | fix: ~~VAR~~ 65 | message: Use ~~VAR~~ instead of ~~FUN~~(~~VAR~~). 66 | 67 | --- 68 | 69 | id: literal_coercion-5 70 | language: r 71 | severity: warning 72 | rule: 73 | pattern: as.integer(NA) 74 | fix: NA_integer_ 75 | message: Use NA_integer_ instead of as.integer(NA). 76 | 77 | --- 78 | 79 | id: literal_coercion-6 80 | language: r 81 | severity: warning 82 | rule: 83 | pattern: $FUN(NA) 84 | constraints: 85 | FUN: 86 | regex: ^(as\.numeric|as\.double)$ 87 | fix: NA_real_ 88 | message: Use NA_real_ instead of ~~FUN~~(NA). 89 | 90 | -------------------------------------------------------------------------------- /tests/testthat/test-function_return.R: -------------------------------------------------------------------------------- 1 | test_that("function_return_linter skips allowed usages", { 2 | lines_simple <- trim_some( 3 | " 4 | foo <- function(x) { 5 | x <- x + 1 6 | return(x) 7 | } 8 | " 9 | ) 10 | expect_no_lint(lines_simple, function_return_linter()) 11 | 12 | # arguably an expression as complicated as this should also be assigned, 13 | # but for now that's out of the scope of this linter 14 | lines_subassignment <- trim_some( 15 | " 16 | foo <- function(x) { 17 | return(x[, { 18 | col <- col + 1 19 | .(grp, col) 20 | }]) 21 | } 22 | " 23 | ) 24 | expect_no_lint(lines_subassignment, function_return_linter()) 25 | }) 26 | 27 | test_that("function_return_linter blocks simple disallowed usages", { 28 | linter <- function_return_linter() 29 | lint_msg <- "Move the assignment outside of the return() clause" 30 | 31 | expect_lint( 32 | trim_some( 33 | " 34 | foo <- function(x) { 35 | return(x <- x + 1) 36 | } 37 | " 38 | ), 39 | lint_msg, 40 | linter 41 | ) 42 | 43 | expect_lint( 44 | trim_some( 45 | " 46 | foo <- function(x) { 47 | return(x <<- x + 1) 48 | } 49 | " 50 | ), 51 | lint_msg, 52 | linter 53 | ) 54 | 55 | expect_lint( 56 | trim_some( 57 | " 58 | foo <- function(x) { 59 | return(x + 1 ->> x) 60 | } 61 | " 62 | ), 63 | lint_msg, 64 | linter 65 | ) 66 | 67 | expect_lint( 68 | trim_some( 69 | " 70 | foo <- function(x) { 71 | return(x + 1 -> x) 72 | } 73 | " 74 | ), 75 | lint_msg, 76 | linter 77 | ) 78 | 79 | side_effect_lines <- expect_lint( 80 | trim_some( 81 | " 82 | e <- new.env() 83 | foo <- function(x) { 84 | return(e$val <- x + 1) 85 | } 86 | " 87 | ), 88 | lint_msg, 89 | linter 90 | ) 91 | }) 92 | -------------------------------------------------------------------------------- /tests/testthat/test-outer_negation.R: -------------------------------------------------------------------------------- 1 | test_that("outer_negation_linter skips allowed usages", { 2 | linter <- outer_negation_linter() 3 | 4 | expect_no_lint("x <- any(y)", linter) 5 | expect_no_lint("y <- all(z)", linter) 6 | 7 | # extended usage of any is not covered 8 | expect_no_lint("any(!a & b)", linter) 9 | expect_no_lint("all(a | !b)", linter) 10 | 11 | expect_no_lint("any(a, b)", linter) 12 | expect_no_lint("all(b, c)", linter) 13 | expect_no_lint("any(!a, b)", linter) 14 | expect_no_lint("any(!!a)", linter) 15 | expect_no_lint("any(!!!a)", linter) 16 | expect_no_lint("all(a, !b)", linter) 17 | expect_no_lint("any(a, !b, na.rm = TRUE)", linter) 18 | # ditto when na.rm is passed quoted 19 | expect_no_lint("any(a, !b, 'na.rm' = TRUE)", linter) 20 | }) 21 | 22 | test_that("outer_negation_linter blocks simple disallowed usages", { 23 | linter <- outer_negation_linter() 24 | not_all_msg <- "!all(x) is better than any(!x)" 25 | not_any_msg <- "!any(x) is better than all(!x)" 26 | 27 | expect_lint("any(!x)", not_all_msg, linter) 28 | expect_lint("all(!foo(x))", not_any_msg, linter) 29 | # TODO: I rather keep it small for now 30 | # # na.rm doesn't change the recommendation 31 | # expect_lint("any(!x, na.rm = TRUE)", not_all_msg, linter) 32 | # also catch nested usage 33 | expect_lint("all(!(x + y))", not_any_msg, linter) 34 | # # catch when all inputs are negated 35 | # expect_lint("any(!x, !y)", not_all_msg, linter) 36 | # expect_lint("all(!x, !y, na.rm = TRUE)", not_any_msg, linter) 37 | }) 38 | 39 | test_that("outer_negation_linter doesn't trigger on empty calls", { 40 | linter <- outer_negation_linter() 41 | 42 | # minimal version of issue 43 | expect_no_lint("any()", linter) 44 | # closer to what was is practically relevant, as another regression test 45 | expect_no_lint("x %>% any()", linter) 46 | }) 47 | 48 | test_that("fix works", { 49 | expect_snapshot(fix_text("any(!x)\nall(!y)")) 50 | expect_snapshot(fix_text("any(!f(x))\nall(!f(x))")) 51 | }) 52 | -------------------------------------------------------------------------------- /tests/testthat/test-expect_true_false.R: -------------------------------------------------------------------------------- 1 | test_that("expect_true_false_linter skips allowed usages", { 2 | # expect_true is a scalar test; testing logical vectors with expect_equal is OK 3 | expect_no_lint("expect_equal(x, c(TRUE, FALSE))", expect_true_false_linter()) 4 | }) 5 | 6 | test_that("expect_true_false_linter blocks simple disallowed usages", { 7 | linter <- expect_true_false_linter() 8 | 9 | expect_lint( 10 | "expect_equal(foo(x), TRUE)", 11 | "expect_true(x) is better than expect_equal(x, TRUE)", 12 | linter 13 | ) 14 | 15 | expect_lint( 16 | "expect_equal(TRUE, foo(x))", 17 | "expect_true(x) is better than expect_equal(x, TRUE)", 18 | linter 19 | ) 20 | 21 | # expect_identical is treated the same as expect_equal 22 | expect_lint( 23 | "expect_identical(x, FALSE)", 24 | "expect_false(x) is better than expect_identical(x, FALSE)", 25 | linter 26 | ) 27 | 28 | # also caught when TRUE/FALSE is the first argument 29 | expect_lint( 30 | "expect_equal(TRUE, foo(x))", 31 | "expect_true(x) is better than expect_equal(x, TRUE)", 32 | linter 33 | ) 34 | }) 35 | 36 | test_that("lints vectorize", { 37 | linter <- expect_true_false_linter() 38 | expect_equal( 39 | nrow( 40 | lint_text( 41 | "{ 42 | expect_equal(x, TRUE) 43 | expect_equal(x, FALSE) 44 | }", 45 | linters = linter 46 | ) 47 | ), 48 | 2 49 | ) 50 | }) 51 | 52 | test_that("fix works", { 53 | linter <- expect_true_false_linter() 54 | 55 | expect_snapshot(fix_text("expect_equal(foo(x), TRUE)", linters = linter)) 56 | expect_snapshot(fix_text("expect_equal(foo(x), FALSE)", linters = linter)) 57 | 58 | expect_snapshot(fix_text("expect_identical(foo(x), TRUE)", linters = linter)) 59 | expect_snapshot(fix_text("expect_identical(foo(x), FALSE)", linters = linter)) 60 | 61 | expect_snapshot(fix_text("expect_equal(TRUE, foo(x))", linters = linter)) 62 | expect_snapshot(fix_text("expect_equal(FALSE, foo(x))", linters = linter)) 63 | }) 64 | -------------------------------------------------------------------------------- /tests/testthat/test-nested_ifelse.R: -------------------------------------------------------------------------------- 1 | test_that("nested_ifelse_linter skips allowed usages", { 2 | linter <- nested_ifelse_linter() 3 | 4 | expect_no_lint("if (TRUE) 1 else 2", linter) 5 | expect_no_lint("if (TRUE) 1 else if (TRUE) 2 else 3", linter) 6 | 7 | expect_no_lint("ifelse(runif(10) > .2, 4, 6)", linter) 8 | 9 | # don't block suggested alternatives 10 | expect_no_lint("fcase(l1, v1, l2, v2)", linter) 11 | expect_no_lint("case_when(l1 ~ v1, l2 ~ v2)", linter) 12 | }) 13 | 14 | test_that("nested_ifelse_linter blocks simple disallowed usages", { 15 | expect_lint( 16 | "ifelse(l1, v1, ifelse(l2, v2, v3))", 17 | "Don't use nested ifelse() calls", 18 | nested_ifelse_linter() 19 | ) 20 | 21 | expect_lint( 22 | "ifelse(l1, ifelse(l2, v1, v2), v3)", 23 | "Don't use nested ifelse() calls", 24 | nested_ifelse_linter() 25 | ) 26 | }) 27 | 28 | test_that("nested_ifelse_linter also catches dplyr::if_else", { 29 | expect_lint( 30 | "if_else(l1, v1, if_else(l2, v2, v3))", 31 | "Don't use nested if_else() calls", 32 | nested_ifelse_linter() 33 | ) 34 | 35 | # TODO 36 | # expect_lint( 37 | # "dplyr::if_else(l1, dplyr::if_else(l2, v1, v2), v3)", 38 | # "Don't use nested if_else() calls", 39 | # nested_ifelse_linter() 40 | # ) 41 | }) 42 | 43 | test_that("nested_ifelse_linter also catches data.table::fifelse", { 44 | expect_lint( 45 | "fifelse(l1, v1, fifelse(l2, v2, v3))", 46 | "Don't use nested fifelse() calls", 47 | nested_ifelse_linter() 48 | ) 49 | 50 | # TODO 51 | # expect_lint( 52 | # "data.table::fifelse(l1, v1, data.table::fifelse(l2, v2, v3))", 53 | # "Don't use nested fifelse() calls", 54 | # nested_ifelse_linter() 55 | # ) 56 | 57 | # TODO 58 | # not sure why anyone would do this, but the readability still argument holds 59 | # expect_lint( 60 | # "data.table::fifelse(l1, dplyr::if_else(l2, v1, v2), v3)", 61 | # rex::rex("Don't use nested if_else() calls"), 62 | # nested_ifelse_linter() 63 | # ) 64 | }) 65 | -------------------------------------------------------------------------------- /inst/rules/builtin/lengths.yml: -------------------------------------------------------------------------------- 1 | id: sapply_lengths-1 2 | language: r 3 | severity: warning 4 | rule: 5 | any: 6 | - pattern: sapply($MYVAR, length) 7 | - pattern: sapply(FUN = length, $MYVAR) 8 | - pattern: sapply($MYVAR, FUN = length) 9 | - pattern: vapply($MYVAR, length $$$) 10 | 11 | - pattern: map_dbl($MYVAR, length) 12 | - pattern: map_dbl($MYVAR, .f = length) 13 | - pattern: map_dbl(.f = length, $MYVAR) 14 | - pattern: map_int($MYVAR, length) 15 | - pattern: map_int($MYVAR, .f = length) 16 | - pattern: map_int(.f = length, $MYVAR) 17 | 18 | - pattern: purrr::map_dbl($MYVAR, length) 19 | - pattern: purrr::map_dbl($MYVAR, .f = length) 20 | - pattern: purrr::map_dbl(.f = length, $MYVAR) 21 | - pattern: purrr::map_int($MYVAR, length) 22 | - pattern: purrr::map_int($MYVAR, .f = length) 23 | - pattern: purrr::map_int(.f = length, $MYVAR) 24 | fix: lengths(~~MYVAR~~) 25 | message: Use lengths() to find the length of each element in a list. 26 | 27 | --- 28 | 29 | id: sapply_lengths-2 30 | language: r 31 | severity: warning 32 | rule: 33 | any: 34 | - pattern: $MYVAR |> sapply(length) 35 | - pattern: $MYVAR |> sapply(FUN = length) 36 | - pattern: $MYVAR |> vapply(length $$$) 37 | - pattern: $MYVAR |> map_int(length) 38 | - pattern: $MYVAR |> map_int(length $$$) 39 | - pattern: $MYVAR |> purrr::map_int(length) 40 | - pattern: $MYVAR |> purrr::map_int(length $$$) 41 | fix: ~~MYVAR~~ |> lengths() 42 | message: Use lengths() to find the length of each element in a list. 43 | 44 | --- 45 | 46 | id: sapply_lengths-3 47 | language: r 48 | severity: warning 49 | rule: 50 | any: 51 | - pattern: $MYVAR %>% sapply(length) 52 | - pattern: $MYVAR %>% sapply(FUN = length) 53 | - pattern: $MYVAR %>% vapply(length $$$) 54 | - pattern: $MYVAR %>% map_int(length) 55 | - pattern: $MYVAR %>% map_int(length $$$) 56 | - pattern: $MYVAR %>% purrr::map_int(length) 57 | - pattern: $MYVAR %>% purrr::map_int(length $$$) 58 | fix: ~~MYVAR~~ %>% lengths() 59 | message: Use lengths() to find the length of each element in a list. 60 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/condition_message.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("stop(paste0('a', 'b'))", linters = linter) 5 | Output 6 | Old code: stop(paste0('a', 'b')) 7 | New code: stop('a', 'b') 8 | 9 | --- 10 | 11 | Code 12 | fix_text("stop(paste0('a', 'b', collapse = 'e'))", linters = linter) 13 | 14 | --- 15 | 16 | Code 17 | fix_text("stop(paste0('a', 'b', recycle0 = 'e'))", linters = linter) 18 | 19 | --- 20 | 21 | Code 22 | fix_text("stop(paste0('a', 'b'), call. = FALSE)", linters = linter) 23 | Output 24 | Old code: stop(paste0('a', 'b'), call. = FALSE) 25 | New code: stop('a', 'b') 26 | 27 | --- 28 | 29 | Code 30 | fix_text("stop(paste0('a', 'b'), domain = FALSE)", linters = linter) 31 | Output 32 | Old code: stop(paste0('a', 'b'), domain = FALSE) 33 | New code: stop('a', 'b') 34 | 35 | --- 36 | 37 | Code 38 | fix_text("stop(domain = FALSE, paste0('a', 'b'))", linters = linter) 39 | Output 40 | Old code: stop(domain = FALSE, paste0('a', 'b')) 41 | New code: stop('a', 'b') 42 | 43 | --- 44 | 45 | Code 46 | fix_text("warning(paste0('a', 'b'))", linters = linter) 47 | Output 48 | Old code: warning(paste0('a', 'b')) 49 | New code: warning('a', 'b') 50 | 51 | --- 52 | 53 | Code 54 | fix_text("warning(paste0('a', 'b', collapse = 'e'))", linters = linter) 55 | 56 | --- 57 | 58 | Code 59 | fix_text("warning(paste0('a', 'b', sep = 'e'))", linters = linter) 60 | Output 61 | Old code: warning(paste0('a', 'b', sep = 'e')) 62 | New code: warning('a', 'b', sep = 'e') 63 | 64 | --- 65 | 66 | Code 67 | fix_text("warning(paste0('a', 'b'), immediate. = FALSE)", linters = linter) 68 | Output 69 | Old code: warning(paste0('a', 'b'), immediate. = FALSE) 70 | New code: warning('a', 'b') 71 | 72 | --- 73 | 74 | Code 75 | fix_text("warning(immediate. = FALSE, paste0('a', 'b'))", linters = linter) 76 | Output 77 | Old code: warning(immediate. = FALSE, paste0('a', 'b')) 78 | New code: warning('a', 'b') 79 | 80 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/redundant_ifelse.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("ifelse(x > 5, TRUE, FALSE)") 5 | Output 6 | Old code: ifelse(x > 5, TRUE, FALSE) 7 | New code: x > 5 8 | 9 | --- 10 | 11 | Code 12 | fix_text("if_else(x > 5, TRUE, FALSE)") 13 | Output 14 | Old code: if_else(x > 5, TRUE, FALSE) 15 | New code: x > 5 16 | 17 | --- 18 | 19 | Code 20 | fix_text("fifelse(x > 5, TRUE, FALSE)") 21 | Output 22 | Old code: fifelse(x > 5, TRUE, FALSE) 23 | New code: x > 5 24 | 25 | --- 26 | 27 | Code 28 | fix_text("ifelse(x > 5, FALSE, TRUE)") 29 | Output 30 | Old code: ifelse(x > 5, FALSE, TRUE) 31 | New code: !(x > 5) 32 | 33 | --- 34 | 35 | Code 36 | fix_text("if_else(x > 5, FALSE, TRUE)") 37 | Output 38 | Old code: if_else(x > 5, FALSE, TRUE) 39 | New code: !(x > 5) 40 | 41 | --- 42 | 43 | Code 44 | fix_text("fifelse(x > 5, FALSE, TRUE)") 45 | Output 46 | Old code: fifelse(x > 5, FALSE, TRUE) 47 | New code: !(x > 5) 48 | 49 | --- 50 | 51 | Code 52 | fix_text("ifelse(x > 5, 0, 1L)") 53 | Output 54 | Old code: ifelse(x > 5, 0, 1L) 55 | New code: as.integer(!(x > 5)) 56 | 57 | --- 58 | 59 | Code 60 | fix_text("if_else(x > 5, 0, 1L)") 61 | Output 62 | Old code: if_else(x > 5, 0, 1L) 63 | New code: as.integer(!(x > 5)) 64 | 65 | --- 66 | 67 | Code 68 | fix_text("fifelse(x > 5, 0, 1L)") 69 | Output 70 | Old code: fifelse(x > 5, 0, 1L) 71 | New code: as.integer(!(x > 5)) 72 | 73 | --- 74 | 75 | Code 76 | fix_text("ifelse(x > 5, 1L, 0L)") 77 | Output 78 | Old code: ifelse(x > 5, 1L, 0L) 79 | New code: as.integer(x > 5) 80 | 81 | --- 82 | 83 | Code 84 | fix_text("if_else(x > 5, 1L, 0L)") 85 | Output 86 | Old code: if_else(x > 5, 1L, 0L) 87 | New code: as.integer(x > 5) 88 | 89 | --- 90 | 91 | Code 92 | fix_text("fifelse(x > 5, 1L, 0L)") 93 | Output 94 | Old code: fifelse(x > 5, 1L, 0L) 95 | New code: as.integer(x > 5) 96 | 97 | -------------------------------------------------------------------------------- /tests/testthat/test-message.R: -------------------------------------------------------------------------------- 1 | test_that("no lints found message works", { 2 | skip_if_not_installed("withr") 3 | withr::local_envvar(list(TESTTHAT = FALSE, GITHUB_ACTIONS = FALSE)) 4 | dest <- tempfile(fileext = ".R") 5 | cat("1 + 1", file = dest) 6 | expect_snapshot(invisible(lint(dest))) 7 | }) 8 | 9 | test_that("found lint message works when no lint can be fixed", { 10 | skip_if_not_installed("withr") 11 | withr::local_envvar(list(TESTTHAT = FALSE, GITHUB_ACTIONS = FALSE)) 12 | temp_dir <- withr::local_tempdir() 13 | dest <- withr::local_tempfile(fileext = ".R", tmpdir = temp_dir) 14 | cat("x <<- 1", file = dest) 15 | expect_snapshot(invisible(lint(temp_dir))) 16 | }) 17 | 18 | test_that("found lint message works when lint can be fixed", { 19 | skip_if_not_installed("withr") 20 | withr::local_envvar(list(TESTTHAT = FALSE, GITHUB_ACTIONS = FALSE)) 21 | temp_dir <- withr::local_tempdir() 22 | dest <- withr::local_tempfile(fileext = ".R", tmpdir = temp_dir) 23 | cat("1 + 1\nany(is.na(1))\nany(duplicated(x))", file = dest) 24 | expect_snapshot(invisible(lint(temp_dir))) 25 | 26 | dest2 <- withr::local_tempfile(fileext = ".R", tmpdir = temp_dir) 27 | cat("1 + 1\nany(is.na(1))", file = dest) 28 | cat("any(duplicated(x))", file = dest2) 29 | expect_snapshot(invisible(lint(temp_dir))) 30 | }) 31 | 32 | test_that("no fixes needed message works", { 33 | skip_if_not_installed("withr") 34 | withr::local_envvar(list(TESTTHAT = FALSE, GITHUB_ACTIONS = FALSE)) 35 | dest <- tempfile(fileext = ".R") 36 | cat("1 + 1", file = dest) 37 | expect_snapshot(fix(dest)) 38 | }) 39 | 40 | test_that("fix needed message works", { 41 | skip_if_not_installed("withr") 42 | withr::local_envvar(list(TESTTHAT = FALSE, GITHUB_ACTIONS = FALSE)) 43 | temp_dir <- withr::local_tempdir() 44 | dest <- withr::local_tempfile(fileext = ".R", tmpdir = temp_dir) 45 | cat("1 + 1\nany(is.na(1))\nany(duplicated(x))", file = dest) 46 | expect_snapshot(fix(temp_dir)) 47 | 48 | dest2 <- withr::local_tempfile(fileext = ".R", tmpdir = temp_dir) 49 | cat("1 + 1\nany(is.na(1))", file = dest) 50 | cat("any(duplicated(x))", file = dest2) 51 | expect_snapshot(fix(temp_dir, force = TRUE)) 52 | }) 53 | -------------------------------------------------------------------------------- /inst/rules/builtin/expect_named.yml: -------------------------------------------------------------------------------- 1 | id: expect_named-1 2 | language: r 3 | severity: warning 4 | rule: 5 | any: 6 | - pattern: 7 | context: expect_identical(names($OBJ), $VALUES) 8 | strictness: ast 9 | - pattern: 10 | context: expect_identical($VALUES, names($OBJ)) 11 | strictness: ast 12 | constraints: 13 | VALUES: 14 | not: 15 | regex: ^(colnames\(|rownames\(|dimnames\(|NULL|names\() 16 | has: 17 | kind: null 18 | fix: expect_named(~~OBJ~~, ~~VALUES~~) 19 | message: expect_named(x, n) is better than expect_identical(names(x), n). 20 | 21 | --- 22 | 23 | id: expect_named-2 24 | language: r 25 | severity: warning 26 | rule: 27 | any: 28 | - pattern: 29 | context: expect_equal(names($OBJ), $VALUES) 30 | strictness: ast 31 | - pattern: 32 | context: expect_equal($VALUES, names($OBJ)) 33 | strictness: ast 34 | constraints: 35 | VALUES: 36 | not: 37 | regex: ^(colnames\(|rownames\(|dimnames\(|NULL|names\() 38 | fix: expect_named(~~OBJ~~, ~~VALUES~~) 39 | message: expect_named(x, n) is better than expect_equal(names(x), n). 40 | 41 | --- 42 | 43 | id: expect_named-3 44 | language: r 45 | severity: warning 46 | rule: 47 | any: 48 | - pattern: 49 | context: testthat::expect_identical(names($OBJ), $VALUES) 50 | strictness: ast 51 | - pattern: 52 | context: testthat::expect_identical($VALUES, names($OBJ)) 53 | strictness: ast 54 | constraints: 55 | VALUES: 56 | not: 57 | regex: ^(colnames\(|rownames\(|dimnames\(|NULL|names\() 58 | fix: testthat::expect_named(~~OBJ~~, ~~VALUES~~) 59 | message: expect_named(x, n) is better than expect_identical(names(x), n). 60 | 61 | --- 62 | 63 | id: expect_named-4 64 | language: r 65 | severity: warning 66 | rule: 67 | any: 68 | - pattern: 69 | context: testthat::expect_equal(names($OBJ), $VALUES) 70 | strictness: ast 71 | - pattern: 72 | context: testthat::expect_equal($VALUES, names($OBJ)) 73 | strictness: ast 74 | constraints: 75 | VALUES: 76 | not: 77 | regex: ^(colnames\(|rownames\(|dimnames\(|NULL|names\() 78 | fix: testthat::expect_named(~~OBJ~~, ~~VALUES~~) 79 | message: expect_named(x, n) is better than expect_equal(names(x), n). 80 | -------------------------------------------------------------------------------- /tests/testthat/test-expect_s4_class.R: -------------------------------------------------------------------------------- 1 | test_that("expect_s4_class_linter skips allowed usages", { 2 | linter <- expect_s4_class_linter() 3 | 4 | # expect_s4_class doesn't have an inverted version 5 | expect_no_lint("expect_true(!is(x, 'class'))", linter) 6 | # NB: also applies to tinytest, but it's sufficient to test testthat 7 | expect_no_lint("testthat::expect_s3_class(!is(x, 'class'))", linter) 8 | 9 | # expect_s4_class() doesn't have info= or label= arguments 10 | expect_no_lint( 11 | "expect_true(is(x, 'SpatialPoly'), info = 'x should be SpatialPoly')", 12 | linter 13 | ) 14 | expect_no_lint( 15 | "expect_true(is(x, 'SpatialPoly'), label = 'x inheritance')", 16 | linter 17 | ) 18 | }) 19 | 20 | test_that("expect_s4_class blocks simple disallowed usages", { 21 | linter <- expect_s4_class_linter() 22 | lint_msg <- "expect_s4_class(x, k) is better than expect_true(is(x, k))" 23 | 24 | expect_lint("expect_true(is(x, 'data.frame'))", lint_msg, linter) 25 | expect_lint( 26 | "expect_true(is(object = x, class2 = 'data.frame'))", 27 | lint_msg, 28 | linter 29 | ) 30 | expect_lint( 31 | "expect_true(is(class2 = 'data.frame', object = x))", 32 | lint_msg, 33 | linter 34 | ) 35 | # namespace qualification is irrelevant 36 | expect_lint( 37 | "testthat::expect_true(methods::is(x, 'SpatialPolygonsDataFrame'))", 38 | lint_msg, 39 | linter 40 | ) 41 | }) 42 | 43 | test_that("fix works", { 44 | linter <- expect_s4_class_linter() 45 | 46 | expect_snapshot(fix_text("expect_true(is(x, 's4class'))", linters = linter)) 47 | expect_snapshot(fix_text( 48 | "expect_true(methods::is(x, 's4class'))", 49 | linters = linter 50 | )) 51 | expect_snapshot(fix_text( 52 | "testthat::expect_true(is(x, 's4class'))", 53 | linters = linter 54 | )) 55 | expect_snapshot(fix_text( 56 | "testthat::expect_true(methods::is(x, 's4class'))", 57 | linters = linter 58 | )) 59 | expect_snapshot(fix_text( 60 | "testthat::expect_true(is(object = x, class2 = 's4class'))", 61 | linters = linter 62 | )) 63 | expect_snapshot(fix_text( 64 | "testthat::expect_true(is(class2 = 's4class', object = x))", 65 | linters = linter 66 | )) 67 | expect_snapshot(fix_text( 68 | "testthat::expect_true(is(class2 = 's4class', object = x))", 69 | linters = linter 70 | )) 71 | }) 72 | -------------------------------------------------------------------------------- /tests/testthat/test-expect_null.R: -------------------------------------------------------------------------------- 1 | test_that("expect_null_linter skips allowed usages", { 2 | linter <- expect_null_linter() 3 | 4 | # NB: this usage should be expect_false(), but this linter should 5 | # nevertheless apply (e.g. for maintainers not using expect_not_linter) 6 | expect_no_lint("expect_true(!is.null(x))", linter) 7 | # NB: also applies to tinytest, but it's sufficient to test testthat 8 | expect_no_lint("testthat::expect_true(!is.null(x))", linter) 9 | 10 | # length-0 could be NULL, but could be integer() or list(), so let it pass 11 | expect_no_lint("expect_length(x, 0L)", linter) 12 | 13 | # no false positive for is.null() at the wrong positional argument 14 | expect_no_lint("expect_true(x, is.null(y))", linter) 15 | }) 16 | 17 | test_that("expect_null_linter blocks simple disallowed usages", { 18 | linter <- expect_null_linter() 19 | 20 | expect_lint( 21 | "expect_equal(x, NULL)", 22 | "expect_null(x) is better than expect_equal(x, NULL)", 23 | linter 24 | ) 25 | 26 | # expect_identical is treated the same as expect_equal 27 | expect_lint( 28 | "expect_identical(x, NULL)", 29 | "expect_null(x) is better than expect_identical(x, NULL)", 30 | linter 31 | ) 32 | 33 | # reverse order lints the same 34 | expect_lint( 35 | "expect_equal(NULL, x)", 36 | "expect_null(x) is better than expect_equal(x, NULL)", 37 | linter 38 | ) 39 | 40 | # different equivalent usage 41 | expect_lint( 42 | "expect_true(is.null(foo(x)))", 43 | "expect_null(x) is better than expect_true(is.null(x))", 44 | linter 45 | ) 46 | }) 47 | 48 | test_that("lints vectorize", { 49 | expect_equal( 50 | nrow( 51 | lint_text( 52 | "{ 53 | expect_equal(x, NULL) 54 | expect_identical(x, NULL) 55 | expect_true(is.null(x)) 56 | }", 57 | linters = expect_null_linter() 58 | ) 59 | ), 60 | 3 61 | ) 62 | }) 63 | 64 | test_that("fix works", { 65 | linter <- expect_null_linter() 66 | 67 | expect_snapshot(fix_text("expect_identical(x, NULL)", linters = linter)) 68 | expect_snapshot(fix_text("expect_equal(x, NULL)", linters = linter)) 69 | expect_snapshot(fix_text("expect_true(is.null(x))", linters = linter)) 70 | 71 | expect_snapshot(fix_text("expect_identical(NULL, x)", linters = linter)) 72 | expect_snapshot(fix_text("expect_equal(NULL, x)", linters = linter)) 73 | }) 74 | -------------------------------------------------------------------------------- /inst/rules/builtin/expect_s4_class.yml: -------------------------------------------------------------------------------- 1 | # Note technically the second argument of `methods::is()` is "class2" not "class" 2 | # but we'll support both "class2" and the abbreviated "class" 3 | id: expect_s4_class-1 4 | language: r 5 | severity: warning 6 | rule: 7 | any: 8 | - pattern: expect_true(is(object = $X, class2 = $K)) 9 | - pattern: expect_true(methods::is(object = $X, class2 = $K)) 10 | - pattern: expect_true(is(object = $X, class = $K)) 11 | - pattern: expect_true(methods::is(object = $X, class = $K)) 12 | - pattern: expect_true(is(class2 = $K, object = $X)) 13 | - pattern: expect_true(methods::is(class2 = $K, object = $X)) 14 | - pattern: expect_true(is(class = $K, object = $X)) 15 | - pattern: expect_true(methods::is(class = $K, object = $X)) 16 | - pattern: expect_true(is($X, class2 = $K)) 17 | - pattern: expect_true(methods::is($X, class2 = $K)) 18 | - pattern: expect_true(is($X, class = $K)) 19 | - pattern: expect_true(methods::is($X, class = $K)) 20 | - pattern: expect_true(is($X, $K)) 21 | - pattern: expect_true(methods::is($X, $K)) 22 | fix: expect_s4_class(~~X~~, ~~K~~) 23 | message: expect_s4_class(x, k) is better than expect_true(is(x, k)). 24 | 25 | --- 26 | 27 | id: expect_s4_class-2 28 | language: r 29 | severity: warning 30 | rule: 31 | any: 32 | - pattern: testthat::expect_true(is(object = $X, class2 = $K)) 33 | - pattern: testthat::expect_true(methods::is(object = $X, class2 = $K)) 34 | - pattern: testthat::expect_true(is(object = $X, class = $K)) 35 | - pattern: testthat::expect_true(methods::is(object = $X, class = $K)) 36 | - pattern: testthat::expect_true(is(class2 = $K, object = $X)) 37 | - pattern: testthat::expect_true(methods::is(class2 = $K, object = $X)) 38 | - pattern: testthat::expect_true(is(class = $K, object = $X)) 39 | - pattern: testthat::expect_true(methods::is(class = $K, object = $X)) 40 | - pattern: testthat::expect_true(is($X, class2 = $K)) 41 | - pattern: testthat::expect_true(methods::is($X, class2 = $K)) 42 | - pattern: testthat::expect_true(is($X, class = $K)) 43 | - pattern: testthat::expect_true(methods::is($X, class = $K)) 44 | - pattern: testthat::expect_true(is($X, $K)) 45 | - pattern: testthat::expect_true(methods::is($X, $K)) 46 | fix: testthat::expect_s4_class(~~X~~, ~~K~~) 47 | message: expect_s4_class(x, k) is better than expect_true(is(x, k)). 48 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/literal_coercion.md: -------------------------------------------------------------------------------- 1 | # literal_coercion_linter blocks simple disallowed usages lgl, from int 2 | 3 | Code 4 | fix_text(sprintf("as.%s(%s)", out_type, input)) 5 | Output 6 | Old code: as.logical(1L) 7 | New code: TRUE 8 | 9 | # literal_coercion_linter blocks simple disallowed usages lgl, from num 10 | 11 | Code 12 | fix_text(sprintf("as.%s(%s)", out_type, input)) 13 | Output 14 | Old code: as.logical(1) 15 | New code: TRUE 16 | 17 | # literal_coercion_linter blocks simple disallowed usages lgl, from chr 18 | 19 | Code 20 | fix_text(sprintf("as.%s(%s)", out_type, input)) 21 | Output 22 | Old code: as.logical("true") 23 | New code: TRUE 24 | 25 | # literal_coercion_linter blocks simple disallowed usages int, from num 26 | 27 | Code 28 | fix_text(sprintf("as.%s(%s)", out_type, input)) 29 | Output 30 | Old code: as.integer(1) 31 | New code: 1L 32 | 33 | # literal_coercion_linter blocks simple disallowed usages num, from num 34 | 35 | Code 36 | fix_text(sprintf("as.%s(%s)", out_type, input)) 37 | Output 38 | Old code: as.numeric(1) 39 | New code: 1 40 | 41 | # literal_coercion_linter blocks simple disallowed usages dbl, from num 42 | 43 | Code 44 | fix_text(sprintf("as.%s(%s)", out_type, input)) 45 | Output 46 | Old code: as.double(1) 47 | New code: 1 48 | 49 | # literal_coercion_linter blocks simple disallowed usages int, from NA 50 | 51 | Code 52 | fix_text(sprintf("as.%s(%s)", out_type, input)) 53 | Output 54 | Old code: as.integer(NA) 55 | New code: NA_integer_ 56 | 57 | # literal_coercion_linter blocks simple disallowed usages num, from NA 58 | 59 | Code 60 | fix_text(sprintf("as.%s(%s)", out_type, input)) 61 | Output 62 | Old code: as.numeric(NA) 63 | New code: NA_real_ 64 | 65 | # literal_coercion_linter blocks simple disallowed usages dbl, from NA 66 | 67 | Code 68 | fix_text(sprintf("as.%s(%s)", out_type, input)) 69 | Output 70 | Old code: as.double(NA) 71 | New code: NA_real_ 72 | 73 | # literal_coercion_linter blocks simple disallowed usages chr, from NA 74 | 75 | Code 76 | fix_text(sprintf("as.%s(%s)", out_type, input)) 77 | Output 78 | Old code: as.character(NA) 79 | New code: NA_character_ 80 | 81 | -------------------------------------------------------------------------------- /tests/testthat/test-expect_length.R: -------------------------------------------------------------------------------- 1 | test_that("expect_length_linter skips allowed usages", { 2 | linter <- expect_length_linter() 3 | 4 | expect_no_lint("expect_equal(nrow(x), 4L)", linter) 5 | # NB: also applies to tinytest, but it's sufficient to test testthat 6 | expect_no_lint("expect_equal(nrow(x), 4L)", linter) 7 | 8 | # only check the first argument. yoda tests in the second argument will be 9 | # missed, but there are legitimate uses of length() in argument 2 10 | # expect_lint("expect_equal(nrow(x), length(y))", NULL, linter) 11 | 12 | # expect_length() doesn't have info= or label= arguments 13 | expect_no_lint( 14 | "expect_equal(length(x), n, info = 'x should have size n')", 15 | linter 16 | ) 17 | expect_no_lint("expect_equal(length(x), n, label = 'x size')", linter) 18 | expect_no_lint("expect_equal(length(x), length(y))", linter) 19 | expect_no_lint( 20 | "expect_equal(length(x), n, expected.label = 'target size')", 21 | linter 22 | ) 23 | }) 24 | 25 | test_that("expect_length_linter blocks simple disallowed usages", { 26 | linter <- expect_length_linter() 27 | lint_msg <- "expect_length(x, n) is better than expect_equal(length(x), n)" 28 | 29 | expect_lint("expect_equal(length(x), 2L)", lint_msg, linter) 30 | 31 | # yoda test cases 32 | expect_lint("expect_equal(2, length(x))", lint_msg, linter) 33 | expect_lint("expect_equal(2L, length(x))", lint_msg, linter) 34 | }) 35 | 36 | test_that("expect_length_linter blocks expect_identical usage as well", { 37 | expect_lint( 38 | "expect_identical(length(x), 2L)", 39 | "expect_length(x, n) is better than expect_identical(length(x), n)", 40 | expect_length_linter() 41 | ) 42 | }) 43 | 44 | test_that("lints vectorize", { 45 | expect_equal( 46 | nrow( 47 | lint_text( 48 | "{ 49 | expect_equal(length(x), n) 50 | expect_identical(length(x), n) 51 | }", 52 | linter = expect_length_linter() 53 | ) 54 | ), 55 | 2 56 | ) 57 | }) 58 | 59 | test_that("fix works", { 60 | linter <- expect_length_linter() 61 | 62 | expect_snapshot(fix_text("expect_equal(length(x), 2L)", linters = linter)) 63 | expect_snapshot(fix_text("expect_identical(length(x), 2L)", linters = linter)) 64 | 65 | expect_snapshot(fix_text("expect_equal(2L, length(x))", linters = linter)) 66 | expect_snapshot(fix_text("expect_identical(2L, length(x))", linters = linter)) 67 | }) 68 | -------------------------------------------------------------------------------- /tests/testthat/test-rep_len.R: -------------------------------------------------------------------------------- 1 | test_that("rep_len_linter skips allowed usages", { 2 | linter <- rep_len_linter() 3 | 4 | # only catch length.out usages 5 | expect_no_lint("rep(x, y)", linter) 6 | expect_no_lint("rep(1:10, 2)", linter) 7 | expect_no_lint("rep(1:10, 10:1)", linter) 8 | 9 | # usage of each is not compatible with rep_len; see ?rep. 10 | expect_no_lint("rep(x, each = 4, length.out = 50)", linter) 11 | # each is implicitly the 4th positional argument. a very strange usage 12 | # (because length.out is ignored), but doesn't hurt to catch it 13 | expect_no_lint("rep(a, b, length.out = c, d)", linter) 14 | # ditto for implicit length.out= 15 | expect_no_lint("rep(a, b, c, d)", linter) 16 | }) 17 | 18 | test_that("rep_len_linter blocks simple disallowed usages", { 19 | linter <- rep_len_linter() 20 | lint_msg <- "Use rep_len(x, n) instead of rep(x, length.out = n)." 21 | 22 | # only catch length.out usages 23 | expect_lint("rep(x, length.out = 4L)", lint_msg, linter) 24 | 25 | # implicit times= argument; length.out has priority over times= (see ?rep), 26 | # so we still lint since it's as if times= is not supplied. 27 | # (notice here that the base behavior is odd -- one might expect output like 28 | # head(rep(1:10, 10:1), 50), but instead we get rep(1:10, length.out = 50)) 29 | # 30 | # TODO: not convinced this is worth supporting 31 | # expect_lint("rep(1:10, 10:1, length.out = 50)", lint_msg, linter) 32 | 33 | # ditto for explicit times= argument 34 | # expect_lint("rep(1:10, times = 10:1, length.out = 50)", lint_msg, linter) 35 | 36 | # implicit usage in third argument 37 | # expect_lint("rep(1:10, 10:1, 50)", lint_msg, linter) 38 | }) 39 | 40 | test_that("vectorized lints work", { 41 | linter <- rep_len_linter() 42 | lint_msg <- "Use rep_len(x, n) instead of rep(x, length.out = n)." 43 | 44 | expect_equal( 45 | nrow( 46 | lint_text( 47 | trim_some( 48 | "{ 49 | rep(x, y) 50 | rep(1:10, length.out = 50) 51 | rep(x, each = 4, length.out = 50) 52 | rep(x, length.out = 50) 53 | }" 54 | ), 55 | linters = linter 56 | ) 57 | ), 58 | 2 59 | ) 60 | }) 61 | 62 | test_that("fix works", { 63 | linter <- rep_len_linter() 64 | expect_snapshot(fix_text("rep(1:3, length.out = 5)", linters = linter)) 65 | expect_snapshot(fix_text("rep(x, length.out = 5)", linters = linter)) 66 | }) 67 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(print,flir_fix) 4 | S3method(print,flir_lint) 5 | export(T_and_F_symbol_linter) 6 | export(add_new_rule) 7 | export(any_duplicated_linter) 8 | export(any_is_na_linter) 9 | export(class_equals_linter) 10 | export(condition_message_linter) 11 | export(double_assignment_linter) 12 | export(duplicate_argument_linter) 13 | export(empty_assignment_linter) 14 | export(equal_assignment_linter) 15 | export(equals_na_linter) 16 | export(expect_comparison_linter) 17 | export(expect_identical_linter) 18 | export(expect_length_linter) 19 | export(expect_named_linter) 20 | export(expect_not_linter) 21 | export(expect_null_linter) 22 | export(expect_s3_class_linter) 23 | export(expect_s4_class_linter) 24 | export(expect_true_false_linter) 25 | export(expect_type_linter) 26 | export(export_new_rule) 27 | export(fix) 28 | export(fix_dir) 29 | export(fix_package) 30 | export(fix_text) 31 | export(for_loop_index_linter) 32 | export(function_return_linter) 33 | export(implicit_assignment_linter) 34 | export(is_numeric_linter) 35 | export(length_levels_linter) 36 | export(length_test_linter) 37 | export(lengths_linter) 38 | export(library_call_linter) 39 | export(lint) 40 | export(lint_dir) 41 | export(lint_package) 42 | export(lint_text) 43 | export(list_comparison_linter) 44 | export(list_linters) 45 | export(literal_coercion_linter) 46 | export(matrix_apply_linter) 47 | export(missing_argument_linter) 48 | export(nested_ifelse_linter) 49 | export(numeric_leading_zero_linter) 50 | export(nzchar_linter) 51 | export(outer_negation_linter) 52 | export(package_hooks_linter) 53 | export(paste_linter) 54 | export(redundant_equals_linter) 55 | export(redundant_ifelse_linter) 56 | export(rep_len_linter) 57 | export(right_assignment_linter) 58 | export(sample_int_linter) 59 | export(seq_linter) 60 | export(setup_flir) 61 | export(setup_flir_gha) 62 | export(sort_linter) 63 | export(stopifnot_all_linter) 64 | export(todo_comment_linter) 65 | export(undesirable_function_linter) 66 | export(undesirable_operator_linter) 67 | export(unnecessary_nesting_linter) 68 | export(vector_logic_linter) 69 | export(which_grepl_linter) 70 | importFrom(data.table,":=") 71 | importFrom(data.table,.BY) 72 | importFrom(data.table,.EACHI) 73 | importFrom(data.table,.GRP) 74 | importFrom(data.table,.I) 75 | importFrom(data.table,.N) 76 | importFrom(data.table,.NGRP) 77 | importFrom(data.table,.SD) 78 | importFrom(data.table,data.table) 79 | -------------------------------------------------------------------------------- /tests/testthat/test-lengths.R: -------------------------------------------------------------------------------- 1 | test_that("NULL skips allowed usages", { 2 | linter <- lengths_linter() 3 | 4 | expect_no_lint("length(x)", linter) 5 | expect_no_lint("function(x) length(x) + 1L", linter) 6 | expect_no_lint("vapply(x, fun, integer(length(y)))", linter) 7 | expect_no_lint("sapply(x, sqrt, simplify = length(x))", linter) 8 | expect_no_lint("lapply(x, length)", linter) 9 | expect_no_lint("map(x, length)", linter) 10 | }) 11 | 12 | test_that("NULL blocks simple disallowed base usages", { 13 | linter <- lengths_linter() 14 | lint_msg <- "Use lengths() to find the length of each element in a list." 15 | 16 | expect_lint("sapply(x, length)", lint_msg, linter) 17 | expect_lint("sapply(x, FUN = length)", lint_msg, linter) 18 | expect_lint("sapply(FUN = length, x)", lint_msg, linter) 19 | 20 | expect_lint("vapply(x, length, integer(1L))", lint_msg, linter) 21 | }) 22 | 23 | test_that("NULL blocks simple disallowed purrr usages", { 24 | linter <- lengths_linter() 25 | lint_msg <- "Use lengths() to find the length of each element in a list." 26 | 27 | expect_lint("purrr::map_dbl(x, length)", lint_msg, linter) 28 | expect_lint("map_dbl(x, .f = length)", lint_msg, linter) 29 | expect_lint("map_dbl(.f = length, x)", lint_msg, linter) 30 | expect_lint("map_int(x, length)", lint_msg, linter) 31 | }) 32 | 33 | test_that("NULL blocks simple disallowed usages with pipes", { 34 | linter <- lengths_linter() 35 | lint_msg <- "Use lengths() to find the length of each element in a list." 36 | 37 | expect_lint("x |> sapply(length)", lint_msg, linter) 38 | expect_lint("x %>% sapply(length)", lint_msg, linter) 39 | 40 | expect_lint("x |> map_int(length)", lint_msg, linter) 41 | expect_lint("x %>% map_int(length)", lint_msg, linter) 42 | 43 | expect_lint("x |> purrr::map_int(length)", lint_msg, linter) 44 | expect_lint("x %>% purrr::map_int(length)", lint_msg, linter) 45 | }) 46 | 47 | test_that("fix works", { 48 | linter <- lengths_linter() 49 | 50 | expect_snapshot(fix_text("x |> sapply(length)", linters = linter)) 51 | expect_snapshot(fix_text("x %>% sapply(length)", linters = linter)) 52 | 53 | expect_snapshot(fix_text("x |> map_int(length)", linters = linter)) 54 | expect_snapshot(fix_text("x %>% map_int(length)", linters = linter)) 55 | 56 | expect_snapshot(fix_text("x |> purrr::map_int(length)", linters = linter)) 57 | expect_snapshot(fix_text("x %>% purrr::map_int(length)", linters = linter)) 58 | }) 59 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/expect_type.md: -------------------------------------------------------------------------------- 1 | # fix works 2 | 3 | Code 4 | fix_text("expect_equal(typeof(x), 'double')", linters = linter) 5 | Output 6 | Old code: expect_equal(typeof(x), 'double') 7 | New code: expect_type(x, 'double') 8 | 9 | --- 10 | 11 | Code 12 | fix_text("expect_equal(typeof(x), \"double\")", linters = linter) 13 | Output 14 | Old code: expect_equal(typeof(x), "double") 15 | New code: expect_type(x, "double") 16 | 17 | --- 18 | 19 | Code 20 | fix_text("expect_identical(typeof(x), 'double')", linters = linter) 21 | Output 22 | Old code: expect_identical(typeof(x), 'double') 23 | New code: expect_type(x, 'double') 24 | 25 | --- 26 | 27 | Code 28 | fix_text("expect_identical(typeof(x), \"double\")", linters = linter) 29 | Output 30 | Old code: expect_identical(typeof(x), "double") 31 | New code: expect_type(x, "double") 32 | 33 | --- 34 | 35 | Code 36 | fix_text("expect_equal('double', typeof(x))", linters = linter) 37 | Output 38 | Old code: expect_equal('double', typeof(x)) 39 | New code: expect_type(x, 'double') 40 | 41 | --- 42 | 43 | Code 44 | fix_text("expect_identical('double', typeof(x))", linters = linter) 45 | Output 46 | Old code: expect_identical('double', typeof(x)) 47 | New code: expect_type(x, 'double') 48 | 49 | --- 50 | 51 | Code 52 | fix_text("expect_true(is.call(x))", linters = linter) 53 | Output 54 | Old code: expect_true(is.call(x)) 55 | New code: expect_type(x, "language") 56 | 57 | --- 58 | 59 | Code 60 | fix_text("expect_true(is.function(x))", linters = linter) 61 | Output 62 | Old code: expect_true(is.function(x)) 63 | New code: expect_type(x, "closure") 64 | 65 | --- 66 | 67 | Code 68 | fix_text("expect_true(is.name(x))", linters = linter) 69 | Output 70 | Old code: expect_true(is.name(x)) 71 | New code: expect_type(x, "symbol") 72 | 73 | --- 74 | 75 | Code 76 | fix_text("expect_true(is.primitive(x))", linters = linter) 77 | Output 78 | Old code: expect_true(is.primitive(x)) 79 | New code: expect_type(x, "builtin") 80 | 81 | # no double replacement 82 | 83 | Code 84 | fix_text("expect_equal(typeof(x), 'double')") 85 | Output 86 | Old code: expect_equal(typeof(x), 'double') 87 | New code: expect_type(x, 'double') 88 | 89 | -------------------------------------------------------------------------------- /inst/rules/builtin/T_and_F_symbol.yml: -------------------------------------------------------------------------------- 1 | id: true_false_symbol 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: T 6 | kind: identifier 7 | not: 8 | any: 9 | - precedes: 10 | any: 11 | - pattern: <- 12 | - pattern: = 13 | - regex: ^~$ 14 | - regex: ^:$ 15 | - follows: 16 | any: 17 | - pattern: $ 18 | - regex: ^:$ 19 | - regex: ^~$ 20 | - inside: 21 | any: 22 | - kind: parameter 23 | - kind: call 24 | - kind: binary_operator 25 | follows: 26 | regex: ^~$ 27 | stopBy: end 28 | stopBy: 29 | kind: 30 | argument 31 | fix: TRUE 32 | message: Use TRUE instead of the symbol T. 33 | 34 | --- 35 | 36 | id: true_false_symbol-2 37 | language: r 38 | severity: warning 39 | rule: 40 | pattern: F 41 | kind: identifier 42 | not: 43 | any: 44 | - precedes: 45 | any: 46 | - pattern: <- 47 | - pattern: = 48 | - regex: ^~$ 49 | - regex: ^:$ 50 | - follows: 51 | any: 52 | - pattern: $ 53 | - regex: ^~$ 54 | - regex: ^:$ 55 | - inside: 56 | any: 57 | - kind: parameter 58 | - kind: call 59 | - kind: binary_operator 60 | follows: 61 | regex: ^~$ 62 | stopBy: end 63 | stopBy: 64 | kind: 65 | argument 66 | fix: FALSE 67 | message: Use FALSE instead of the symbol F. 68 | 69 | --- 70 | 71 | id: true_false_symbol-3 72 | language: r 73 | severity: warning 74 | rule: 75 | pattern: T 76 | kind: identifier 77 | precedes: 78 | any: 79 | - pattern: <- 80 | - pattern: = 81 | not: 82 | inside: 83 | kind: argument 84 | message: Don't use T as a variable name, as it can break code relying on T being TRUE. 85 | 86 | --- 87 | 88 | id: true_false_symbol-4 89 | language: r 90 | severity: warning 91 | rule: 92 | pattern: F 93 | kind: identifier 94 | precedes: 95 | any: 96 | - pattern: <- 97 | - pattern: = 98 | not: 99 | inside: 100 | kind: argument 101 | message: Don't use F as a variable name, as it can break code relying on F being FALSE. 102 | -------------------------------------------------------------------------------- /tests/testthat/test-equals_na.R: -------------------------------------------------------------------------------- 1 | test_that("equals_na_linter skips allowed usages", { 2 | linter <- equals_na_linter() 3 | 4 | expect_no_lint("blah", linter) 5 | expect_no_lint(" blah", linter) 6 | expect_no_lint(" blah", linter) 7 | expect_no_lint("x == 'NA'", linter) 8 | expect_no_lint("x <- NA", linter) 9 | expect_no_lint("x <- NaN", linter) 10 | expect_no_lint("x <- NA_real_", linter) 11 | expect_no_lint("is.na(x)", linter) 12 | expect_no_lint("is.nan(x)", linter) 13 | expect_no_lint("x[!is.na(x)]", linter) 14 | 15 | # equals_na_linter should ignore strings and comments 16 | expect_no_lint("is.na(x) # do not flag x == NA if inside a comment", linter) 17 | expect_no_lint("lint_msg <- 'do not flag x == NA if inside a string'", linter) 18 | 19 | # nested NAs are okay 20 | expect_no_lint("x==f(1, ignore = NA)", linter) 21 | }) 22 | 23 | skip_if_not_installed("tibble") 24 | patrick::with_parameters_test_that( 25 | "equals_na_linter blocks disallowed usages for all combinations of operators and types of NAs", 26 | expect_lint( 27 | paste("x", operation, type_na), 28 | "Use is.na for comparisons to NA (not == or !=)", 29 | NULL 30 | ), 31 | .cases = tibble::tribble( 32 | ~.test_name , ~operation , ~type_na , 33 | "equality, logical NA" , "==" , NA , 34 | "equality, integer NA" , "==" , "NA_integer_" , 35 | "equality, real NA" , "==" , "NA_real_" , 36 | "equality, complex NA" , "==" , "NA_complex_" , 37 | "equality, character NA" , "==" , "NA_character_" , 38 | "inequality, logical NA" , "!=" , NA , 39 | "inequality, integer NA" , "!=" , "NA_integer_" , 40 | "inequality, real NA" , "!=" , "NA_real_" , 41 | "inequality, complex NA" , "!=" , "NA_complex_" , 42 | "inequality, character NA" , "!=" , "NA_character_" 43 | ) 44 | ) 45 | 46 | test_that("equals_na_linter blocks disallowed usages in edge cases", { 47 | linter <- equals_na_linter() 48 | lint_msg <- "Use is.na for comparisons to NA (not == or !=)" 49 | 50 | # missing spaces around operators 51 | expect_lint("x==NA", lint_msg, linter) 52 | expect_lint("x!=NA", lint_msg, linter) 53 | 54 | # order doesn't matter 55 | expect_lint("NA == x", lint_msg, linter) 56 | 57 | # correct line number for multiline code 58 | expect_lint("x ==\nNA", lint_msg, linter) 59 | }) 60 | -------------------------------------------------------------------------------- /inst/rules/builtin/sort.yml: -------------------------------------------------------------------------------- 1 | id: sort-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: $OBJ[order($OBJ)] 6 | fix: sort(~~OBJ~~, na.last = TRUE) 7 | message: sort(~~OBJ~~, na.last = TRUE) is better than ~~OBJ~~[order(~~OBJ~~)]. 8 | 9 | --- 10 | 11 | id: sort-2 12 | language: r 13 | severity: warning 14 | rule: 15 | any: 16 | - pattern: $OBJ[order($OBJ, decreasing = $DECREASING)] 17 | - pattern: $OBJ[order(decreasing = $DECREASING, $OBJ)] 18 | constraints: 19 | DECREASING: 20 | regex: ^(TRUE|FALSE)$ 21 | fix: sort(~~OBJ~~, decreasing = ~~DECREASING~~, na.last = TRUE) 22 | message: | 23 | sort(~~OBJ~~, decreasing = ~~DECREASING~~, na.last = TRUE) is better than 24 | ~~OBJ~~[order(~~OBJ~~, decreasing = ~~DECREASING~~)]. 25 | 26 | --- 27 | 28 | id: sort-3 29 | language: r 30 | severity: warning 31 | rule: 32 | any: 33 | - pattern: $OBJ[order($OBJ, na.last = $NALAST)] 34 | - pattern: $OBJ[order(na.last = $NALAST, $OBJ)] 35 | constraints: 36 | NALAST: 37 | regex: ^(TRUE|FALSE)$ 38 | fix: sort(~~OBJ~~, na.last = ~~NALAST~~, na.last = TRUE) 39 | message: | 40 | sort(~~OBJ~~, na.last = ~~NALAST~~, na.last = TRUE) is better than 41 | ~~OBJ~~[order(~~OBJ~~, na.last = ~~NALAST~~)]. 42 | 43 | --- 44 | 45 | id: sort-4 46 | language: r 47 | severity: warning 48 | rule: 49 | any: 50 | - pattern: $OBJ[order($OBJ, decreasing = TRUE, na.last = FALSE)] 51 | - pattern: $OBJ[order($OBJ, na.last = FALSE, decreasing = TRUE)] 52 | - pattern: $OBJ[order(decreasing = TRUE, $OBJ, na.last = FALSE)] 53 | - pattern: $OBJ[order(decreasing = TRUE, na.last = FALSE, $OBJ)] 54 | - pattern: $OBJ[order(na.last = FALSE, decreasing = TRUE, $OBJ)] 55 | - pattern: $OBJ[order(na.last = FALSE, $OBJ, decreasing = TRUE)] 56 | fix: sort(~~OBJ~~, decreasing = TRUE, na.last = FALSE) 57 | message: | 58 | sort(~~OBJ~~, decreasing = TRUE, na.last = FALSE) is better than 59 | ~~OBJ~~[order(~~OBJ~~, na.last = FALSE, decreasing = TRUE)]. 60 | 61 | --- 62 | 63 | id: sort-5 64 | language: r 65 | severity: warning 66 | rule: 67 | any: 68 | - pattern: sort($OBJ) == $OBJ 69 | - pattern: $OBJ == sort($OBJ) 70 | fix: !is.unsorted(~~OBJ~~) 71 | message: | 72 | Use !is.unsorted(~~OBJ~~) to test the sortedness of a vector. 73 | 74 | --- 75 | 76 | id: sort-6 77 | language: r 78 | severity: warning 79 | rule: 80 | any: 81 | - pattern: sort($OBJ) != $OBJ 82 | - pattern: $OBJ != sort($OBJ) 83 | fix: is.unsorted(~~OBJ~~) 84 | message: | 85 | Use is.unsorted(~~OBJ~~) to test the unsortedness of a vector. 86 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/seq.md: -------------------------------------------------------------------------------- 1 | # fix 1:length(...) expressions 2 | 3 | Code 4 | fix_text("1:length(x)") 5 | Output 6 | Old code: 1:length(x) 7 | New code: seq_along(x) 8 | 9 | --- 10 | 11 | Code 12 | fix_text("1:nrow(x)") 13 | Output 14 | Old code: 1:nrow(x) 15 | New code: seq_len(nrow(x)) 16 | 17 | --- 18 | 19 | Code 20 | fix_text("1:ncol(x)") 21 | Output 22 | Old code: 1:ncol(x) 23 | New code: seq_len(ncol(x)) 24 | 25 | --- 26 | 27 | Code 28 | fix_text("1:NROW(x)") 29 | Output 30 | Old code: 1:NROW(x) 31 | New code: seq_len(NROW(x)) 32 | 33 | --- 34 | 35 | Code 36 | fix_text("1:NCOL(x)") 37 | Output 38 | Old code: 1:NCOL(x) 39 | New code: seq_len(NCOL(x)) 40 | 41 | --- 42 | 43 | Code 44 | fix_text("2:length(x)") 45 | 46 | --- 47 | 48 | Code 49 | fix_text("2:nrow(x)") 50 | 51 | --- 52 | 53 | Code 54 | fix_text("2:ncol(x)") 55 | 56 | --- 57 | 58 | Code 59 | fix_text("2:NROW(x)") 60 | 61 | --- 62 | 63 | Code 64 | fix_text("2:NCOL(x)") 65 | 66 | --- 67 | 68 | Code 69 | fix_text("2L:length(x)") 70 | 71 | --- 72 | 73 | Code 74 | fix_text("2L:nrow(x)") 75 | 76 | --- 77 | 78 | Code 79 | fix_text("2L:ncol(x)") 80 | 81 | --- 82 | 83 | Code 84 | fix_text("2L:NROW(x)") 85 | 86 | --- 87 | 88 | Code 89 | fix_text("2L:NCOL(x)") 90 | 91 | --- 92 | 93 | Code 94 | fix_text("mutate(x, .id = 1:n())") 95 | Output 96 | Old code: mutate(x, .id = 1:n()) 97 | New code: mutate(x, .id = seq_len(n())) 98 | 99 | # fix seq(...) expressions 100 | 101 | Code 102 | fix_text("seq(length(x))") 103 | Output 104 | Old code: seq(length(x)) 105 | New code: seq_along(x) 106 | 107 | --- 108 | 109 | Code 110 | fix_text("seq(nrow(x))") 111 | Output 112 | Old code: seq(nrow(x)) 113 | New code: seq_len(nrow(x)) 114 | 115 | --- 116 | 117 | Code 118 | fix_text("seq(1, 100)") 119 | Output 120 | Old code: seq(1, 100) 121 | New code: seq_len(100) 122 | 123 | --- 124 | 125 | Code 126 | fix_text("rev(seq(length(x)))") 127 | Output 128 | Old code: rev(seq(length(x))) 129 | New code: rev(seq_along(x)) 130 | 131 | --- 132 | 133 | Code 134 | fix_text("rev(seq(nrow(x)))") 135 | Output 136 | Old code: rev(seq(nrow(x))) 137 | New code: rev(seq_len(nrow(x))) 138 | 139 | -------------------------------------------------------------------------------- /vignettes/automatic_fixes.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Automatic fixes" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Automatic fixes} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>" 14 | ) 15 | ``` 16 | 17 | ```{r setup, echo=FALSE, message=FALSE} 18 | library(flir) 19 | ``` 20 | 21 | The main feature of `flir` is that it can provide automatic fixes for some linters. 22 | This is very useful to avoid the tedious job of fixing many files by hand but you may be worried about it applying wrong fixes. 23 | Moreover, since those fixes can be applied to several files at once, going back in time before those fixes were applied can be very difficult. 24 | `flir` provides two mechanisms to be more confident about automated fixes. 25 | 26 | ## Use Git 27 | 28 | By default, if `flir` would automatically fix several files and the project doesn't use Git for version control, a menu will appear recommending you to use Git. 29 | At this stage, you can still decide whether you want to apply the fixes or stop here without changing anything. 30 | 31 | Git makes it much easier to see changed files and it is therefore very recommended to use it in combination with `flir`. 32 | Similarly, if some files are unstaged (meaning that they have changes not covered by Git yet), another menu will ask you to confirm that you want to apply fixes. 33 | The reason is that for those unstaged files, it may be difficult to distinguish the new changes introduced by automated fixes from previous, unsaved changes. 34 | 35 | ## Interactively review all fixes 36 | 37 | Whether you use Git or not, you can also opt in for interactive review of changes using the `interactive` parameter in most of the `fix_*()` functions. 38 | Setting `interactive = TRUE` will open a Shiny application enabling you to see all the changes that *would be* introduced by `flir`. 39 | Those changes are not applied yet: you can decide whether you want to apply them or to skip them. 40 | If you have several files to review, accepting or skipping the changes for one file will automatically go to the next file. 41 | 42 | The video below shows that I have two files, `foo1.R` and `foo2.R`, both of which would be modified by automated fixes. 43 | Using `interactive = TRUE` allows me to decide whether I actually want fixes to be applied or not: 44 | 45 | ![A gif showing `fix()` applied on two files and opening a Shiny app to review changes. Changes for the first file are accepted but not those for the second file. Only the first file is modified.](demo.gif) 46 | -------------------------------------------------------------------------------- /tests/testthat/test-flint_ignore.R: -------------------------------------------------------------------------------- 1 | test_that("flir-ignore works for a single line", { 2 | expect_no_lint("# flir-ignore\nany(duplicated(x))", NULL) 3 | expect_fix("# flir-ignore\nany(duplicated(x))", character(0)) 4 | }) 5 | 6 | test_that("flir-ignore: specific rules work", { 7 | expect_no_lint("# flir-ignore: any_duplicated-1\nany(duplicated(x))", NULL) 8 | expect_fix( 9 | "# flir-ignore: any_duplicated-1\nany(duplicated(x))", 10 | character(0) 11 | ) 12 | 13 | expect_lint( 14 | "# flir-ignore: any_na-1\nany(duplicated(x))", 15 | "anyDuplicated(x, ...) > 0 is better than any(duplicated(x), ...).", 16 | NULL 17 | ) 18 | }) 19 | 20 | test_that("also ignore lines that have # nolint for compatibility", { 21 | expect_lint("# nolint\nany(duplicated(x))", NULL, NULL) 22 | expect_fix("# nolint\nany(duplicated(x))", character(0)) 23 | }) 24 | 25 | test_that("flir-ignore doesn't ignore more than one line", { 26 | expect_lint( 27 | "# flir-ignore\nany(duplicated(x))\nany(duplicated(y))", 28 | "anyDuplicated(x, ...) > 0 is better than any(duplicated(x), ...).", 29 | NULL 30 | ) 31 | expect_fix( 32 | "# flir-ignore\nany(duplicated(x))\nany(duplicated(y))", 33 | "# flir-ignore\nany(duplicated(x))\nanyDuplicated(y) > 0" 34 | ) 35 | }) 36 | 37 | test_that("flir-ignore-start and end work", { 38 | expect_no_lint( 39 | "# flir-ignore-start\nany(duplicated(x))\nany(duplicated(y))\n# flir-ignore-end", 40 | NULL 41 | ) 42 | expect_fix( 43 | "# flir-ignore-start\nany(duplicated(x))\nany(duplicated(y))\n# flir-ignore-end", 44 | character(0) 45 | ) 46 | }) 47 | 48 | test_that("flir-ignore-start and end work with specific rule", { 49 | expect_no_lint( 50 | "# flir-ignore-start: any_duplicated-1\nany(duplicated(x))\nany(duplicated(y))\n# flir-ignore-end", 51 | NULL 52 | ) 53 | expect_fix( 54 | "# flir-ignore-start: any_duplicated-1\nany(duplicated(x))\nany(duplicated(y))\n# flir-ignore-end", 55 | character(0) 56 | ) 57 | 58 | expect_lint( 59 | "# flir-ignore-start: any_na-1\nany(duplicated(x))\nany(duplicated(y))\n# flir-ignore-end", 60 | "anyDuplicated(x, ...) > 0 is better than any(duplicated(x), ...).", 61 | NULL 62 | ) 63 | }) 64 | 65 | test_that("flir-ignore-start and end error if mismatch", { 66 | expect_snapshot( 67 | lint_text("# flir-ignore-start\nany(duplicated(x))\nany(duplicated(y))"), 68 | error = TRUE 69 | ) 70 | 71 | expect_error( 72 | lint_text("any(duplicated(x))\nany(duplicated(y))\n# flir-ignore-end"), 73 | "Mismatch: the number of `start` patterns (0) and of `end` patterns (1) must be equal.", 74 | fixed = TRUE 75 | ) 76 | }) 77 | -------------------------------------------------------------------------------- /inst/rules/builtin/any_duplicated.yml: -------------------------------------------------------------------------------- 1 | id: any_duplicated-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: any($$$ duplicated($MYVAR) $$$) 6 | fix: anyDuplicated(~~MYVAR~~) > 0 7 | message: anyDuplicated(x, ...) > 0 is better than any(duplicated(x), ...). 8 | 9 | --- 10 | 11 | id: any_duplicated-2 12 | language: r 13 | severity: warning 14 | rule: 15 | any: 16 | - pattern: length(unique($MYVAR)) == length($MYVAR) 17 | - pattern: length($MYVAR) == length(unique($MYVAR)) 18 | fix: anyDuplicated(~~MYVAR~~) == 0L 19 | message: anyDuplicated(x) == 0L is better than length(unique(x)) == length(x). 20 | 21 | --- 22 | 23 | id: any_duplicated-3 24 | language: r 25 | severity: warning 26 | rule: 27 | pattern: length(unique($MYVAR)) != length($MYVAR) 28 | fix: anyDuplicated(~~MYVAR~~) != 0L 29 | message: | 30 | Use anyDuplicated(x) != 0L (or > or <) instead of length(unique(x)) != length(x) 31 | (or > or <). 32 | 33 | --- 34 | 35 | id: any_duplicated-4 36 | language: r 37 | severity: warning 38 | rule: 39 | any: 40 | - pattern: nrow($DATA) != length(unique($DATA$µCOL)) 41 | - pattern: length(unique($DATA$µCOL)) != nrow($DATA) 42 | fix: anyDuplicated(~~DATA~~$~~COL~~) != 0L 43 | message: | 44 | anyDuplicated(DF$col) != 0L is better than length(unique(DF$col)) != nrow(DF) 45 | 46 | --- 47 | 48 | # id: any_duplicated-5 49 | # language: r 50 | # severity: warning 51 | # rule: 52 | # any: 53 | # - pattern: 54 | # context: nrow($DATA) != length(unique($DATA[["µCOL"]])) 55 | # strictness: ast 56 | # - pattern: 57 | # context: length(unique($DATA[["µCOL"]])) != nrow($DATA) 58 | # strictness: ast 59 | # fix: anyDuplicated(~~DATA~~[["~~COL~~"]]) != 0L 60 | # message: | 61 | # anyDuplicated(DF[["col"]]) != 0L is better than length(unique(DF[["col"]])) != nrow(DF) 62 | # 63 | # --- 64 | 65 | id: any_duplicated-6 66 | language: r 67 | severity: warning 68 | rule: 69 | any: 70 | - pattern: nrow($DATA) == length(unique($DATA$µCOL)) 71 | - pattern: length(unique($DATA$µCOL)) == nrow($DATA) 72 | fix: anyDuplicated(~~DATA~~$~~COL~~) == 0L 73 | message: | 74 | anyDuplicated(DF$col) == 0L is better than length(unique(DF$col)) == nrow(DF) 75 | 76 | # --- 77 | # 78 | # id: any_duplicated-7 79 | # language: r 80 | # severity: warning 81 | # rule: 82 | # any: 83 | # - pattern: 84 | # context: nrow($DATA) == length(unique($DATA[["µCOL"]])) 85 | # strictness: ast 86 | # - pattern: 87 | # context: length(unique($DATA[["µCOL"]])) == nrow($DATA) 88 | # strictness: ast 89 | # fix: anyDuplicated(~~DATA~~[["~~COL~~"]]) == 0L 90 | # message: | 91 | # anyDuplicated(DF[["col"]]) == 0L is better than length(unique(DF[["col"]])) == nrow(DF) 92 | -------------------------------------------------------------------------------- /R/review_app.R: -------------------------------------------------------------------------------- 1 | # Taken from testthat:::review_app() 2 | # https://github.com/r-lib/testthat/blob/30f5b119875852005f2a02e6adf96276dbd1ef26/R/snapshot-manage.R#L45 3 | # MIT License 4 | review_app <- function(name, old_path, new_path) { 5 | stopifnot( 6 | length(name) == length(old_path), 7 | length(old_path) == length(new_path) 8 | ) 9 | n <- length(name) 10 | case_index <- stats::setNames(seq_along(name), name) 11 | skipped <- FALSE 12 | handled <- rep(FALSE, n) 13 | ui <- shiny::fluidPage( 14 | style = "margin: 0.5em", 15 | shiny::fluidRow( 16 | style = "display: flex", 17 | shiny::div( 18 | style = "flex: 1 1", 19 | shiny::selectInput("cases", NULL, case_index, width = "100%") 20 | ), 21 | shiny::div( 22 | class = "btn-group", 23 | style = "margin-left: 1em; flex: 0 0 auto", 24 | shiny::actionButton("skip", "Skip"), 25 | shiny::actionButton("accept", "Accept", class = "btn-success"), 26 | ) 27 | ), 28 | shiny::fluidRow(diffviewer::visual_diff_output("diff")) 29 | ) 30 | server <- function(input, output, session) { 31 | i <- shiny::reactive(as.numeric(input$cases)) 32 | output$diff <- diffviewer::visual_diff_render({ 33 | diffviewer::visual_diff(old_path[[i()]], new_path[[i()]]) 34 | }) 35 | shiny::observeEvent(input$accept, { 36 | cli::cli_inform(paste0( 37 | "Accepting snapshot: {.path {old_path[[i()]]}}" 38 | )) 39 | file.rename(new_path[[i()]], old_path[[i()]]) 40 | update_cases() 41 | }) 42 | shiny::observeEvent(input$skip, { 43 | handled[[i()]] <<- TRUE 44 | skipped <<- TRUE 45 | i <- next_case() 46 | shiny::updateSelectInput(session, "cases", selected = i) 47 | }) 48 | update_cases <- function() { 49 | handled[[i()]] <<- TRUE 50 | i <- next_case() 51 | shiny::updateSelectInput( 52 | session, 53 | "cases", 54 | choices = case_index[!handled], 55 | selected = i 56 | ) 57 | } 58 | next_case <- function() { 59 | if (all(handled)) { 60 | cli::cli_inform("Review complete") 61 | shiny::stopApp() 62 | return() 63 | } 64 | remaining <- case_index[!handled] 65 | next_cases <- which(remaining > i()) 66 | if (length(next_cases) == 0) { 67 | remaining[[1]] 68 | } else { 69 | remaining[[next_cases[[1]]]] 70 | } 71 | } 72 | } 73 | cli::cli_inform(c( 74 | "Starting Shiny app for snapshot review", 75 | i = "Use Escape or Ctrl+C to quit" 76 | )) 77 | shiny::runApp( 78 | shiny::shinyApp(ui, server), 79 | quiet = TRUE, 80 | launch.browser = shiny::paneViewer() 81 | ) 82 | skipped 83 | } 84 | -------------------------------------------------------------------------------- /inst/rules/builtin/seq.yml: -------------------------------------------------------------------------------- 1 | id: seq-1 2 | language: r 3 | severity: warning 4 | rule: 5 | pattern: seq(length($VAR)) 6 | fix: seq_along(~~VAR~~) 7 | message: | 8 | seq(length(...)) is likely to be wrong in the empty edge case. Use seq_along(...) instead. 9 | 10 | --- 11 | 12 | id: seq-2 13 | language: r 14 | severity: warning 15 | rule: 16 | any: 17 | - pattern: 1:nrow($VAR) 18 | - pattern: 1L:nrow($VAR) 19 | regex: ^1 20 | fix: seq_len(nrow(~~VAR~~)) 21 | message: | 22 | 1:nrow(...) is likely to be wrong in the empty edge case. Use seq_len(nrow(...)) instead. 23 | 24 | --- 25 | 26 | id: seq-3 27 | language: r 28 | severity: warning 29 | rule: 30 | any: 31 | - pattern: 1:n() 32 | - pattern: 1L:n() 33 | regex: ^1 34 | fix: seq_len(n()) 35 | message: | 36 | 1:n() is likely to be wrong in the empty edge case. Use seq_len(n()) instead. 37 | 38 | --- 39 | 40 | id: seq-4 41 | language: r 42 | severity: warning 43 | rule: 44 | pattern: seq(nrow($VAR)) 45 | fix: seq_len(nrow(~~VAR~~)) 46 | message: | 47 | seq(nrow(...)) is likely to be wrong in the empty edge case. Use seq_len(nrow(...)) instead. 48 | 49 | --- 50 | 51 | id: seq-5 52 | language: r 53 | severity: warning 54 | rule: 55 | any: 56 | - pattern: 1:length($VAR) 57 | - pattern: 1L:length($VAR) 58 | regex: ^1 59 | fix: seq_along(~~VAR~~) 60 | message: | 61 | 1:length(...) is likely to be wrong in the empty edge case. Use seq_along(...) instead. 62 | 63 | --- 64 | 65 | id: seq-6 66 | language: r 67 | severity: warning 68 | rule: 69 | any: 70 | - pattern: 1:ncol($VAR) 71 | - pattern: 1L:ncol($VAR) 72 | regex: ^1 73 | fix: seq_len(ncol(~~VAR~~)) 74 | message: | 75 | 1:ncol(...) is likely to be wrong in the empty edge case. Use seq_len(ncol(...)) instead. 76 | 77 | --- 78 | 79 | id: seq-7 80 | language: r 81 | severity: warning 82 | rule: 83 | any: 84 | - pattern: 1:NCOL($VAR) 85 | - pattern: 1L:NCOL($VAR) 86 | regex: ^1 87 | fix: seq_len(NCOL(~~VAR~~)) 88 | message: | 89 | 1:NCOL(...) is likely to be wrong in the empty edge case. Use seq_len(NCOL(...)) instead. 90 | 91 | --- 92 | 93 | id: seq-8 94 | language: r 95 | severity: warning 96 | rule: 97 | any: 98 | - pattern: 1:NROW($VAR) 99 | - pattern: 1L:NROW($VAR) 100 | regex: ^1 101 | fix: seq_len(NROW(~~VAR~~)) 102 | message: | 103 | 1:NROW(...) is likely to be wrong in the empty edge case. Use seq_len(NROW(...)) instead. 104 | 105 | 106 | --- 107 | 108 | id: seq-9 109 | language: r 110 | severity: warning 111 | rule: 112 | pattern: seq(1, $VAL) 113 | not: 114 | pattern: seq(1, 0) 115 | constraints: 116 | VAL: 117 | regex: ^\d+(|L)$ 118 | fix: seq_len(~~VAL~~) 119 | message: seq_len(~~VAL~~) is more efficient than seq(1, ~~VAL~~). 120 | 121 | 122 | --------------------------------------------------------------------------------