Skip to content

Instantly share code, notes, and snippets.

@klmr
Last active July 11, 2024 08:52
Show Gist options
  • Save klmr/86752f3bee5ee35048eb90b6c7b2bb7c to your computer and use it in GitHub Desktop.
Save klmr/86752f3bee5ee35048eb90b6c7b2bb7c to your computer and use it in GitHub Desktop.
Alternative R linters

A portable configuration for the ‘lintr’ R package that includes a handful of custom linters that I commonly use.

arrow_assignment_linter = function () {
xpath = '//LEFT_ASSIGN[text() != ":="] | //RIGHT_ASSIGN'
lint_message_fmt = 'Use =, not %s, for assignment.'
lintr::Linter(\(source_expression) {
if (! lintr::is_lint_level(source_expression, 'expression')) {
return(list())
}
xml = source_expression$xml_parsed_content
bad_expr = xml2::xml_find_all(xml, xpath)
if (length(bad_expr) == 0L) {
return(list())
}
operator = xml2::xml_text(bad_expr)
lint_message = sprintf(lint_message_fmt, operator)
lintr::xml_nodes_to_lints(bad_expr, source_expression, lint_message, type = 'style')
})
}
linters: {
config_dir = dirname(file)
lapply(dir(config_dir, '\\.[rR]$', full.names = TRUE), source, local = environment())
lintr::linters_with_defaults(
assignment_linter = arrow_assignment_linter(),
function_left_parentheses_linter = function_definition_linter(),
line_length_linter = line_length_linter(120L),
object_usage_linter = NULL, # unusably buggy
quotes_linter = lintr::quotes_linter("'")
)
}
function_definition_linter = function () {
bad_line_fun_xpath = '(//FUNCTION | //OP-LAMBDA)[@line1 != following-sibling::OP-LEFT-PAREN/@line1]'
bad_line_call_xpath = '//SYMBOL_FUNCTION_CALL[@line1 != parent::expr/following-sibling::OP-LEFT-PAREN/@line1]'
bad_col_fun_xpath = '//FUNCTION[
@line1 = following-sibling::OP-LEFT-PAREN/@line1
and @col2 != following-sibling::OP-LEFT-PAREN/@col1 - 2
]'
bad_col_call_xpath = '//SYMBOL_FUNCTION_CALL[
line1 = parent::expr/following-sibling::OP-LEFT-PAREN/@line1
and @col2 != parent::expr/following-sibling::OP-LEFT-PAREN/@col1 - 1
]'
lintr::Linter(\(source_expression) {
if (! lintr::is_lint_level(source_expression, 'expression')) {
return(list())
}
xml = source_expression$xml_parsed_content
bad_line_fun_exprs = xml2::xml_find_all(xml, bad_line_fun_xpath)
bad_line_fun_lints = lintr::xml_nodes_to_lints(
bad_line_fun_exprs,
source_expression = source_expression,
lint_message = 'Left parenthesis should be on the same line as the \'function\' symbol.'
)
bad_line_call_exprs = xml2::xml_find_all(xml, bad_line_call_xpath)
bad_line_call_lints = lintr::xml_nodes_to_lints(
bad_line_call_exprs,
source_expression = source_expression,
lint_message = 'Left parenthesis should be on the same line as the function\'s symbol.'
)
bad_col_fun_exprs = xml2::xml_find_all(xml, bad_col_fun_xpath)
bad_col_fun_lints = lintr::xml_nodes_to_lints(
bad_col_fun_exprs,
source_expression = source_expression,
lint_message = 'Add spaces before the left parenthesis in a function definition.',
range_start_xpath = 'number(./@col2 + 1)',
range_end_xpath = 'number(./following-sibling::OP-LEFT-PAREN/@col1)'
)
bad_col_call_exprs = xml2::xml_find_all(xml, bad_col_call_xpath)
bad_col_call_lints = lintr::xml_nodes_to_lints(
bad_col_call_exprs,
source_expression = source_expression,
lint_message = 'Remove spaces before the left parenthesis in a function call.',
range_start_xpath = 'number(./@col2 + 1)',
range_end_xpath = 'number(./parent::expr/following-sibling::OP-LEFT-PAREN/@col1 - 1)'
)
c(bad_line_fun_lints, bad_line_call_lints, bad_col_fun_lints, bad_col_call_lints)
})
}
line_length_linter = function (length = 120L) {
general_msg = paste('Lines should not be more than', length, 'characters.')
lintr::Linter(\(source_expression) {
if (! lintr::is_lint_level(source_expression, 'file')) {
return(list())
}
# Note that this will handle “comment-looking” lines in multi-line strings incorrectly. But that’s fine.
comment_lines = grep('^\\s*#', source_expression$file_lines)
line_lengths = nchar(source_expression$file_lines)
long_lines = setdiff(which(line_lengths > length), comment_lines)
Map(
\(long_line, line_length) {
lintr::Lint(
filename = source_expression$filename,
line_number = long_line,
column_number = length + 1L,
type = 'style',
message = paste(general_msg, 'This line is', line_length, 'characters.'),
line = source_expression$file_lines[long_line],
ranges = list(c(1L, line_length))
)
},
long_lines,
line_lengths[long_lines]
)
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment