diff --git a/checks/AvoidContainersWithAbstractTypes.jl b/checks/AvoidContainersWithAbstractTypes.jl index f5d4046..f29021e 100644 --- a/checks/AvoidContainersWithAbstractTypes.jl +++ b/checks/AvoidContainersWithAbstractTypes.jl @@ -38,20 +38,22 @@ const ABSTRACT_NUMBER_TYPES = Set([ ]) struct Check<:Analysis.Check end -id(::Check) = "avoid-containers-with-abstract-types" -severity(::Check) = 6 -synopsis(::Check) = "Avoid containers with abstract types." +Analysis.id(::Check) = "avoid-containers-with-abstract-types" +Analysis.severity(::Check) = 6 +Analysis.synopsis(::Check) = "Avoid containers with abstract types." -function init(this::Check, ctxt::AnalysisContext)::Nothing - register_syntaxnode_action(ctxt, is_container, n -> check(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, _is_container, n -> _check(this, ctxt, n)) return nothing end -# Structure of a typical node we want to check here: -# (= num_vector (ref Real 1.0 2 3)) -# We want to check the type right-hand side of the assignment. -# For some invocations, this is also wrapped inside a call (eg. list comprehensions) -function is_container(node::SyntaxNode)::Bool +#= +Structure of a typical node we want to check here: +(= num_vector (ref Real 1.0 2 3)) +We want to check the type right-hand side of the assignment. +For some invocations, this is also wrapped inside a call (eg. list comprehensions) +=# +function _is_container(node::SyntaxNode)::Bool if !is_assignment(node) || numchildren(node) < 2 return false end @@ -59,7 +61,7 @@ function is_container(node::SyntaxNode)::Bool return !is_leaf(rhs) && kind(rhs) in KSet"ref call curly" end -function check(this::Check, ctxt::AnalysisContext, node::SyntaxNode)::Nothing +function _check(this::Check, ctxt::AnalysisContext, node::SyntaxNode)::Nothing assignment_rhs = children(node)[2] id_type_node = _get_identifier_node_to_check(assignment_rhs) if !isnothing(id_type_node) @@ -76,11 +78,13 @@ function check(this::Check, ctxt::AnalysisContext, node::SyntaxNode)::Nothing return nothing end -# Curly braces notations get translated like this: -# - Array{Number}[] => (curly Array Number) -# - Array{Array}{Number}[] => (curly Array (curly Array Number)) -# As such, to find the type of multidimensional arrays, it's convenient to be able -# to walk down the tree until an identifier is found. +#= +Curly braces notations get translated like this: +- Array{Number}[] => (curly Array Number) +- Array{Array}{Number}[] => (curly Array (curly Array Number)) +As such, to find the type of multidimensional arrays, it's convenient to be able +to walk down the tree until an identifier is found. +=# function _get_identifier_node_to_check(node::SyntaxNode)::NullableNode while _search_further(node) node = _get_next_search_node(node) diff --git a/checks/AvoidCreatingEmptyArraysAndVectors.jl b/checks/AvoidCreatingEmptyArraysAndVectors.jl index d92ae92..21428fc 100644 --- a/checks/AvoidCreatingEmptyArraysAndVectors.jl +++ b/checks/AvoidCreatingEmptyArraysAndVectors.jl @@ -6,16 +6,16 @@ using ...SymbolTable: node_is_declaration_of_variable include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "avoid-creating-empty-arrays-and-vectors" -severity(::Check) = 8 -synopsis(::Check) = "Avoid resizing arrays after initialization." +Analysis.id(::Check) = "avoid-creating-empty-arrays-and-vectors" +Analysis.severity(::Check) = 8 +Analysis.synopsis(::Check) = "Avoid resizing arrays after initialization." -function init(this::Check, ctxt::AnalysisContext)::Nothing - register_syntaxnode_action(ctxt, is_assignment, n -> check(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, is_assignment, n -> _check(this, ctxt, n)) return nothing end -function check(this::Check, ctxt::AnalysisContext, assignment_node::SyntaxNode)::Nothing +function _check(this::Check, ctxt::AnalysisContext, assignment_node::SyntaxNode)::Nothing if ! node_is_declaration_of_variable(ctxt.symboltable, first(children(assignment_node))) return end diff --git a/checks/AvoidExtraneousWhitespaceBetweenOpenAndCloseCharacters.jl b/checks/AvoidExtraneousWhitespaceBetweenOpenAndCloseCharacters.jl index e6ebc81..764abc9 100644 --- a/checks/AvoidExtraneousWhitespaceBetweenOpenAndCloseCharacters.jl +++ b/checks/AvoidExtraneousWhitespaceBetweenOpenAndCloseCharacters.jl @@ -6,9 +6,11 @@ using ...Properties: is_toplevel include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "avoid-extraneous-whitespace-between-open-and-close-characters" -severity(::Check) = 7 -synopsis(::Check) = "Avoid extraneous whitespace inside parentheses, square brackets or braces." +Analysis.id(::Check) = "avoid-extraneous-whitespace-between-open-and-close-characters" +Analysis.severity(::Check) = 7 +function Analysis.synopsis(::Check)::String + return "Avoid extraneous whitespace inside parentheses, square brackets or braces." +end """ Syntax node types for which whitespace should be checked. @@ -100,7 +102,7 @@ function _check(this::Check, ctxt::AnalysisContext, sf::SourceFile)::Nothing location_msg = " before '$next_leaf_text'" else expected_spaces = 1 # Exactly one space between elements - location_msg = " between '$prev_leaf_text' and '$next_leaf_text'" + location_msg = " between '$prev_leaf_text' and '$next_leaf_text'" end if !isnothing(expected_spaces) && length(cur.range) != expected_spaces @@ -111,7 +113,7 @@ function _check(this::Check, ctxt::AnalysisContext, sf::SourceFile)::Nothing return nothing end -function init(this::Check, ctxt::AnalysisContext)::Nothing +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, is_toplevel, root -> _check(this, ctxt, root.source)) return nothing end diff --git a/checks/AvoidGlobalVariables.jl b/checks/AvoidGlobalVariables.jl index 5243491..2455d0e 100644 --- a/checks/AvoidGlobalVariables.jl +++ b/checks/AvoidGlobalVariables.jl @@ -1,8 +1,8 @@ module AvoidGlobalVariables using ...Properties: is_global_decl, is_constant, find_lhs_of_kind -using ...SyntaxNodeHelpers using ...SymbolTable +using ...SyntaxNodeHelpers include("_common.jl") @@ -10,11 +10,11 @@ struct Check<:Analysis.Check already_reported::Set{SyntaxNode} Check() = new(Set{SyntaxNode}()) end -id(::Check) = "avoid-global-variables" -severity(::Check) = 3 -synopsis(::Check) = "Avoid global variables when possible" +Analysis.id(::Check) = "avoid-global-variables" +Analysis.severity(::Check) = 3 +Analysis.synopsis(::Check) = "Avoid global variables when possible" -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, is_global_decl, node -> begin id = find_lhs_of_kind(K"Identifier", node) if isnothing(id) @@ -35,6 +35,7 @@ function init(this::Check, ctxt::AnalysisContext) report_violation(ctxt, this, id, synopsis(this)) return nothing end) + return nothing end end # module AvoidGlobalVariables diff --git a/checks/AvoidHardCodedNumbers.jl b/checks/AvoidHardCodedNumbers.jl index 154e50d..8626f5e 100644 --- a/checks/AvoidHardCodedNumbers.jl +++ b/checks/AvoidHardCodedNumbers.jl @@ -4,25 +4,52 @@ using ...Properties: get_number, is_constant, is_global_decl, is_literal_number include("_common.jl") +""" Positive powers of 10 (up till 10^18) """ +const POWERS_OF_TEN_POSITIVE = Set{Int64}([10^i for i in 1:18]) + +""" Negative powers of 10 (up till -10^18) """ +const POWERS_OF_TEN_NEGATIVE = Set{Int64}([-i for i in POWERS_OF_TEN_POSITIVE]) + +""" Positive and negative powers of 10 """ +const POWERS_OF_TEN = POWERS_OF_TEN_POSITIVE ∪ POWERS_OF_TEN_NEGATIVE + +""" Positive powers of 2 (up till 2^20) """ +const POWERS_OF_TWO = Set{Int64}([2^i for i in 1:20]) + +""" Integers with special meaning, such as number of seconds, degrees, etc . """ +const SOME_SPECIAL_INTS = Set{Int64}([0, 1, + 60, # minutes, seconds + 90, 180, 270, 360 # degrees + ]) + +""" Integers that are not considered magical and may appear as constants. """ +const KNOWN_INTS = SOME_SPECIAL_INTS ∪ POWERS_OF_TEN ∪ POWERS_OF_TWO + +""" Floats that are not considered magical and may appear as constants. """ +const KNOWN_FLOATS = Set{Float64}([0.1, 0.01, 0.001, 0.0001, 0.5]) ∪ + Set{Float64}(convert.(Float64, POWERS_OF_TEN)) ∪ + Set{Float64}(convert.(Float64, SOME_SPECIAL_INTS)) + struct Check<:Analysis.Check - seen_before + seen_before::Set{Number} # FIXME Fine for integers but, for floats, we should # probably use a tolerance to compare them. Check() = new(Set{Number}()) end -id(::Check) = "avoid-hard-coded-numbers" -severity(::Check) = 3 -synopsis(::Check) = "Avoid hard-coded numbers" +Analysis.id(::Check) = "avoid-hard-coded-numbers" +Analysis.severity(::Check) = 3 +Analysis.synopsis(::Check) = "Avoid hard-coded numbers" -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, is_literal_number, n -> check(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, is_literal_number, n -> _check(this, ctxt, n)) + return nothing end # Also FIXME: should I use all the 64 bits versions of the types? -function check(this::Check, ctxt::AnalysisContext, node::SyntaxNode) +function _check(this::Check, ctxt::AnalysisContext, node::SyntaxNode)::Nothing @assert is_literal_number(node) "Expected a node with a literal number, got $(kind(node))" - if !is_const_declaration(node) && !in_array_assignment(node) && is_magic_number(node) + if !_is_const_declaration(node) && !_in_array_assignment(node) && _is_magic_number(node) n = get_number(node) if n ∈ this.seen_before report_violation(ctxt, this, node, "Hard-coded number '$n' should be a const variable.") @@ -30,6 +57,7 @@ function check(this::Check, ctxt::AnalysisContext, node::SyntaxNode) push!(this.seen_before, n) end end + return nothing end """ @@ -39,7 +67,7 @@ Check if the literal number is part of a constant declaration. To that end, we climb up the tree until we find a constant declaration, or the root. """ -function is_const_declaration(node::SyntaxNode)::Bool +function _is_const_declaration(node::SyntaxNode)::Bool x = node while !(isnothing(x) || is_constant(x)) x = x.parent @@ -47,35 +75,20 @@ function is_const_declaration(node::SyntaxNode)::Bool return !isnothing(x) end -function in_array_assignment(node::SyntaxNode)::Bool +function _in_array_assignment(node::SyntaxNode)::Bool p = node.parent return !isnothing(p) && kind(p) == K"vect" end # TODO Add (unit?) tests -## Magic numbers -# Integers -const POWERS_OF_TEN_POSITIVE = Set{Int64}([10^i for i in 1:18]) -const POWERS_OF_TEN_NEGATIVE = Set{Int64}([-i for i in POWERS_OF_TEN_POSITIVE]) -const POWERS_OF_TEN = POWERS_OF_TEN_POSITIVE ∪ POWERS_OF_TEN_NEGATIVE -const POWERS_OF_TWO = Set{Int64}([2^i for i in 1:20]) -const SOME_SPECIAL_INTS = Set{Int64}([0, 1, - 60, # minutes, seconds - 90, 180, 270, 360 # degrees - ]) -const KNOWN_INTS = SOME_SPECIAL_INTS ∪ POWERS_OF_TEN ∪ POWERS_OF_TWO -# Floats -const KNOWN_FLOATS = Set{Float64}([0.1, 0.01, 0.001, 0.0001, 0.5]) ∪ - Set{Float64}(convert.(Float64, POWERS_OF_TEN)) ∪ - Set{Float64}(convert.(Float64, SOME_SPECIAL_INTS)) """ is_magic_number(node::SyntaxNode)::Bool Check if the given literal is a magic number, i.e., it is not a "usual number", i.e., one usually found in initializations. """ -function is_magic_number(node::SyntaxNode)::Bool +function _is_magic_number(node::SyntaxNode)::Bool n = get_number(node) return !isnothing(n) && ( kind(node) == K"Float" ? n ∉ KNOWN_FLOATS : n ∉ KNOWN_INTS diff --git a/checks/ConsistentLineEndings.jl b/checks/ConsistentLineEndings.jl index 450cd56..8fcd2aa 100644 --- a/checks/ConsistentLineEndings.jl +++ b/checks/ConsistentLineEndings.jl @@ -6,12 +6,12 @@ using ...Properties: is_toplevel include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "consistent-line-endings" -severity(::Check) = 7 -synopsis(::Check) = "Make sure that the line endings are consistent within a file" +Analysis.id(::Check) = "consistent-line-endings" +Analysis.severity(::Check) = 7 +Analysis.synopsis(::Check) = "Make sure that the line endings are consistent within a file" -function init(this::Check, ctxt::AnalysisContext)::Nothing +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, is_toplevel, n -> _check(this, ctxt, n)) return nothing end diff --git a/checks/DoNotChangeGeneratedIndices.jl b/checks/DoNotChangeGeneratedIndices.jl index 436b87d..ddf630f 100644 --- a/checks/DoNotChangeGeneratedIndices.jl +++ b/checks/DoNotChangeGeneratedIndices.jl @@ -1,39 +1,41 @@ module DoNotChangeGeneratedIndices using ...Properties: first_child, get_assignee, get_iteration_parts, - is_assignment, is_flow_cntrl, is_range + is_assignment, is_flow_cntrl, is_range include("_common.jl") - + struct Check<:Analysis.Check end -id(::Check) = "do-not-change-generated-indices" -severity(::Check) = 5 -synopsis(::Check) = "Do not change generated indices" +Analysis.id(::Check) = "do-not-change-generated-indices" +Analysis.severity(::Check) = 5 +Analysis.synopsis(::Check) = "Do not change generated indices" -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, n -> kind(n) == K"for", n -> checkForLoop(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, n -> kind(n) == K"for", n -> _check_for_loop(this, ctxt, n)) + return nothing end -function checkForLoop(this::Check, ctxt::AnalysisContext, for_loop::SyntaxNode) +function _check_for_loop(this::Check, ctxt::AnalysisContext, for_loop::SyntaxNode)::Nothing loop_var, iter_expr = get_iteration_parts(for_loop) if isnothing(loop_var) || isnothing(iter_expr) return nothing end - var_name = loop_var_to_string(loop_var) + var_name = _loop_var_to_string(loop_var) if is_range(iter_expr) || ( kind(iter_expr) == K"call" && kind(first_child(iter_expr)) == K"Identifier" && string(first_child(iter_expr)) ∈ ["eachindex", "enumerate", "axes"] - ) + ) # Look into the loop's body to see if `loop_var` is modified. @assert numchildren(for_loop) == 2 && kind(children(for_loop)[2]) == K"block" "An empty loop or what? $for_loop" body = children(for_loop)[2] - frisk_for_modification(this, ctxt, body, var_name) + _frisk_for_modification(this, ctxt, body, var_name) end + return nothing end -function loop_var_to_string(var::SyntaxNode) +function _loop_var_to_string(var::SyntaxNode)::String x = var if kind(x) == K"tuple" x = first_child(x) end if kind(x) == K"Identifier" return string(x) end @@ -41,7 +43,7 @@ function loop_var_to_string(var::SyntaxNode) return "" end -function frisk_for_modification(this::Check, ctxt::AnalysisContext, body::SyntaxNode, var_name::String)::Nothing +function _frisk_for_modification(this::Check, ctxt::AnalysisContext, body::SyntaxNode, var_name::String)::Nothing for expr in children(body) if is_assignment(expr) lhs_node, lhs_str = get_assignee(expr) @@ -52,7 +54,7 @@ function frisk_for_modification(this::Check, ctxt::AnalysisContext, body::Syntax elseif is_flow_cntrl(expr) next_victim = findfirst(x -> kind(x) == K"block", children(expr)) if ! isnothing(next_victim) - frisk_for_modification(this, ctxt, children(expr)[next_victim], var_name) + _frisk_for_modification(this, ctxt, children(expr)[next_victim], var_name) end end end diff --git a/checks/DoNotCommentOutCode.jl b/checks/DoNotCommentOutCode.jl index 5b7f900..28160c5 100644 --- a/checks/DoNotCommentOutCode.jl +++ b/checks/DoNotCommentOutCode.jl @@ -1,11 +1,13 @@ module DoNotCommentOutCode -using ...CommentHelpers: Comment, CommentBlock, get_comment_blocks, get_range, get_text, contains_comments +using ...CommentHelpers: Comment, CommentBlock, contains_comments, get_comment_blocks, get_range, get_text +using JuliaSyntax: kind, parseall, source_location using ...WhitespaceHelpers: combine_ranges -using JuliaSyntax: kind, @K_str, source_location, JuliaSyntax as JS include("_common.jl") +const CommentOrCommentBlock = Union{Comment, CommentBlock} + """ Some keywords and other signifiers that need to be in the string in order for it to be considered code @@ -18,12 +20,12 @@ const KEYWORDS = ["baremodule", "begin", "break", "const", "continue", "do", "ex "type", "var", "(", ")"] struct Check<:Analysis.Check end -id(::Check) = "do-not-comment-out-code" -severity(::Check) = 9 -synopsis(::Check) = "Do not comment out code." +Analysis.id(::Check) = "do-not-comment-out-code" +Analysis.severity(::Check) = 9 +Analysis.synopsis(::Check) = "Do not comment out code." -function init(this::Check, ctxt::AnalysisContext)::Nothing +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, contains_comments, n -> _check(this, ctxt, n)) return nothing end @@ -44,22 +46,23 @@ function _check(this::Check, ctxt::AnalysisContext, node::SyntaxNode)::Nothing return nothing end -function _report(ctxt::AnalysisContext, this::Check, range::UnitRange) +function _report(ctxt::AnalysisContext, this::Check, range::UnitRange)::Nothing report_violation(ctxt, this, range, "Comment contains code") + return nothing end -# If JS can parse the comment contents, it must be code +# If JuliaSyntax can parse the comment contents, it must be code function _contains_code(text::AbstractString)::Bool if !any(occursin(text), KEYWORDS) return false end try - JS.parseall(SyntaxNode, text) + parseall(SyntaxNode, text) catch return false end return true end -function _contains_code(comment::Union{Comment, CommentBlock})::Bool +function _contains_code(comment::CommentOrCommentBlock)::Bool return _contains_code(get_text(comment)) end diff --git a/checks/DoNotNestMultilineComments.jl b/checks/DoNotNestMultilineComments.jl index 7c9f1c5..0f9ae6a 100644 --- a/checks/DoNotNestMultilineComments.jl +++ b/checks/DoNotNestMultilineComments.jl @@ -5,27 +5,28 @@ using ...SyntaxNodeHelpers include("_common.jl") -struct Check<:Analysis.Check end -id(::Check) = "do-not-nest-multiline-comments" -severity(::Check) = 9 -synopsis(::Check) = "Don't nest multiline comments" - +""" Start of multiline comment in Julia """ const ML_COMMENT = "#=" -function init(this::Check, ctxt::AnalysisContext) +struct Check<:Analysis.Check end +Analysis.id(::Check) = "do-not-nest-multiline-comments" +Analysis.severity(::Check) = 9 +Analysis.synopsis(::Check) = "Don't nest multiline comments" + +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, is_toplevel, node -> begin code = node.source.code comments = filter(gl -> kind(gl) == K"Comment", ctxt.greenleaves) for comment in comments text = sourcetext(comment) if startswith(text, ML_COMMENT) # We are only interested in multiline comments - # Search for next comment inside comment + # Search for next comment inside comment found::Union{UnitRange{Int}, Nothing} = findnext(ML_COMMENT, text, length(ML_COMMENT)) if !isnothing(found) - found = (comment.range.start-1) .+ found - report_violation(ctxt, this, - source_location(node.source, found.start), - found, + found = (comment.range.start - 1) .+ found + report_violation(ctxt, this, + source_location(node.source, found.start), + found, synopsis(this) ) end @@ -33,6 +34,7 @@ function init(this::Check, ctxt::AnalysisContext) end return nothing end) + return nothing end end # module DoNotNestMultilineComments diff --git a/checks/DoNotSetVariablesToInf.jl b/checks/DoNotSetVariablesToInf.jl index d86dce8..8aa404c 100644 --- a/checks/DoNotSetVariablesToInf.jl +++ b/checks/DoNotSetVariablesToInf.jl @@ -5,11 +5,11 @@ using ...SyntaxNodeHelpers include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "do-not-set-variables-to-inf" -severity(::Check) = 3 -synopsis(::Check) = "Do not set variables to Inf, Inf16, Inf32 or Inf64" +Analysis.id(::Check) = "do-not-set-variables-to-inf" +Analysis.severity(::Check) = 3 +Analysis.synopsis(::Check) = "Do not set variables to Inf, Inf16, Inf32 or Inf64" -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, n -> kind(n) == K"=", node -> begin if numchildren(node) != 2 @debug "Assignment with $(numchildren(node)) children instead of 2." @@ -22,6 +22,7 @@ function init(this::Check, ctxt::AnalysisContext) end end) + return nothing end end # module DoNotSetVariablesToInf diff --git a/checks/DoNotSetVariablesToNan.jl b/checks/DoNotSetVariablesToNan.jl index 3a320fc..952f8b5 100644 --- a/checks/DoNotSetVariablesToNan.jl +++ b/checks/DoNotSetVariablesToNan.jl @@ -5,11 +5,11 @@ using ...SyntaxNodeHelpers include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "do-not-set-variables-to-nan" -severity(::Check) = 3 -synopsis(::Check) = "Do not set variables to NaN, NaN16, NaN32 or NaN64" +Analysis.id(::Check) = "do-not-set-variables-to-nan" +Analysis.severity(::Check) = 3 +Analysis.synopsis(::Check) = "Do not set variables to NaN, NaN16, NaN32 or NaN64" -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, n -> kind(n) == K"=", node -> begin if numchildren(node) != 2 @debug "Assignment with $(numchildren(node)) children instead of 2." @@ -20,8 +20,9 @@ function init(this::Check, ctxt::AnalysisContext) if extract_special_value(rhs) ∈ SyntaxNodeHelpers.NAN_VALUES report_violation(ctxt, this, rhs, synopsis(this)) end - + end) + return nothing end end # module DoNotSetVariablesToNan diff --git a/checks/DocumentConstants.jl b/checks/DocumentConstants.jl index 93057e1..2de9b99 100644 --- a/checks/DocumentConstants.jl +++ b/checks/DocumentConstants.jl @@ -6,15 +6,16 @@ using ...SyntaxNodeHelpers: find_descendants include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "document-constants" -severity(::Check) = 7 -synopsis(::Check) = "Constants must have a docstring" +Analysis.id(::Check) = "document-constants" +Analysis.severity(::Check) = 7 +Analysis.synopsis(::Check) = "Constants must have a docstring" -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, n -> kind(n) == K"const", n -> check(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, n -> kind(n) == K"const", n -> _check(this, ctxt, n)) + return nothing end -function check(this::Check, ctxt::AnalysisContext, const_node::SyntaxNode) +function _check(this::Check, ctxt::AnalysisContext, const_node::SyntaxNode)::Nothing @assert kind(const_node) == K"const" "Expected a [const] const_node, got $(kind(const_node))." if kind(const_node.parent) == K"doc" @@ -32,6 +33,7 @@ function check(this::Check, ctxt::AnalysisContext, const_node::SyntaxNode) ) end end + return nothing end -end +end # module DocumentConstants diff --git a/checks/ExclamationMarkInFunctionIdentifierIfMutating.jl b/checks/ExclamationMarkInFunctionIdentifierIfMutating.jl index 2534968..31ef1b1 100644 --- a/checks/ExclamationMarkInFunctionIdentifierIfMutating.jl +++ b/checks/ExclamationMarkInFunctionIdentifierIfMutating.jl @@ -5,19 +5,24 @@ using ...MutatingFunctionsHelpers: get_mutated_variables_in_scope using ...Properties: get_func_body, get_func_name, get_string_fn_args, is_function include("_common.jl") -struct Check <: Analysis.Check end +struct Check<:Analysis.Check end -id(::Check) = "exclamation-mark-in-function-identifier-if-mutating" -severity(::Check) = 4 -synopsis(::Check) = "Only functions postfixed with an exclamation mark can mutate an argument." +Analysis.id(::Check) = "exclamation-mark-in-function-identifier-if-mutating" +Analysis.severity(::Check) = 4 +function Analysis.synopsis(::Check)::String + return "Only functions postfixed with an exclamation mark can mutate an argument." +end -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, _is_nonmutating_fn, n -> check_function(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, _is_nonmutating_fn, n -> _check_function(this, ctxt, n)) + return nothing end -_is_nonmutating_fn(n::SyntaxNode)::Bool = is_function(n) && !endswith(string(get_func_name(n)), "!") +function _is_nonmutating_fn(n::SyntaxNode)::Bool + return is_function(n) && !endswith(string(get_func_name(n)), "!") +end -function check_function(this::Check, ctxt::AnalysisContext, function_node::SyntaxNode) +function _check_function(this::Check, ctxt::AnalysisContext, function_node::SyntaxNode)::Nothing func_arg_strings = get_string_fn_args(function_node) all_mutated_variables = get_mutated_variables_in_scope(ctxt, get_func_body(function_node)) for func_arg in func_arg_strings @@ -26,6 +31,7 @@ function check_function(this::Check, ctxt::AnalysisContext, function_node::Synta "Function mutates argument $(string(func_arg)) without having an exclamation mark.") end end + return nothing end end # end ExclamationMarkInFunctionIdentifierIfMutating diff --git a/checks/FunctionArgumentsLowerSnakeCase.jl b/checks/FunctionArgumentsLowerSnakeCase.jl index b93b602..4d1ddc3 100644 --- a/checks/FunctionArgumentsLowerSnakeCase.jl +++ b/checks/FunctionArgumentsLowerSnakeCase.jl @@ -5,11 +5,11 @@ using ...Properties: find_lhs_of_kind, is_lower_snake, get_func_name, get_func_a include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "function-arguments-lower-snake-case" -severity(::Check) = 7 -synopsis(::Check) = "Function arguments must be written in \"lower_snake_case\"" +Analysis.id(::Check) = "function-arguments-lower-snake-case" +Analysis.severity(::Check) = 7 +Analysis.synopsis(::Check) = "Function arguments must be written in \"lower_snake_case\"" -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, n -> kind(n) == K"function", node -> begin fname = get_func_name(node) fname_str = string(fname) @@ -21,20 +21,21 @@ function init(this::Check, ctxt::AnalysisContext) end # The last argument in the list is itself a list, of named arguments. for arg in children(arg) - checkArgument(this, ctxt, fname_str, arg) + _check_argument(this, ctxt, fname_str, arg) end else - checkArgument(this, ctxt, fname_str, arg) + _check_argument(this, ctxt, fname_str, arg) end end end) + return nothing end -function checkArgument(this::Check, ctxt::AnalysisContext, f_name::AbstractString, f_arg::SyntaxNode) +function _check_argument(this::Check, ctxt::AnalysisContext, f_name::AbstractString, f_arg::SyntaxNode)::Nothing if kind(f_arg) == K"::" f_arg = numchildren(f_arg) == 1 ? nothing : children(f_arg)[1] end - if f_arg !== nothing + if ! isnothing(f_arg) f_arg = find_lhs_of_kind(K"Identifier", f_arg) end if isnothing(f_arg) @@ -44,11 +45,12 @@ function checkArgument(this::Check, ctxt::AnalysisContext, f_name::AbstractStrin end arg_name = string(f_arg) if ! is_lower_snake(arg_name) - report_violation(ctxt, this, f_arg, + report_violation(ctxt, this, f_arg, "Argument '$arg_name' of function '$f_name' must be written in \"lower_snake_case\"." ) end + return nothing end -end # module FunctionArgumentsInLowerSnakeCase +end # module FunctionArgumentsLowerSnakeCase diff --git a/checks/FunctionIdentifiersInLowerSnakeCase.jl b/checks/FunctionIdentifiersInLowerSnakeCase.jl index 23a366b..8f896d9 100644 --- a/checks/FunctionIdentifiersInLowerSnakeCase.jl +++ b/checks/FunctionIdentifiersInLowerSnakeCase.jl @@ -5,22 +5,23 @@ using ...Properties: inside, is_lower_snake, is_struct, get_func_name include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "function-identifiers-in-lower-snake-case" -severity(::Check) = 8 -synopsis(::Check) = "Function name should be written in \"lower_snake_case\"" +Analysis.id(::Check) = "function-identifiers-in-lower-snake-case" +Analysis.severity(::Check) = 8 +Analysis.synopsis(::Check) = "Function name should be written in \"lower_snake_case\"" -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, n -> kind(n) == K"function", node -> begin fname = get_func_name(node) if kind(fname.parent) == K"." return #RM-37316: do not trigger on extension of a function defined in another module end - checkFunctionName(this, ctxt, fname) + _check_function_name(this, ctxt, fname) end) + return nothing end -function checkFunctionName(this::Check, ctxt::AnalysisContext, func_name::SyntaxNode) +function _check_function_name(this::Check, ctxt::AnalysisContext, func_name::SyntaxNode)::Nothing @assert kind(func_name) == K"Identifier" "Expected an [Identifier] node, got [$(kind(func_name))]" if inside(func_name, is_struct) # Inner constructors (functions inside a type definition) must match the @@ -34,6 +35,7 @@ function checkFunctionName(this::Check, ctxt::AnalysisContext, func_name::Syntax "Function name $fname should be written in lower_snake_case." ) end + return nothing end end # module FunctionIdentifiersInLowerSnakeCase diff --git a/checks/FunctionsMutateOnlyZeroOrOneArguments.jl b/checks/FunctionsMutateOnlyZeroOrOneArguments.jl index 4c91202..29e2594 100644 --- a/checks/FunctionsMutateOnlyZeroOrOneArguments.jl +++ b/checks/FunctionsMutateOnlyZeroOrOneArguments.jl @@ -5,17 +5,17 @@ using ...MutatingFunctionsHelpers: get_mutated_variables_in_scope using ...Properties: get_flattened_fn_arg_nodes, get_func_body, get_string_arg, is_function include("_common.jl") -struct Check <: Analysis.Check end +struct Check<:Analysis.Check end +Analysis.id(::Check) = "functions-mutate-only-zero-or-one-arguments" +Analysis.severity(::Check) = 3 +Analysis.synopsis(::Check) = "Functions should change only one or zero argument(s)." -id(::Check) = "functions-mutate-only-zero-or-one-arguments" -severity(::Check) = 3 -synopsis(::Check) = "Functions should change only one or zero argument(s)." - -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, is_function, n -> check_function(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, is_function, n -> _check_function(this, ctxt, n)) + return nothing end -function check_function(this::Check, ctxt::AnalysisContext, function_node::SyntaxNode) +function _check_function(this::Check, ctxt::AnalysisContext, function_node::SyntaxNode)::Nothing func_arg_nodes = get_flattened_fn_arg_nodes(function_node) all_mutated_variables = get_mutated_variables_in_scope(ctxt, get_func_body(function_node)) for func_arg in func_arg_nodes[2:end] @@ -25,6 +25,7 @@ function check_function(this::Check, ctxt::AnalysisContext, function_node::Synta "Function mutates variable $(string(func_arg_string)) while it is not the first argument.") end end + return nothing end end # end FunctionsMutateOnlyZeroOrOneArguments diff --git a/checks/GlobalNonConstVariablesShouldHaveTypeAnnotations.jl b/checks/GlobalNonConstVariablesShouldHaveTypeAnnotations.jl index 584bcf1..cb1caa6 100644 --- a/checks/GlobalNonConstVariablesShouldHaveTypeAnnotations.jl +++ b/checks/GlobalNonConstVariablesShouldHaveTypeAnnotations.jl @@ -5,18 +5,18 @@ using ...Properties: first_child, is_constant, is_global_decl, haschildren, is_m include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "global-non-const-variables-should-have-type-annotations" -severity(::Check) = 6 -synopsis(::Check) = "Global non-const variables should have type annotations" +Analysis.id(::Check) = "global-non-const-variables-should-have-type-annotations" +Analysis.severity(::Check) = 6 +Analysis.synopsis(::Check) = "Global non-const variables should have type annotations" -function init(this::Check, ctxt::AnalysisContext)::Nothing +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, n -> is_global_decl(n) && !is_constant(n) && !isnothing(n.parent) && is_mod_toplevel(n.parent), node -> begin - check(this, ctxt, node) + _check(this, ctxt, node) end) return nothing end -function check(this::Check, ctxt::AnalysisContext, glob_var::SyntaxNode)::Nothing +function _check(this::Check, ctxt::AnalysisContext, glob_var::SyntaxNode)::Nothing # This must not be a [const], so it must be [global]. @assert is_global_decl(glob_var) "Expected a global declaration, got [$(kind(glob_var))]." @assert !is_constant(glob_var) "Run this check on non-const global declarations only!" diff --git a/checks/GlobalVariablesUpperSnakeCase.jl b/checks/GlobalVariablesUpperSnakeCase.jl index 55fb4ee..858f8f8 100644 --- a/checks/GlobalVariablesUpperSnakeCase.jl +++ b/checks/GlobalVariablesUpperSnakeCase.jl @@ -7,11 +7,11 @@ using ...SyntaxNodeHelpers: get_all_assignees include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "global-variables-upper-snake-case" -severity(::Check) = 3 -synopsis(::Check) = "Casing of globals" +Analysis.id(::Check) = "global-variables-upper-snake-case" +Analysis.severity(::Check) = 3 +Analysis.synopsis(::Check) = "Casing of globals" -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, n -> is_assignment(n) && !is_field_assignment(n), n -> begin ids = get_all_assignees(n) for id in ids @@ -28,6 +28,7 @@ function init(this::Check, ctxt::AnalysisContext) end end end) + return nothing end end # module GlobalVariablesUpperSnakeCase diff --git a/checks/ImplementUnionsAsConsts.jl b/checks/ImplementUnionsAsConsts.jl index 3ee9599..b239db6 100644 --- a/checks/ImplementUnionsAsConsts.jl +++ b/checks/ImplementUnionsAsConsts.jl @@ -5,17 +5,18 @@ using ...Properties: is_assignment, is_constant, is_union_decl include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "implement-unions-as-consts" -severity(::Check) = 3 -synopsis(::Check) = "Implement Unions as const" +Analysis.id(::Check) = "implement-unions-as-consts" +Analysis.severity(::Check) = 3 +Analysis.synopsis(::Check) = "Implement Unions as const" -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, is_union_decl, node -> begin - check_union(this, ctxt, node) + _check_union(this, ctxt, node) end) + return nothing end -function check_union(this::Check, ctxt::AnalysisContext, union::SyntaxNode)::Nothing +function _check_union(this::Check, ctxt::AnalysisContext, union::SyntaxNode)::Nothing @assert is_union_decl(union) "Expected a Union declaration, got $(kind(union))" if is_assignment(union.parent) && is_constant(union.parent.parent) # This seems to be a Union type declaration diff --git a/checks/IndentationLevelsAreFourSpaces.jl b/checks/IndentationLevelsAreFourSpaces.jl index 827042d..6af17a1 100644 --- a/checks/IndentationLevelsAreFourSpaces.jl +++ b/checks/IndentationLevelsAreFourSpaces.jl @@ -1,16 +1,15 @@ module IndentationLevelsAreFourSpaces -include("_common.jl") - using ...Properties: is_toplevel -using ...SyntaxNodeHelpers + +include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "indentation-levels-are-four-spaces" -severity(::Check) = 7 -synopsis(::Check) = "Indentation should be a multiple of four spaces" +Analysis.id(::Check) = "indentation-levels-are-four-spaces" +Analysis.severity(::Check) = 7 +Analysis.synopsis(::Check) = "Indentation should be a multiple of four spaces" -function init(this::Check, ctxt::AnalysisContext)::Nothing +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, is_toplevel, n -> begin for gl in ctxt.greenleaves # We will inspect nodes of kind [NewlineWs] containing indentation spaces @@ -26,7 +25,7 @@ function init(this::Check, ctxt::AnalysisContext)::Nothing # their presence here, counting 4-1 extra spaces for each tab. indentation::Int = length(indenttext) + 3 * count(r"\t", indenttext) if rem(indentation, 4) > 0 - rng = range(gl.range.stop - length(indenttext) + 1, length=length(indenttext)) + rng = range(gl.range.stop - length(indenttext) + 1; length=length(indenttext)) pos = source_location(n.source, rng.start) report_violation(ctxt, this, pos, rng, synopsis(this)) end diff --git a/checks/IndentationOfModules.jl b/checks/IndentationOfModules.jl index 4d690b4..1dba0bc3 100644 --- a/checks/IndentationOfModules.jl +++ b/checks/IndentationOfModules.jl @@ -1,18 +1,18 @@ module IndentationOfModules -include("_common.jl") - using JuliaSyntax: view using ...Properties: is_module, get_module_name using ...SyntaxNodeHelpers: ancestors using ...WhitespaceHelpers: normalized_green_child_range +include("_common.jl") + struct Check<:Analysis.Check end -id(::Check) = "indentation-of-modules" -severity(::Check) = 7 -synopsis(::Check) = "Do not indent top level module body, do indent submodules" +Analysis.id(::Check) = "indentation-of-modules" +Analysis.severity(::Check) = 7 +Analysis.synopsis(::Check) = "Do not indent top level module body, do indent submodules" -function init(this::Check, ctxt::AnalysisContext)::Nothing +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, is_module, n -> _check(this, ctxt, n)) return nothing end @@ -36,11 +36,12 @@ function _check(this::Check, ctxt::AnalysisContext, module_node::SyntaxNode)::No actual_indent = length(strip(view(module_node.source, ws_range), ['\r', '\n'])) # Only report on indent on current line (not newlines) if exp_indent != actual_indent - nudged_range = range(;stop=ws_range.stop, length=actual_indent) + nudged_range = range(; stop=ws_range.stop, length=actual_indent) report_violation(ctxt, this, nudged_range, "Contents of module '$module_name' should have an indentation of width $exp_indent, but found $actual_indent") end end end + return nothing end end # module IndentationOfModules diff --git a/checks/InfiniteWhileLoop.jl b/checks/InfiniteWhileLoop.jl index 982048e..8d51c94 100644 --- a/checks/InfiniteWhileLoop.jl +++ b/checks/InfiniteWhileLoop.jl @@ -3,15 +3,16 @@ module InfiniteWhileLoop include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "infinite-while-loop" -severity(::Check) = 5 -synopsis(::Check) = "Do not use while true" +Analysis.id(::Check) = "infinite-while-loop" +Analysis.severity(::Check) = 5 +Analysis.synopsis(::Check) = "Do not use while true" -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, n -> kind(n) == K"while", n -> checkWhileNode(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, n -> kind(n) == K"while", n -> _check_while_node(this, ctxt, n)) + return nothing end -function checkWhileNode(this::Check, ctxt::AnalysisContext, node::SyntaxNode)::Nothing +function _check_while_node(this::Check, ctxt::AnalysisContext, node::SyntaxNode)::Nothing @assert kind(node) == K"while" "Expected a [while], got $(kind(node))" @assert numchildren(node) > 0 "A [while] without children! Is this an incomplete tree, from code under edition?" condition = children(node)[1] diff --git a/checks/LeadingAndTrailingDigits.jl b/checks/LeadingAndTrailingDigits.jl index e36406f..285c286 100644 --- a/checks/LeadingAndTrailingDigits.jl +++ b/checks/LeadingAndTrailingDigits.jl @@ -5,15 +5,18 @@ include("_common.jl") using JuliaSyntax: sourcetext struct Check<:Analysis.Check end -id(::Check) = "leading-and-trailing-digits" -severity(::Check) = 3 -synopsis(::Check) = "Floating-point numbers should always have one digit before the decimal point and at least one after" +Analysis.id(::Check) = "leading-and-trailing-digits" +Analysis.severity(::Check) = 3 +function Analysis.synopsis(::Check) + return "Floating-point numbers should always have one digit before the decimal point and at least one after" +end -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, n -> kind(n) == K"Float", n -> checkFloatNode(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, n -> kind(n) == K"Float", n -> _check_float_node(this, ctxt, n)) + return nothing end -function checkFloatNode(this::Check, ctxt::AnalysisContext, node::SyntaxNode)::Nothing +function _check_float_node(this::Check, ctxt::AnalysisContext, node::SyntaxNode)::Nothing text = sourcetext(node) index = findfirst('.', text) diff --git a/checks/LocationOfGlobalVariables.jl b/checks/LocationOfGlobalVariables.jl index 6c0ab05..fa50049 100644 --- a/checks/LocationOfGlobalVariables.jl +++ b/checks/LocationOfGlobalVariables.jl @@ -5,15 +5,18 @@ include("_common.jl") using ...Properties: haschildren, is_export, is_global_decl, is_import, is_mod_toplevel struct Check<:Analysis.Check end -id(::Check) = "location-of-global-variables" -severity(::Check) = 7 -synopsis(::Check) = "Global variables should be placed at the top of a module or file" +Analysis.id(::Check) = "location-of-global-variables" +Analysis.severity(::Check) = 7 +function Analysis.synopsis(::Check) + return "Global variables should be placed at the top of a module or file" +end -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, is_global_decl, n -> check(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, is_global_decl, n -> _check(this, ctxt, n)) + return nothing end -function check(this::Check, ctxt::AnalysisContext, glob_decl::SyntaxNode)::Nothing +function _check(this::Check, ctxt::AnalysisContext, glob_decl::SyntaxNode)::Nothing @assert is_global_decl(glob_decl) "Expected a global declaration node, got $(kind(glob_decl))" toplevel = glob_decl.parent if !is_mod_toplevel(toplevel) @@ -36,4 +39,4 @@ function check(this::Check, ctxt::AnalysisContext, glob_decl::SyntaxNode)::Nothi return nothing end -end +end # LocationOfGlobalVariables diff --git a/checks/LongFormFunctionsHaveATerminatingReturnStatement.jl b/checks/LongFormFunctionsHaveATerminatingReturnStatement.jl index 731e5c4..8072ff3 100644 --- a/checks/LongFormFunctionsHaveATerminatingReturnStatement.jl +++ b/checks/LongFormFunctionsHaveATerminatingReturnStatement.jl @@ -6,20 +6,23 @@ using ...Properties: inside, is_struct, get_func_name, get_func_body, haschildre using ...WhitespaceHelpers: normalized_green_child_range struct Check<:Analysis.Check end -id(::Check) = "long-form-functions-have-a-terminating-return-statement" -severity(::Check) = 3 -synopsis(::Check) = "Long form functions should end with an explicit return statement" +Analysis.id(::Check) = "long-form-functions-have-a-terminating-return-statement" +Analysis.severity(::Check) = 3 +function Analysis.synopsis(::Check) + return "Long form functions should end with an explicit return statement" +end -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, n -> kind(n) == K"function", node -> begin body = get_func_body(node) - if body !== nothing - checkFuncBody(this, ctxt, body) + if ! isnothing(body) + _check(this, ctxt, body) end end) + return nothing end -function checkFuncBody(this::Check, ctxt::AnalysisContext, func_body::SyntaxNode)::Nothing +function _check(this::Check, ctxt::AnalysisContext, func_body::SyntaxNode)::Nothing @assert kind(func_body.parent) == K"function" "Expected the body of a [function], got $(kind(func_body))" fname = get_func_name(func_body.parent) if isnothing(fname) fname = "" end diff --git a/checks/ModuleEndComment.jl b/checks/ModuleEndComment.jl index 4f10b17..641dbb9 100644 --- a/checks/ModuleEndComment.jl +++ b/checks/ModuleEndComment.jl @@ -6,27 +6,30 @@ using JuliaSyntax: last_byte using ...Properties: is_module, is_toplevel, get_module_name struct Check<:Analysis.Check end -id(::Check) = "module-end-comment" -severity(::Check) = 9 -synopsis(::Check) = "The end statement of a module should have a comment with the module name" +Analysis.id(::Check) = "module-end-comment" +Analysis.severity(::Check) = 9 +function Analysis.synopsis(::Check) + return "The end statement of a module should have a comment with the module name" +end -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, is_module, n -> checkModule2(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, is_module, n -> _check(this, ctxt, n)) + return nothing end -function checkModule2(this::Check, ctxt::AnalysisContext, mod::SyntaxNode)::Nothing +function _check(this::Check, ctxt::AnalysisContext, mod::SyntaxNode)::Nothing code = mod.source.code mod_end = last_byte(mod) eol = something(findnext('\n', code, mod_end), length(code)) comment_start = something(findnext('#', code, mod_end), length(code)) if comment_start >= eol filepos = source_location(mod.source, mod_end) - report_violation(ctxt, this, filepos, range(mod_end-2, length=3), "Missing end module comment") - else + report_violation(ctxt, this, filepos, range(mod_end - 2; length=3), "Missing end module comment") + else comment_range = comment_start:eol comment = code[comment_range] (_, mod_name_str) = get_module_name(mod) - if !matches_module_name(mod_name_str, comment) + if !_matches_module_name(mod_name_str, comment) filepos = source_location(mod.source, mod_end) report_violation(ctxt, this, filepos, comment_range, synopsis(this)) end @@ -34,7 +37,7 @@ function checkModule2(this::Check, ctxt::AnalysisContext, mod::SyntaxNode)::Noth return nothing end -function matches_module_name(mod_name::AbstractString, comment::AbstractString) +function _matches_module_name(mod_name::AbstractString, comment::AbstractString)::Bool return occursin(Regex("(module[ ]+)?" * mod_name), comment) end diff --git a/checks/ModuleExportLocation.jl b/checks/ModuleExportLocation.jl index 73f7d8e..97f44da 100644 --- a/checks/ModuleExportLocation.jl +++ b/checks/ModuleExportLocation.jl @@ -5,27 +5,28 @@ include("_common.jl") using ...Properties: is_export, is_import, is_module struct Check<:Analysis.Check end -id(::Check) = "module-export-location" -severity(::Check) = 9 -synopsis(::Check) = "Exports should be implemented after the include instructions" +Analysis.id(::Check) = "module-export-location" +Analysis.severity(::Check) = 9 +Analysis.synopsis(::Check) = "Exports should be implemented after the include instructions" -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, is_module, n -> check(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, is_module, n -> _check(this, ctxt, n)) + return nothing end -no_ex_imports(node::SyntaxNode) = ! (is_import(node) || is_export(node)) +_no_ex_imports(node::SyntaxNode) = ! (is_import(node) || is_export(node)) -function check(this::Check, ctxt::AnalysisContext, modjule::SyntaxNode)::Nothing +function _check(this::Check, ctxt::AnalysisContext, modjule::SyntaxNode)::Nothing @assert kind(modjule) == K"module" "Expected a [module] node, got [$(kind(modjule))]." - @assert numchildren(modjule) == 2 "This module has a weird shape: "* string(modjule) + @assert numchildren(modjule) == 2 "This module has a weird shape: " * string(modjule) @assert kind(children(modjule)[2]) == K"block" "The second child of a [module] node is not a [block]!" mod_body = children(children(modjule)[2]) last_export = findlast(is_export, mod_body) - if last_export === nothing return nothing end + if isnothing(last_export) return nothing end - code_begin = findfirst(no_ex_imports, mod_body) - if code_begin === nothing + code_begin = findfirst(_no_ex_imports, mod_body) + if isnothing(code_begin) # Nothing to check that is not yet covered by other rules. return nothing end diff --git a/checks/ModuleImportLocation.jl b/checks/ModuleImportLocation.jl index e6547c0..9516817 100644 --- a/checks/ModuleImportLocation.jl +++ b/checks/ModuleImportLocation.jl @@ -5,29 +5,32 @@ include("_common.jl") using ...Properties: is_import, is_include, is_module struct Check<:Analysis.Check end -id(::Check) = "module-import-location" -severity(::Check) = 9 -synopsis(::Check) = "Packages should be imported after the module keyword." - -const USER_MSG = "Move imports to the top of the module, before any actual code" +Analysis.id(::Check) = "module-import-location" +Analysis.severity(::Check) = 9 +function Analysis.synopsis(::Check) + return "Packages should be imported after the module keyword." +end -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, is_module, n -> check(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, is_module, n -> _check(this, ctxt, n)) + return nothing end -function check(this::Check, ctxt::AnalysisContext, modjule::SyntaxNode)::Nothing +function _check(this::Check, ctxt::AnalysisContext, modjule::SyntaxNode)::Nothing @assert kind(modjule) == K"module" "Expected a [module] node, got [$(kind(node))]." - @assert numchildren(modjule) == 2 "This module has a weird shape: "* string(modjule) + @assert numchildren(modjule) == 2 "This module has a weird shape: " * string(modjule) @assert kind(children(modjule)[2]) == K"block" "The second child of a [module] node is not a [block]!" mod_body = children(children(modjule)[2]) code_starts_here = findfirst(!is_import, mod_body) - if code_starts_here !== nothing + if ! isnothing(code_starts_here) for node in mod_body[code_starts_here:end] if is_import(node) && !is_include(node) # We can skip include's because they are followed by an import # or a using (we made sure in 'is_import'). - report_violation(ctxt, this, node, USER_MSG) + report_violation(ctxt, this, node, + "Move imports to the top of the module, before any actual code" + ) end end end diff --git a/checks/ModuleIncludeLocation.jl b/checks/ModuleIncludeLocation.jl index 8b29366..fc082a7 100644 --- a/checks/ModuleIncludeLocation.jl +++ b/checks/ModuleIncludeLocation.jl @@ -5,27 +5,30 @@ using ...Properties: get_imported_pkg, is_import, is_include, is_module include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "module-include-location" -severity(::Check) = 9 -synopsis(::Check) = "The list of included files should be after the list of imported packages" +Analysis.id(::Check) = "module-include-location" +Analysis.severity(::Check) = 9 +function Analysis.synopsis(::Check) + return "The list of included files should be after the list of imported packages" +end -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, is_module, n -> check(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, is_module, n -> _check(this, ctxt, n)) + return nothing end -function check(this::Check, ctxt::AnalysisContext, modjule::SyntaxNode)::Nothing +function _check(this::Check, ctxt::AnalysisContext, modjule::SyntaxNode)::Nothing @assert kind(modjule) == K"module" "Expected a [module] node, got [$(kind(modjule))]." - @assert numchildren(modjule) == 2 "This module has a weird shape: "* string(modjule) + @assert numchildren(modjule) == 2 "This module has a weird shape: " * string(modjule) @assert kind(children(modjule)[2]) == K"block" "The second child of a [module] node is not a [block]!" mod_body = children(children(modjule)[2]) code_beginning = findfirst(!is_import, mod_body) - if code_beginning === nothing + if isnothing(code_beginning) # No code, only imports. It usually happens in packages "entry" files. code_beginning = length(mod_body) + 1 end includes_start = findfirst(is_include, mod_body[1:code_beginning-1]) - if includes_start !== nothing + if ! isnothing(includes_start) for (i, node) in enumerate(mod_body[includes_start+1 : code_beginning-1]) if !is_include(node) # It must be an [import] or [using] diff --git a/checks/ModuleNameCasing.jl b/checks/ModuleNameCasing.jl index 57b41b9..a514356 100644 --- a/checks/ModuleNameCasing.jl +++ b/checks/ModuleNameCasing.jl @@ -4,11 +4,13 @@ include("_common.jl") using ...Properties: get_module_name, is_upper_camel_case struct Check<:Analysis.Check end -id(::Check) = "module-name-casing" -severity(::Check) = 5 -synopsis(::Check) = "Package names and module names should be written in UpperCamelCase" +Analysis.id(::Check) = "module-name-casing" +Analysis.severity(::Check) = 5 +function Analysis.synopsis(::Check) + return "Package names and module names should be written in UpperCamelCase" +end -function init(this::Check, ctxt::AnalysisContext)::Nothing +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, n -> kind(n) == K"module", node -> begin (mod_id_node, module_name) = get_module_name(node) if ! is_upper_camel_case(module_name) diff --git a/checks/ModuleSingleImportLine.jl b/checks/ModuleSingleImportLine.jl index 17e8d13..6df149c 100644 --- a/checks/ModuleSingleImportLine.jl +++ b/checks/ModuleSingleImportLine.jl @@ -4,17 +4,18 @@ include("_common.jl") using ...Properties: get_imported_pkg, is_import, is_include, is_module struct Check<:Analysis.Check end -id(::Check) = "module-single-import-line" -severity(::Check) = 9 -synopsis(::Check) = "The list of packages should be in alphabetical order" +Analysis.id(::Check) = "module-single-import-line" +Analysis.severity(::Check) = 9 +Analysis.synopsis(::Check) = "The list of packages should be in alphabetical order" -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, is_module, n -> check(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, is_module, n -> _check(this, ctxt, n)) + return nothing end -function check(this::Check, ctxt::AnalysisContext, module_node::SyntaxNode)::Nothing +function _check(this::Check, ctxt::AnalysisContext, module_node::SyntaxNode)::Nothing @assert kind(module_node) == K"module" "Expected a [module] node, got [$(kind(module_node))]." - @assert numchildren(module_node) == 2 "This module has a weird shape: "* string(module_node) + @assert numchildren(module_node) == 2 "This module has a weird shape: " * string(module_node) @assert kind(children(module_node)[2]) == K"block" "The second child of a [module] node is not a [block]!" # Filters on using, import, include. diff --git a/checks/MultilineCommentsForManyLines.jl b/checks/MultilineCommentsForManyLines.jl index 319b375..3232677 100644 --- a/checks/MultilineCommentsForManyLines.jl +++ b/checks/MultilineCommentsForManyLines.jl @@ -5,11 +5,11 @@ using ...CommentHelpers: CommentBlock, get_comment_blocks, get_range, contains_c include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "multiline-comments-for-many-lines" -severity(::Check) = 9 -synopsis(::Check) = "Use multiline comments for large blocks." +Analysis.id(::Check) = "multiline-comments-for-many-lines" +Analysis.severity(::Check) = 9 +Analysis.synopsis(::Check) = "Use multiline comments for large blocks." -function init(this::Check, ctxt::AnalysisContext)::Nothing +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, contains_comments, n -> _check(this, ctxt, n)) return nothing end diff --git a/checks/NestingOfConditionalStatements.jl b/checks/NestingOfConditionalStatements.jl index fcb6bee..040fbe2 100644 --- a/checks/NestingOfConditionalStatements.jl +++ b/checks/NestingOfConditionalStatements.jl @@ -5,28 +5,33 @@ include("_common.jl") using ...Properties: is_flow_cntrl, is_stop_point struct Check<:Analysis.Check end -id(::Check) = "nesting-of-conditional-statements" -severity(::Check) = 4 -synopsis(::Check) = "Avoid deep nesting of conditional statements" +Analysis.id(::Check) = "nesting-of-conditional-statements" +Analysis.severity(::Check) = 4 +Analysis.synopsis(::Check) = "Avoid deep nesting of conditional statements" +""" Violation is produced when nesting exceeds this threshold """ const MAX_ALLOWED_NESTING_LEVELS = 3 -const USER_MSG = "This conditional expression is too deeply nested (deeper than $MAX_ALLOWED_NESTING_LEVELS levels)." -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, is_flow_cntrl, n -> check(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, is_flow_cntrl, n -> _check(this, ctxt, n)) + return nothing end -function check(this::Check, ctxt::AnalysisContext, node::SyntaxNode) +function _check(this::Check, ctxt::AnalysisContext, node::SyntaxNode)::Nothing @assert is_flow_cntrl(node) "Expected a flow control node, got [$(kind(node))]." # Count the nesting level of conditional statements - if conditional_nesting_level(node) > MAX_ALLOWED_NESTING_LEVELS + if _conditional_nesting_level(node) > MAX_ALLOWED_NESTING_LEVELS length_of_keyword = length(string(kind(node))) - report_violation(ctxt, this, node, USER_MSG; offsetspan=(0, length_of_keyword)) + report_violation(ctxt, this, node, + "This conditional expression is too deeply nested (deeper than $MAX_ALLOWED_NESTING_LEVELS levels)." + ; offsetspan=(0, length_of_keyword) + ) end + return nothing end -function conditional_nesting_level(node::SyntaxNode)::Int +function _conditional_nesting_level(node::SyntaxNode)::Int level = 0 while !isnothing(node) && !is_stop_point(node) if is_flow_cntrl(node) diff --git a/checks/NewlineAtFileEnd.jl b/checks/NewlineAtFileEnd.jl index 617943d..0dbbd67 100644 --- a/checks/NewlineAtFileEnd.jl +++ b/checks/NewlineAtFileEnd.jl @@ -7,11 +7,11 @@ using ...WhitespaceHelpers: get_line_range include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "newline-at-file-end" -severity(::Check) = 7 -synopsis(::Check) = "Single newline at the end of file" +Analysis.id(::Check) = "newline-at-file-end" +Analysis.severity(::Check) = 7 +Analysis.synopsis(::Check) = "Single newline at the end of file" -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, is_toplevel, n -> _check(this, ctxt, n)) return nothing end diff --git a/checks/NoWhitespaceAroundTypeOperators.jl b/checks/NoWhitespaceAroundTypeOperators.jl index 8526c15..a2c3731 100644 --- a/checks/NoWhitespaceAroundTypeOperators.jl +++ b/checks/NoWhitespaceAroundTypeOperators.jl @@ -1,29 +1,29 @@ module NoWhitespaceAroundTypeOperators -using JuliaSyntax: first_byte, last_byte, is_prefix_call, is_prefix_op_call -using ...Properties: is_toplevel +using JuliaSyntax: first_byte, last_byte, is_prefix_op_call include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "no-whitespace-around-type-operators" -severity(::Check) = 7 -synopsis(::Check) = "Do not add whitespace around type operators" +Analysis.id(::Check) = "no-whitespace-around-type-operators" +Analysis.severity(::Check) = 7 +Analysis.synopsis(::Check) = "Do not add whitespace around type operators" -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, is_type_assertion_or_constraint, n -> check(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, _is_type_assertion_or_constraint, n -> _check(this, ctxt, n)) + return nothing end -function is_type_assertion_or_constraint(node) +function _is_type_assertion_or_constraint(node)::Bool return kind(node) in KSet":: <: >:" end -function check(this::Check, ctxt::AnalysisContext, node::SyntaxNode) +function _check(this::Check, ctxt::AnalysisContext, node::SyntaxNode)::Nothing code = node.source.code if is_prefix_op_call(node) start = node.position last = first_byte(node.children[1]) - else + else if length(node.children) != 2 @warn "Expected a node with two children, got [$(length(node.children))]." node return @@ -34,12 +34,13 @@ function check(this::Check, ctxt::AnalysisContext, node::SyntaxNode) text_between = code[start:last] if any(isspace, text_between) linepos = source_location(node.source, start) - report_violation(ctxt, this, + report_violation(ctxt, this, linepos, - range(start, length=length(text_between)), - "Omit whitespace around this operator" + range(start; length=length(text_between)), + "Omit whitespace around this operator" ) end + return nothing end end # module NoWhitespaceAroundTypeOperators diff --git a/checks/OmitTrailingWhiteSpace.jl b/checks/OmitTrailingWhiteSpace.jl index c9ce483..58e5f8d 100644 --- a/checks/OmitTrailingWhiteSpace.jl +++ b/checks/OmitTrailingWhiteSpace.jl @@ -1,26 +1,28 @@ module OmitTrailingWhiteSpace -include("_common.jl") - using ...Properties: is_toplevel +include("_common.jl") + struct Check<:Analysis.Check end -id(::Check) = "omit-trailing-white-space" -severity(::Check) = 7 -synopsis(::Check) = "Omit spaces at the end of a line" +Analysis.id(::Check) = "omit-trailing-white-space" +Analysis.severity(::Check) = 7 +Analysis.synopsis(::Check) = "Omit spaces at the end of a line" -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, is_toplevel, n -> check(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, is_toplevel, n -> _check(this, ctxt, n)) + return nothing end -function check(this::Check, ctxt::AnalysisContext, node::SyntaxNode) +function _check(this::Check, ctxt::AnalysisContext, node::SyntaxNode)::Nothing code = node.source.code for m in eachmatch(r"( +)\r?\n", code) line::Int = count("\n", code[1:m.offset]) + 1 col::Int = m.offset - something(findprev('\n', code, m.offset), 1) + 1 - bufferrange = m.offset:m.offset+length(m.captures[1]) - report_violation(ctxt, this, (line,col), bufferrange, synopsis(this)) + bufferrange = m.offset:m.offset + length(m.captures[1]) + report_violation(ctxt, this, (line, col), bufferrange, synopsis(this)) end + return nothing end end # module OmitTrailingWhiteSpace diff --git a/checks/OneExpressionPerLine.jl b/checks/OneExpressionPerLine.jl index c3e0a33..99f7c01 100644 --- a/checks/OneExpressionPerLine.jl +++ b/checks/OneExpressionPerLine.jl @@ -7,11 +7,11 @@ using ...WhitespaceHelpers: get_line_range include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "one-expression-per-line" -severity(::Check) = 7 -synopsis(::Check) = "The number of expressions per line is limited to one." +Analysis.id(::Check) = "one-expression-per-line" +Analysis.severity(::Check) = 7 +Analysis.synopsis(::Check) = "The number of expressions per line is limited to one." -function init(this::Check, ctxt::AnalysisContext)::Nothing +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, n -> n == ctxt.rootNode, n -> _check(this, ctxt, n)) return nothing end @@ -31,7 +31,7 @@ Analyzing deeper within concatenated statements may lead to duplicate reporting or storing of global data (both of which is not wanted). """ function _check(this::Check, ctxt::AnalysisContext, node::SyntaxNode)::Nothing - lines_to_report = Set{Integer}() + lines_to_report = Set{Int}() nodes_to_check = _get_subnodes_to_check(node) for subnode in nodes_to_check lines_to_report = union!(lines_to_report, _find_semicolon_lines(subnode)) @@ -76,7 +76,7 @@ function _has_semicolon_without_newline(green_children, green_idx::Integer)::Boo end function _find_semicolon_lines(node::SyntaxNode)::Set{Integer} - lines_to_report = Set{Integer}() + lines_to_report = Set{Int}() offset = 0 green_children = children(node.raw) for green_idx in eachindex(green_children) diff --git a/checks/PreferConstVariablesOverNonConstGlobalVariables.jl b/checks/PreferConstVariablesOverNonConstGlobalVariables.jl index 2182657..9aaeb5f 100644 --- a/checks/PreferConstVariablesOverNonConstGlobalVariables.jl +++ b/checks/PreferConstVariablesOverNonConstGlobalVariables.jl @@ -6,11 +6,11 @@ using ...Properties: is_assignment, is_global_decl using ...SyntaxNodeHelpers: get_all_assignees, ancestors, is_scope_construct struct Check<:Analysis.Check end -id(::Check) = "prefer-const-variables-over-non-const-global-variables" -severity(::Check) = 3 -synopsis(::Check) = "Prefer const variables over non-const global variables" +Analysis.id(::Check) = "prefer-const-variables-over-non-const-global-variables" +Analysis.severity(::Check) = 3 +Analysis.synopsis(::Check) = "Prefer const variables over non-const global variables" -function init(this::Check, ctxt::AnalysisContext)::Nothing +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, n -> is_global_decl(n) && is_assignment(n), node -> begin lhs = first(children(node)) if kind(lhs) ∈ KSet". ref" # Exclude mutation (field assignment `A.x = 1` or array mutation `A[i] = 1`) @@ -23,6 +23,7 @@ function init(this::Check, ctxt::AnalysisContext)::Nothing end end end) + return nothing end function has_const_annotation(node::SyntaxNode)::Bool diff --git a/checks/PrefixOfAbstractTypeNames.jl b/checks/PrefixOfAbstractTypeNames.jl index 09d5c2f..74e8715 100644 --- a/checks/PrefixOfAbstractTypeNames.jl +++ b/checks/PrefixOfAbstractTypeNames.jl @@ -4,24 +4,26 @@ include("_common.jl") using ...Properties: find_lhs_of_kind, is_upper_camel_case struct Check<:Analysis.Check end -id(::Check) = "prefix-of-abstract-type-names" -severity(::Check) = 4 -synopsis(::Check) = "Abstract type names should be prefixed by \"Abstract\"." +Analysis.id(::Check) = "prefix-of-abstract-type-names" +Analysis.severity(::Check) = 4 +Analysis.synopsis(::Check) = "Abstract type names should be prefixed by \"Abstract\"." -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, n -> kind(n) == K"abstract", node -> check(this, ctxt, node)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, n -> kind(n) == K"abstract", node -> _check(this, ctxt, node)) + return nothing end -function check(this::Check, ctxt::AnalysisContext, node::SyntaxNode) +function _check(this::Check, ctxt::AnalysisContext, node::SyntaxNode)::Nothing @assert kind(node) == K"abstract" "Expected an [abstract] node, got $(kind(node))" type_id = find_lhs_of_kind(K"Identifier", node) - @assert type_id !== nothing "Got a type declaration without name (identifier)." + @assert ! isnothing(type_id) "Got a type declaration without name (identifier)." type_name = string(type_id) if ! startswith(type_name, "Abstract") report_violation(ctxt, this, type_id, "Abstract type names like $type_name should have prefix \"Abstract\"." ) end + return nothing end end # module PrefixOfAbstractTypeNames diff --git a/checks/ShortHandFunctionTooComplicated.jl b/checks/ShortHandFunctionTooComplicated.jl index 7b54ada..17dc914 100644 --- a/checks/ShortHandFunctionTooComplicated.jl +++ b/checks/ShortHandFunctionTooComplicated.jl @@ -5,24 +5,28 @@ using JuliaSyntax: sourcetext using ...Properties: MAX_LINE_LENGTH, expr_depth, expr_size, get_func_name, get_func_body struct Check<:Analysis.Check end -id(::Check) = "short-hand-function-too-complicated" -severity(::Check) = 3 -synopsis(::Check) = "Short-hand notation with concise functions" +Analysis.id(::Check) = "short-hand-function-too-complicated" +Analysis.severity(::Check) = 3 +Analysis.synopsis(::Check) = "Short-hand notation with concise functions" -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, n -> kind(n) == K"function", func -> begin body = get_func_body(func) if !isnothing(body) && kind(body) != K"block" - checkFunction(this, ctxt, func, body) + _check(this, ctxt, func, body) end end) + return nothing end -function checkFunction(this::Check, ctxt::AnalysisContext, func::SyntaxNode, body::SyntaxNode) - report() = report_violation(ctxt, this, body, - "Function '$(get_func_name(func))' is too complex for the shorthand notation; use keyword 'function'." - ) - +function _check(this::Check, ctxt::AnalysisContext, func::SyntaxNode, body::SyntaxNode)::Nothing + function report()::Nothing + report_violation(ctxt, this, body, + "Function '$(get_func_name(func))' is too complex for the shorthand notation; use keyword 'function'." + ) + return nothing + end + line_len = length(sourcetext(func)) if line_len > MAX_LINE_LENGTH report() diff --git a/checks/SingleModuleFile.jl b/checks/SingleModuleFile.jl index 2af142c..a1244e6 100644 --- a/checks/SingleModuleFile.jl +++ b/checks/SingleModuleFile.jl @@ -5,16 +5,17 @@ using JuliaSyntax: filename using ...Properties: is_module struct Check<:Analysis.Check end -id(::Check) = "single-module-file" -severity(::Check) = 5 -synopsis(::Check) = "Single module per file" +Analysis.id(::Check) = "single-module-file" +Analysis.severity(::Check) = 5 +Analysis.synopsis(::Check) = "Single module per file" -function init(this::Check, ctxt::AnalysisContext) - register_syntaxnode_action(ctxt, is_module, node -> check(this, ctxt, node)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, is_module, node -> _check(this, ctxt, node)) + return nothing end -function check(this::Check, ctxt::AnalysisContext, module_node::SyntaxNode) - @assert kind(module_node) == K"module" "Expected a [module] node, got [$(kind(node))]." +function _check(this::Check, ctxt::AnalysisContext, module_node::SyntaxNode)::Nothing + @assert kind(module_node) == K"module" "Expected a [module] node, got [$(kind(module_node))]." father = module_node.parent kids = children(father) if kind(father) == K"toplevel" @@ -45,6 +46,7 @@ function check(this::Check, ctxt::AnalysisContext, module_node::SyntaxNode) end end end + return nothing end end # module SingleModuleFile diff --git a/checks/SingleSpaceAfterCommasAndSemicolons.jl b/checks/SingleSpaceAfterCommasAndSemicolons.jl index e8446b5..19adfb2 100644 --- a/checks/SingleSpaceAfterCommasAndSemicolons.jl +++ b/checks/SingleSpaceAfterCommasAndSemicolons.jl @@ -6,35 +6,37 @@ using ...Properties: is_toplevel using ...SyntaxNodeHelpers struct Check<:Analysis.Check end -id(::Check) = "single-space-after-commas-and-semicolons" -severity(::Check) = 7 -synopsis(::Check) = "Commas and semicolons are followed, but not preceded, by a space." +Analysis.id(::Check) = "single-space-after-commas-and-semicolons" +Analysis.severity(::Check) = 7 +Analysis.synopsis(::Check) = "Commas and semicolons are followed, but not preceded, by a space." -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, is_toplevel, node -> begin code = node.source.code - report_if_space(pos::Integer, func::Function, shouldhave::Integer, msg::String) = begin + report_if_space(pos::Integer, func::Function, shouldhave::Integer, msg::String)::Nothing = begin range = func(code, pos) if length(range) != shouldhave && !contains(code[range], '\n') # skip check if range contains a newline - report_violation(ctxt, this, - source_location(node.source, range.start), + report_violation(ctxt, this, + source_location(node.source, range.start), range, msg ) end + return nothing end for m in eachmatch(r"[;,]", code) # Find each occurrence in code pos = m.offset leaf = find_greenleaf(ctxt, pos) # Find the GreenLeaf containing the character if kind(leaf.node) ∉ KSet"Char Comment String" # Skip strings and comments - report_if_space(pos, find_whitespace_func(false), 0, "Unexpected whitespace") - report_if_space(pos, find_whitespace_func(true), 1, "Expected single whitespace") + report_if_space(pos, _find_whitespace_func(false), 0, "Unexpected whitespace") + report_if_space(pos, _find_whitespace_func(true), 1, "Expected single whitespace") end end end) + return nothing end -function find_whitespace_func(forward::Bool)::Function +function _find_whitespace_func(forward::Bool)::Function find = forward ? nextind : prevind return (s::AbstractString, start::Integer) -> begin p = find(s, start) diff --git a/checks/SpaceAroundBinaryInfixOperators.jl b/checks/SpaceAroundBinaryInfixOperators.jl index 9c73299..8a992b1 100644 --- a/checks/SpaceAroundBinaryInfixOperators.jl +++ b/checks/SpaceAroundBinaryInfixOperators.jl @@ -18,9 +18,9 @@ using ...WhitespaceHelpers: include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "space-around-binary-infix-operators" -severity(::Check) = 7 -function synopsis(::Check) +Analysis.id(::Check) = "space-around-binary-infix-operators" +Analysis.severity(::Check) = 7 +function Analysis.synopsis(::Check)::String return "Selected binary infix operators and the = character are followed and preceded by a single space." end @@ -33,7 +33,7 @@ Kinds for which the surrounding whitespace is not checked """ const EXCLUDED_KINDS = [":", "."] -function init(this::Check, ctxt::AnalysisContext)::Nothing +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action( ctxt, _node_applicable, diff --git a/checks/StructMembersAreInLowerSnakeCase.jl b/checks/StructMembersAreInLowerSnakeCase.jl index 9b7f2cf..d0398ff 100644 --- a/checks/StructMembersAreInLowerSnakeCase.jl +++ b/checks/StructMembersAreInLowerSnakeCase.jl @@ -5,19 +5,20 @@ include("_common.jl") using ...Properties: find_lhs_of_kind, is_lower_snake, get_struct_members struct Check<:Analysis.Check end -id(::Check) = "struct-members-are-in-lower-snake-case" -severity(::Check) = 8 -synopsis(::Check) = "Struct members should be in \"lower_snake_case\"." +Analysis.id(::Check) = "struct-members-are-in-lower-snake-case" +Analysis.severity(::Check) = 8 +Analysis.synopsis(::Check) = "Struct members should be in \"lower_snake_case\"." -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, n -> kind(n) === K"struct", node -> begin for field in get_struct_members(node) - check(this, ctxt, field) + _check(this, ctxt, field) end end) + return nothing end -function check(this::Check, ctxt::AnalysisContext, field::SyntaxNode) +function _check(this::Check, ctxt::AnalysisContext, field::SyntaxNode)::Nothing @assert kind(field.parent) == K"block" && kind(field.parent.parent) == K"struct" "Expected a node representing" * " a field (child of a [struct])" field.parent @@ -31,6 +32,7 @@ function check(this::Check, ctxt::AnalysisContext, field::SyntaxNode) if !is_lower_snake(string(field_name)) report_violation(ctxt, this, field_name, "Field '$field_name' not in \"lower_snake_case\"") end + return nothing end end # module StructMembersAreInLowerSnakeCase diff --git a/checks/TooManyTypesInUnions.jl b/checks/TooManyTypesInUnions.jl index 54df9e9..fc3b531 100644 --- a/checks/TooManyTypesInUnions.jl +++ b/checks/TooManyTypesInUnions.jl @@ -4,13 +4,14 @@ include("_common.jl") using ...Properties: is_union_decl struct Check<:Analysis.Check end -id(::Check) = "too-many-types-in-unions" -severity(::Check) = 6 -synopsis(::Check) = "Too many types in Unions" +Analysis.id(::Check) = "too-many-types-in-unions" +Analysis.severity(::Check) = 6 +Analysis.synopsis(::Check) = "Too many types in Unions" +""" Maximum number of generic arguments in a Union type. """ const MAX_UNION_TYPES = 4 -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, is_union_decl, node -> begin local union_types = children(node)[2:end] # discard the 1st, which is "Union" local count = length(union_types) @@ -18,6 +19,7 @@ function init(this::Check, ctxt::AnalysisContext) report_violation(ctxt, this, node, "Union has too many types ($count > $MAX_UNION_TYPES).") end end) + return nothing end -end +end # module TooManyTypesInUnions diff --git a/checks/TypeNamesUpperCamelCase.jl b/checks/TypeNamesUpperCamelCase.jl index 13eb856..eaceba6 100644 --- a/checks/TypeNamesUpperCamelCase.jl +++ b/checks/TypeNamesUpperCamelCase.jl @@ -5,20 +5,21 @@ include("_common.jl") using ...Properties: is_upper_camel_case, find_lhs_of_kind struct Check<:Analysis.Check end -id(::Check) = "type-names-upper-camel-case" -severity(::Check) = 3 -synopsis(::Check) = "Type names should be in \"UpperCamelCase\"" +Analysis.id(::Check) = "type-names-upper-camel-case" +Analysis.severity(::Check) = 3 +Analysis.synopsis(::Check) = "Type names should be in \"UpperCamelCase\"" -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, n -> kind(n) ∈ KSet"abstract struct", node -> begin identifier = find_lhs_of_kind(K"Identifier", node) - if identifier !== nothing + if ! isnothing(identifier) name = string(identifier) if ! is_upper_camel_case(name) report_violation(ctxt, this, identifier, "Type names such as '$name' should be written in \"UpperCamelCase\".") end end end) + return nothing end end # module TypeNamesUpperCamelCase diff --git a/checks/UnderscorePrefixForPrivateFunctions.jl b/checks/UnderscorePrefixForPrivateFunctions.jl index 2645c58..694968f 100644 --- a/checks/UnderscorePrefixForPrivateFunctions.jl +++ b/checks/UnderscorePrefixForPrivateFunctions.jl @@ -5,11 +5,13 @@ include("_common.jl") using ...Properties: get_func_name, is_export, is_function, is_module struct Check<:Analysis.Check end -id(::Check) = "underscore-prefix-for-private-functions" -severity(::Check) = 8 -synopsis(::Check) = "Private functions are prefixed with one underscore _ character." +Analysis.id(::Check) = "underscore-prefix-for-private-functions" +Analysis.severity(::Check) = 8 +function Analysis.synopsis(::Check)::String + return "Private functions are prefixed with one underscore _ character." +end -function init(this::Check, ctxt::AnalysisContext)::Nothing +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, is_module, n -> _check(this, ctxt, n)) return nothing end diff --git a/checks/UseAmericanEnglish.jl b/checks/UseAmericanEnglish.jl index 71a536a..dacb417 100644 --- a/checks/UseAmericanEnglish.jl +++ b/checks/UseAmericanEnglish.jl @@ -6,11 +6,11 @@ using JuliaSyntax: byte_range include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "use-american-english" -severity(::Check) = 9 -synopsis(::Check) = "Comments should be in the American-English language" +Analysis.id(::Check) = "use-american-english" +Analysis.severity(::Check) = 9 +Analysis.synopsis(::Check) = "Comments should be in the American-English language" -function init(this::Check, ctxt::AnalysisContext)::Nothing +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing forbidden_words = _load_words(joinpath(@__DIR__, "_config", "words_en-GB.txt")) register_syntaxnode_action(ctxt, contains_comments, n -> _check_comment(this, ctxt, n, forbidden_words)) register_syntaxnode_action(ctxt, n -> kind(n) == K"doc", n -> _check_docstring(this, ctxt, n, forbidden_words)) diff --git a/checks/UseEachindexToIterateIndices.jl b/checks/UseEachindexToIterateIndices.jl index 234f3da..b059852 100644 --- a/checks/UseEachindexToIterateIndices.jl +++ b/checks/UseEachindexToIterateIndices.jl @@ -7,12 +7,15 @@ using ...Properties: get_iteration_parts, is_range using ...SyntaxNodeHelpers: find_descendants struct Check<:Analysis.Check end -id(::Check) = "use-eachindex-to-iterate-indices" -severity(::Check) = 5 -synopsis(::Check) = "Use eachindex() instead of a constructed range for iteration over a collection." +Analysis.id(::Check) = "use-eachindex-to-iterate-indices" +Analysis.severity(::Check) = 5 +function Analysis.synopsis(::Check)::String + return "Use eachindex() instead of a constructed range for iteration over a collection." +end -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, n -> kind(n) == K"for", node -> _check(this, ctxt, node)) + return nothing end function _check(this::Check, ctxt::AnalysisContext, for_node::SyntaxNode)::Nothing diff --git a/checks/UseIsinfToCheckForInfinite.jl b/checks/UseIsinfToCheckForInfinite.jl index 732673f..002e525 100644 --- a/checks/UseIsinfToCheckForInfinite.jl +++ b/checks/UseIsinfToCheckForInfinite.jl @@ -6,11 +6,11 @@ using ...SyntaxNodeHelpers include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "use-isinf-to-check-for-infinite" -severity(::Check) = 3 -synopsis(::Check) = "Use isinf to check for infinite values" +Analysis.id(::Check) = "use-isinf-to-check-for-infinite" +Analysis.severity(::Check) = 3 +Analysis.synopsis(::Check) = "Use isinf to check for infinite values" -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, is_eq_neq_comparison, node -> begin apply_to_operands(node, node -> begin if extract_special_value(node) ∈ SyntaxNodeHelpers.INF_VALUES @@ -18,6 +18,7 @@ function init(this::Check, ctxt::AnalysisContext) end end) end) + return nothing end end # module UseIsinfToCheckForInfinite diff --git a/checks/UseIsmissingToCheckForMissingValues.jl b/checks/UseIsmissingToCheckForMissingValues.jl index e9c6828..651c2ee 100644 --- a/checks/UseIsmissingToCheckForMissingValues.jl +++ b/checks/UseIsmissingToCheckForMissingValues.jl @@ -6,11 +6,11 @@ using ...SyntaxNodeHelpers include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "use-ismissing-to-check-for-missing-values" -severity(::Check) = 3 -synopsis(::Check) = "Use ismissing to check for missing values" +Analysis.id(::Check) = "use-ismissing-to-check-for-missing-values" +Analysis.severity(::Check) = 3 +Analysis.synopsis(::Check) = "Use ismissing to check for missing values" -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, is_eq_neq_comparison, node -> begin apply_to_operands(node, node -> begin if extract_special_value(node) ∈ SyntaxNodeHelpers.MISSING_VALUES @@ -18,6 +18,7 @@ function init(this::Check, ctxt::AnalysisContext) end end) end) + return nothing end end # module UseIsmissingToCheckForMissingValues diff --git a/checks/UseIsnanToCheckForNan.jl b/checks/UseIsnanToCheckForNan.jl index c423d4e..0922794 100644 --- a/checks/UseIsnanToCheckForNan.jl +++ b/checks/UseIsnanToCheckForNan.jl @@ -6,11 +6,11 @@ using ...SyntaxNodeHelpers include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "use-isnan-to-check-for-nan" -severity(::Check) = 3 -synopsis(::Check) = "Use isnan to check for not-a-number values" +Analysis.id(::Check) = "use-isnan-to-check-for-nan" +Analysis.severity(::Check) = 3 +Analysis.synopsis(::Check) = "Use isnan to check for not-a-number values" -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, is_eq_neq_comparison, node -> begin apply_to_operands(node, node -> begin if extract_special_value(node) ∈ SyntaxNodeHelpers.NAN_VALUES @@ -18,6 +18,7 @@ function init(this::Check, ctxt::AnalysisContext) end end) end) + return nothing end end # module UseIsnanToCheckForNan diff --git a/checks/UseIsnothingToCheckForNothingValues.jl b/checks/UseIsnothingToCheckForNothingValues.jl index d323c53..1a7b269 100644 --- a/checks/UseIsnothingToCheckForNothingValues.jl +++ b/checks/UseIsnothingToCheckForNothingValues.jl @@ -6,11 +6,11 @@ using ...Properties: is_eq_neq_comparison using ...SyntaxNodeHelpers: apply_to_operands, extract_special_value struct Check<:Analysis.Check end -id(::Check) = "use-isnothing-to-check-for-nothing-values" -severity(::Check) = 3 -synopsis(::Check) = "Use isnothing to check variables for nothing" +Analysis.id(::Check) = "use-isnothing-to-check-for-nothing-values" +Analysis.severity(::Check) = 3 +Analysis.synopsis(::Check) = "Use isnothing to check variables for nothing" -function init(this::Check, ctxt::AnalysisContext)::Nothing +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, is_eq_neq_comparison, node -> begin apply_to_operands(node, node -> begin if extract_special_value(node) == "nothing" diff --git a/checks/UseSpacesInsteadOfTabs.jl b/checks/UseSpacesInsteadOfTabs.jl index 0360603..0d4b7c4 100644 --- a/checks/UseSpacesInsteadOfTabs.jl +++ b/checks/UseSpacesInsteadOfTabs.jl @@ -3,30 +3,32 @@ module UseSpacesInsteadOfTabs include("_common.jl") struct Check<:Analysis.Check end -id(::Check) = "use-spaces-instead-of-tabs" -severity(::Check) = 7 -synopsis(::Check) = "Use spaces instead of tabs for indentation" +Analysis.id(::Check) = "use-spaces-instead-of-tabs" +Analysis.severity(::Check) = 7 +Analysis.synopsis(::Check) = "Use spaces instead of tabs for indentation" -const REGEX = r"(\s*)\t+.*" +""" Regular expression matching spaces at the beginning of the string. """ +const REGEX = r"^(\s*)\t+.*" -function init(this::Check, ctxt::AnalysisContext) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing register_syntaxnode_action(ctxt, n -> kind(n) == K"toplevel", node -> begin code = node.source.code starts = node.source.line_starts successive_pairs = collect(zip(starts, Iterators.drop(starts, 1))) linenr::Int = 1 - for (start,stop) in successive_pairs + for (start, stop) in successive_pairs line::String = code[start:prevind(code, stop)] m = match(REGEX, line) - if m !== nothing + if ! isnothing(m) offset::Int = length(m.captures[1]) - linepos = (linenr, offset+1) + linepos = (linenr, offset + 1) bufferrange = range(start + offset, length=1) report_violation(ctxt, this, linepos, bufferrange, synopsis(this)) end linenr += 1 end end) + return nothing end end # module UseSpacesInsteadOfTabs diff --git a/checks/VariablesHaveFixedTypes.jl b/checks/VariablesHaveFixedTypes.jl index c32fcc0..c12e4df 100644 --- a/checks/VariablesHaveFixedTypes.jl +++ b/checks/VariablesHaveFixedTypes.jl @@ -21,16 +21,16 @@ and doing this completely would be a _lot_ of specific cases. =# struct Check<:Analysis.Check end -id(::Check) = "variables-have-fixed-types" -severity(::Check) = 3 -synopsis(::Check) = "Types of variables should not change." +Analysis.id(::Check) = "variables-have-fixed-types" +Analysis.severity(::Check) = 3 +Analysis.synopsis(::Check) = "Types of variables should not change." -function init(this::Check, ctxt::AnalysisContext)::Nothing - register_syntaxnode_action(ctxt, is_assignment, n -> check(this, ctxt, n)) +function Analysis.init(this::Check, ctxt::AnalysisContext)::Nothing + register_syntaxnode_action(ctxt, is_assignment, n -> _check(this, ctxt, n)) return nothing end -function check(this::Check, ctxt::AnalysisContext, assignment_node::SyntaxNode)::Nothing +function _check(this::Check, ctxt::AnalysisContext, assignment_node::SyntaxNode)::Nothing if type_has_changed_from_init(ctxt.symboltable, assignment_node) assigned_variable = get_var_from_assignment(assignment_node) if isnothing(assigned_variable) @@ -39,7 +39,8 @@ function check(this::Check, ctxt::AnalysisContext, assignment_node::SyntaxNode): initial_type = get_initial_type_of_node(ctxt.symboltable, assignment_node) current_type = get_variable_type_from_node(assignment_node) report_violation(ctxt, this, assignment_node, - "Variable '$assigned_variable' has changed type (from $initial_type to $current_type).") + "Variable '$assigned_variable' has changed type (from $initial_type to $current_type)." + ) end return nothing end diff --git a/checks/_common.jl b/checks/_common.jl index d9d0e3b..dd6784e 100644 --- a/checks/_common.jl +++ b/checks/_common.jl @@ -1,3 +1,2 @@ -import ..Analysis: id, init, synopsis, severity using ..Analysis using JuliaSyntax: SyntaxNode, @K_str, @KSet_str, kind, children, numchildren, source_location diff --git a/src/Analysis.jl b/src/Analysis.jl index e3f0cd5..7e668f0 100644 --- a/src/Analysis.jl +++ b/src/Analysis.jl @@ -1,14 +1,15 @@ module Analysis +using JuliaSyntax + +import JuliaSyntax: SyntaxNode, GreenNode, kind, sourcetext, source_location +import InteractiveUtils: subtypes + export AnalysisContext, Violation, run_analysis, register_syntaxnode_action, report_violation export Check, id, synopsis, severity, init export GreenLeaf, find_greenleaf, kind, sourcetext export dfs_traversal, find_syntaxnode_at_position, source_location -using JuliaSyntax - -import JuliaSyntax: SyntaxNode, GreenNode, Kind, kind, sourcetext, source_location -import InteractiveUtils: subtypes # Here to keep Properties importable as ..Properties by SymbolTable. # Mainly to ensure that it's imported in the same way by both @@ -26,7 +27,7 @@ init(this::Check, ctxt) = error("init() not implemented for this check") struct Violation check::Check sourcefile::SourceFile - linepos::Tuple{Int,Int} # The line and column of the violation + linepos::Tuple{Int, Int} # The line and column of the violation bufferrange::UnitRange{Integer} # The character range in the source code msg::String end @@ -42,6 +43,9 @@ sourcetext(gl::GreenLeaf)::String = gl.sourcefile.code[gl.range] kind(gl::GreenLeaf) = kind(gl.node) source_location(gl::GreenLeaf) = source_location(gl.sourcefile, gl.range.start) +const NullableGreenLeaf = Union{GreenLeaf, Nothing} +const NullableSyntaxNode = Union{SyntaxNode, Nothing} + struct CheckRegistration predicate::Function # A predicate function that determines if the action applies to a SyntaxNode action::Function # The action to be performed on SyntaxNode when the predicate applies @@ -54,16 +58,18 @@ struct AnalysisContext violations::Vector{Violation} symboltable::SymbolTableStruct - AnalysisContext(node::SyntaxNode, greenLeaves::Vector{GreenLeaf}) = new(node, greenLeaves, CheckRegistration[], Violation[], SymbolTableStruct()) + function AnalysisContext(node::SyntaxNode, greenLeaves::Vector{GreenLeaf}) + return new(node, greenLeaves, CheckRegistration[], Violation[], SymbolTableStruct()) + end end "Finds GreenLeaf containing given position." -function find_greenleaf(ctxt::AnalysisContext, pos::Int)::Union{GreenLeaf, Nothing} +function find_greenleaf(ctxt::AnalysisContext, pos::Int)::NullableGreenLeaf return _find_greenleaf(ctxt.greenleaves, pos) end "Performs a binary search to find the GreenLeaf containing given position." -function _find_greenleaf(leaves::Vector{GreenLeaf}, pos::Int)::Union{GreenLeaf, Nothing} +function _find_greenleaf(leaves::Vector{GreenLeaf}, pos::Int)::NullableGreenLeaf low = 1 high = length(leaves) while low <= high @@ -82,12 +88,12 @@ function _find_greenleaf(leaves::Vector{GreenLeaf}, pos::Int)::Union{GreenLeaf, return nothing end -function _get_green_leaves!(list::Vector{GreenLeaf}, sf::SourceFile, node::GreenNode, pos::Int) +function _get_green_leaves!(list::Vector{GreenLeaf}, sf::SourceFile, node::GreenNode, pos::Int)::Nothing cs = children(node) - if cs === nothing + if isnothing(cs) rng = range(pos, prevind(sf.code, pos + node.span)) push!(list, GreenLeaf(sf, node, rng)) - return + return nothing end p = pos @@ -95,6 +101,7 @@ function _get_green_leaves!(list::Vector{GreenLeaf}, sf::SourceFile, node::Green _get_green_leaves!(list, sf, child, p) p += child.span end + return nothing end function _get_green_leaves(node::SyntaxNode)::Vector{GreenLeaf} @@ -104,11 +111,11 @@ function _get_green_leaves(node::SyntaxNode)::Vector{GreenLeaf} end """ - find_syntaxnode_at_position(node::SyntaxNode, pos::Integer)::Union{SyntaxNode, Nothing} + find_syntaxnode_at_position(node::SyntaxNode, pos::Integer)::NullableSyntaxNode Finds the most specific SyntaxNode that spans the given character position `pos`. """ -function find_syntaxnode_at_position(node::SyntaxNode, pos::Integer)::Union{SyntaxNode, Nothing} +function find_syntaxnode_at_position(node::SyntaxNode, pos::Integer)::NullableSyntaxNode # Check if the current node's range contains the position. if ! (pos in JuliaSyntax.byte_range(node)) return nothing @@ -117,7 +124,7 @@ function find_syntaxnode_at_position(node::SyntaxNode, pos::Integer)::Union{Synt # Iterate through children to find a more specific node. for child in something(children(node), []) found_child = find_syntaxnode_at_position(child, pos) - if found_child !== nothing + if ! isnothing(found_child) return found_child end end @@ -127,11 +134,11 @@ function find_syntaxnode_at_position(node::SyntaxNode, pos::Integer)::Union{Synt end """ - find_syntaxnode_at_position(ctxt::AnalysisContext, pos::Integer)::Union{SyntaxNode, Nothing} + find_syntaxnode_at_position(ctxt::AnalysisContext, pos::Integer)::NullableSyntaxNode Finds the most specific SyntaxNode that spans the given character position `pos`. """ -function find_syntaxnode_at_position(ctxt::AnalysisContext, pos::Integer)::Union{SyntaxNode, Nothing} +function find_syntaxnode_at_position(ctxt::AnalysisContext, pos::Integer)::NullableSyntaxNode return find_syntaxnode_at_position(ctxt.rootNode, pos) end @@ -148,12 +155,12 @@ end Use `offsetspan` to specify the range of the violation relative to the node's position. """ function report_violation(ctxt::AnalysisContext, check::Check, node::SyntaxNode, msg::String; - offsetspan::Union{Nothing, Tuple{Int,Int}}=nothing + offsetspan::Union{Nothing, Tuple{Int, Int}}=nothing )::Nothing linepos = JuliaSyntax.source_location(node) bufferrange = JuliaSyntax.byte_range(node) - if offsetspan !== nothing + if ! isnothing(offsetspan) bufferrange = range(bufferrange.start + offsetspan[1], length=offsetspan[2]) end @@ -165,7 +172,7 @@ end Reports a violation on location `linepos` and range `bufferrange` in the current context. """ function report_violation(ctxt::AnalysisContext, check::Check, - linepos::Tuple{Int,Int}, + linepos::Tuple{Int, Int}, bufferrange::UnitRange{Int}, msg::String )::Nothing @@ -211,7 +218,7 @@ function dfs_traversal(ctxt::AnalysisContext, node::SyntaxNode, visitor_func::Fu # 3. Recursively visit children local children = JuliaSyntax.children(node) - if children === nothing + if isnothing(children) return end for child_node in children @@ -240,7 +247,7 @@ function discover_checks()::Nothing end function _invoke_checks(ctxt::AnalysisContext, node::SyntaxNode)::Nothing - visitor = function(n::SyntaxNode) + visitor(n::SyntaxNode)::Nothing = begin for reg in ctxt.registrations if reg.predicate(n) #println("Invoking action for node type: ", reg.nodeType) @@ -249,6 +256,7 @@ function _invoke_checks(ctxt::AnalysisContext, node::SyntaxNode)::Nothing #println("Not a match: $(reg.nodeType) vs $(kind(n))") end end + return nothing end # TODO: Is the enter and exit on the main level really necessary? @@ -259,8 +267,8 @@ function _invoke_checks(ctxt::AnalysisContext, node::SyntaxNode)::Nothing end function run_analysis(sourcefile::SourceFile, checks::Vector{Check}; - print_ast::Bool = false, - print_llt::Bool = false, + print_ast::Bool=false, + print_llt::Bool=false, )::Vector{Violation} if length(checks) >= 1 diff --git a/src/AnalysisDemo.jl b/src/AnalysisDemo.jl deleted file mode 100644 index 79fdded..0000000 --- a/src/AnalysisDemo.jl +++ /dev/null @@ -1,25 +0,0 @@ -# This file can be used to invoke a single rule on a piece of code, useful during development -using InteractiveUtils - -using JuliaCheck - -using JuliaSyntax: SourceFile -using JuliaCheck.Analysis -using JuliaCheck.Analysis: Check, id -using JuliaCheck.Output: print_violations - -global checks = map(c -> c(), subtypes(Check)) -target_check = "type-names-upper-camel-case" -global checks1 = filter(c -> id(c) === target_check, checks) -@assert length(checks1) == 1 - -text = """ -struct transX end -struct TransX end -""" - -sourcefile = SourceFile(text, filename="dummy.jl") -printer = JuliaCheck.Output.select_violation_printer("highlighting") -violations = Analysis.run_analysis(sourcefile, checks1; print_ast=true, print_llt=true) -output_file_arg = "" -print_violations(printer, output_file_arg, violations) diff --git a/src/CommentHelpers.jl b/src/CommentHelpers.jl index a5d6ea9..0cd7c14 100644 --- a/src/CommentHelpers.jl +++ b/src/CommentHelpers.jl @@ -3,7 +3,7 @@ module CommentHelpers using JuliaSyntax: @K_str, @KSet_str, SyntaxNode, kind, child_position_span, view, JuliaSyntax as JS using ..WhitespaceHelpers: combine_ranges, normalized_green_child_range -export Comment, CommentBlock, get_comment_blocks, get_range, get_text, contains_comments +export Comment, CommentBlock, get_comments, get_comment_blocks, get_range, get_text, contains_comments struct Comment range::UnitRange @@ -23,7 +23,7 @@ function contains_comments(sn::SyntaxNode)::Bool end function get_comments(sn::SyntaxNode)::Vector{Comment} - comments = [] + comments = Vector{Comment}() for (i, ch) in enumerate(sn.raw.children) if kind(ch) == K"Comment" range = normalized_green_child_range(sn, i) @@ -38,9 +38,9 @@ Get the range and text representation of the direct children that are comment no Subsequent single-line comments are merged. Only sibling comments can ever belong to the same block. """ function get_comment_blocks(sn::SyntaxNode)::Vector{CommentBlock} - blocks = [] + blocks = Vector{CommentBlock}() green_children = sn.raw.children - curblock = [] + curblock = Vector{Comment}() # Iterate through green children, combining consecutive comment siblings into blocks # if there is only whitespace between them for (i, ch) in enumerate(green_children) diff --git a/src/JuliaCheck.jl b/src/JuliaCheck.jl index e403228..59b0293 100755 --- a/src/JuliaCheck.jl +++ b/src/JuliaCheck.jl @@ -4,15 +4,15 @@ using JuliaSyntax: first_byte, last_byte, SourceFile using ArgParse: ArgParseSettings, project_version, @add_arg_table!, parse_args using InteractiveUtils -include("Properties.jl"); import .Properties -include("TypeHelpers.jl"); import .TypeHelpers +include("Properties.jl") +include("TypeHelpers.jl") include("SyntaxNodeHelpers.jl") include("SymbolTable.jl") include("Analysis.jl") include("Output.jl") include("MutatingFunctionsHelpers.jl") -include("WhitespaceHelpers.jl"); import .WhitespaceHelpers -include("CommentHelpers.jl"); import .CommentHelpers +include("WhitespaceHelpers.jl") +include("CommentHelpers.jl") using .Analysis using .Output @@ -64,7 +64,7 @@ function _parse_commandline(args::Vector{String}) return parse_args(args, s) end -function main(args::Vector{String}) +function main(args::Vector{String})::Nothing if isempty(args) _parse_commandline(["-h"]) return nothing @@ -106,19 +106,22 @@ function main(args::Vector{String}) fresh_checks::Vector{Check} = map(type -> typeof(type)(), checks_to_run) new_violations = Analysis.run_analysis(sourcefile, fresh_checks; - print_ast = arguments["ast"], - print_llt = arguments["llt"]) + print_ast=arguments["ast"], + print_llt=arguments["llt"]) append!(violations, new_violations) end end print_violations(violation_printer, output_file_arg, violations) println() + return nothing end -_has_julia_ext(file_arg::String)::Bool = lowercase(splitext(file_arg)[end]) == ".jl" +function _has_julia_ext(file_arg::String)::Bool + return lowercase(splitext(file_arg)[end]) == ".jl" +end function _get_files_to_analyze(file_arg::Vector{String})::Vector{String} - file_set = [] + file_set = Vector{String}() for element in file_arg if isfile(element) && _has_julia_ext(element) push!(file_set, element) diff --git a/src/MutatingFunctionsHelpers.jl b/src/MutatingFunctionsHelpers.jl index cec7c98..788f685 100644 --- a/src/MutatingFunctionsHelpers.jl +++ b/src/MutatingFunctionsHelpers.jl @@ -17,7 +17,7 @@ assignments and call that mutate a variable. Currently, this list is: """ function get_mutated_variables_in_scope(ctxt::AnalysisContext, scope_node::SyntaxNode)::Set{String} all_mutated_variables = Set{String}() - visitor_func = function(n::SyntaxNode) + visitor_func(n::SyntaxNode)::Nothing = begin if is_array_assignment(n) mutated_var = string(first(children(n))) push!(all_mutated_variables, mutated_var) @@ -32,6 +32,7 @@ function get_mutated_variables_in_scope(ctxt::AnalysisContext, scope_node::Synta mutated_var = string(first(children(field_assignment))) push!(all_mutated_variables, mutated_var) end + return nothing end Analysis.dfs_traversal(ctxt, scope_node, visitor_func) return all_mutated_variables diff --git a/src/Output.jl b/src/Output.jl index 008bcc5..ae76e9e 100644 --- a/src/Output.jl +++ b/src/Output.jl @@ -1,7 +1,7 @@ module Output export ViolationPrinter, get_available_printers, shorthand, requiresfile, print_violations, - select_violation_printer, parse_output_file_arg + select_violation_printer, parse_output_file_arg import ..Analysis: Violation import InteractiveUtils: subtypes diff --git a/src/Properties.jl b/src/Properties.jl index 0259e83..7205e55 100644 --- a/src/Properties.jl +++ b/src/Properties.jl @@ -3,7 +3,7 @@ module Properties import JuliaSyntax: Kind, GreenNode, SyntaxNode, SourceFile, @K_str, @KSet_str, head, is_dotted, is_leaf, kind, numchildren, sourcetext, span, untokenize, JuliaSyntax as JS -export AnyTree, NullableNode, EOL, MAX_LINE_LENGTH, +export AnyTree, NullableNode, MAX_LINE_LENGTH, children, closes_module, closes_scope, expr_depth, expr_size, @@ -33,10 +33,9 @@ const NullableString = Union{String, Nothing} const NullableNode = Union{AnyTree, Nothing} const NodeAndString = Tuple{AnyTree, NullableString} - ## Global definitions +""" Maximum allowed length of a source line. """ const MAX_LINE_LENGTH = 92 -const EOL = (Sys.iswindows() ? "\n\r" : "\n") ## Functions @@ -108,7 +107,7 @@ function is_global_decl(node::AnyTree)::Bool end function is_constant(node::AnyTree)::Bool return kind(node) == K"const" || - (kind(node) == K"global" && haschildren(node) && + (kind(node) == K"global" && haschildren(node) && kind(children(node)[1]) == K"const") end @@ -122,7 +121,7 @@ end function is_operator(node::AnyTree)::Bool return JS.is_prefix_op_call(node) || - is_infix_operator(node) || + is_infix_operator(node) || JS.is_postfix_op_call(node) end function is_infix_operator(node::AnyTree)::Bool @@ -146,7 +145,7 @@ function is_range(node::SyntaxNode)::Bool (kind(kids[1]) == K"Identifier" && string(kids[1]) == "range") || (kind(kids[2]) == K"Identifier" && string(kids[2]) == ":") - ) + ) end return false end @@ -171,7 +170,7 @@ end function opens_scope(node::SyntaxNode) return is_function(node) || - kind(node) ∈ [KSet"for while try do let macro generator"] + kind(node) ∈ [KSet"for while try do let macro generator"] # comprehensions contain a generator end function closes_scope(node::SyntaxNode) @@ -225,11 +224,13 @@ function get_func_arguments(node::SyntaxNode)::Vector{SyntaxNode} # Probably a function "stub", which declares a function name but no methods. return [] end - # This returns all the arguments without any further processing. - # As such, this may contain: - # - only positional arguments (direct children) - # - only keyword arguments (grandchildren, children of a parameters node) - # - both (combination of children and parameters->grandchildren) + #= + This returns all the arguments without any further processing. + As such, this may contain: + - only positional arguments (direct children) + - only keyword arguments (grandchildren, children of a parameters node) + - both (combination of children and parameters->grandchildren) + =# return children(call)[2:end] # discard the function's name (1st identifier in this list) end @@ -313,7 +314,7 @@ function get_imported_pkg(node::SyntaxNode)::String else pkg = children(node)[1] if kind(pkg) == K":" || # importing/using items from a package - kind(pkg) == K"as" # import with an alias + kind(pkg) == K"as" # import with an alias pkg = children(pkg)[1] end @assert kind(pkg) == K"importpath" @@ -370,7 +371,7 @@ of whether there are still named arguments in there. """ function get_flattened_fn_arg_nodes(function_node::SyntaxNode)::Vector{SyntaxNode} func_arguments = get_func_arguments(function_node) - func_arg_nodes = [] + func_arg_nodes = Vector{SyntaxNode}() for arg in func_arguments # Parameters signifies keyword (also known as named) arguments. # All named arguments are then reported in subnodes. For now, we don't @@ -498,8 +499,8 @@ returned parts are `nothing` (but it is still a pair). function get_iteration_parts(for_loop::SyntaxNode)::Tuple{NullableNode, NullableNode} if kind(for_loop) == K"for" if !(haschildren(for_loop) && - kind(first_child(for_loop)) == K"iteration" - ) + kind(first_child(for_loop)) == K"iteration" + ) @debug "for loop does not have an [iteration]" for_loop return nothing, nothing end diff --git a/src/SymbolTable.jl b/src/SymbolTable.jl index b27882a..abcb576 100644 --- a/src/SymbolTable.jl +++ b/src/SymbolTable.jl @@ -32,9 +32,10 @@ When searching for a symbol, we scan the stack of scopes of the current module, top to bottom. Symbols from other modules have to be qualified, or entered into the current module's global scope with a `using` declaration. =# +const Scope = Dict{String, SymbolTableItem} + +const NestedScopes = Stack{Scope} -Scope = Dict{String, SymbolTableItem} -NestedScopes = Stack{Scope} """ A module containing an identifier and a stack of scopes. @@ -88,8 +89,9 @@ Module 'Main' is always there, at the bottom of the stack of modules. This function makes sure to reflect that situation. """ -function enter_main_module!(table::SymbolTableStruct) +function enter_main_module!(table::SymbolTableStruct)::Nothing _enter_module!(table, "Main") + return nothing end """ @@ -134,18 +136,22 @@ scopes_within_module(table::SymbolTableStruct)::NestedScopes = _current_module(t _current_module(table::SymbolTableStruct)::Module = first(table.stack) -# TODO: a file can be `include`d into another, thus into another -# module and, what is most important from the point of view of the -# symbols table and declarations: something can be declared outside -# the file under analysis, and we will surely get confused about its -# scope. +#= +TODO: a file can be `include`d into another, thus into another +module and, what is most important from the point of view of the +symbols table and declarations: something can be declared outside +the file under analysis, and we will surely get confused about its +scope. +=# -function _enter_scope!(table::SymbolTableStruct) +function _enter_scope!(table::SymbolTableStruct)::Nothing push!(scopes_within_module(table), Scope()) + return nothing end -function _exit_scope!(table::SymbolTableStruct) +function _exit_scope!(table::SymbolTableStruct)::Nothing pop!(scopes_within_module(table)) + return nothing end _global_scope(table::SymbolTableStruct)::Scope = last(scopes_within_module(table)) @@ -189,13 +195,14 @@ Register an identifier. """ _declare!(table::SymbolTableStruct, symbol::SyntaxNode) = _declare_on_scope!(_current_scope(table), symbol, nothing) -function _declare_on_scope!(scp::Scope, node::SyntaxNode, type_spec::TypeSpecifier) +function _declare_on_scope!(scp::Scope, node::SyntaxNode, type_spec::TypeSpecifier)::Nothing symbol_id = _get_symbol_id(node) if haskey(scp, symbol_id) push!(scp[symbol_id].all_nodes, node) else scp[symbol_id] = SymbolTableItem(node, type_spec) end + return nothing end @@ -218,13 +225,13 @@ the abstract syntax tree. When a node is hit, this ensures that the syntax tree is updated as expected. The reason why this cannot easily be done as a part of other functionality -(for example, also making this use predicate behaviour like the rules do) +(for example, also making this use predicate behavior like the rules do) is that there is also a necessity to have this work on _exiting_ a node while preserving the state in between. Currently logs new modules, functions, and (global) variables. """ -function update_symbol_table_on_node_enter!(table::SymbolTableStruct, node::SyntaxNode) +function update_symbol_table_on_node_enter!(table::SymbolTableStruct, node::SyntaxNode)::Nothing if is_module(node) _enter_module!(table, node) elseif is_function(node) @@ -237,9 +244,10 @@ function update_symbol_table_on_node_enter!(table::SymbolTableStruct, node::Synt scope = is_assignment_to_global ? _global_scope(table) : _current_scope(table) _process_assignment!(scope, node) end + return nothing end -function _process_function!(table::SymbolTableStruct, node::SyntaxNode) +function _process_function!(table::SymbolTableStruct, node::SyntaxNode)::Nothing fname = get_func_name(node) if !isnothing(fname) if kind(fname) == K"Identifier" @@ -260,9 +268,10 @@ function _process_function!(table::SymbolTableStruct, node::SyntaxNode) _process_argument!(table, arg) end end + return nothing end -function _process_global!(table::SymbolTableStruct, node::SyntaxNode) +function _process_global!(table::SymbolTableStruct, node::SyntaxNode)::Nothing # Handle statements like `global x, y = 1, 2` # We need to handle assignment here and cannot wait until the descendant 'assignment' expression # is encountered in `update_symbol_table_on_node_enter!`, because the check might listen to `is_global_decl` event. @@ -277,17 +286,18 @@ function _process_global!(table::SymbolTableStruct, node::SyntaxNode) _declare_global!(table, c) end end + return nothing end -function _process_argument!(table::SymbolTableStruct, node::SyntaxNode) +function _process_argument!(table::SymbolTableStruct, node::SyntaxNode)::Nothing arg = find_lhs_of_kind(K"Identifier", node) - if isnothing(arg) - return nothing + if ! isnothing(arg) + _declare!(table, arg) end - _declare!(table, arg) + return nothing end -function _process_assignment!(scope::Scope, node::SyntaxNode) +function _process_assignment!(scope::Scope, node::SyntaxNode)::Nothing @assert kind(node) == K"=" "Expected a [=] node, got [$(kind(node))]." assignees = get_all_assignees(node) if length(assignees) == 1 @@ -301,10 +311,12 @@ function _process_assignment!(scope::Scope, node::SyntaxNode) _declare_on_scope!(scope, var_node, nothing) end end + return nothing end -function _process_struct!(table::SymbolTableStruct, node::SyntaxNode) +function _process_struct!(table::SymbolTableStruct, node::SyntaxNode)::Nothing _declare!(table, find_lhs_of_kind(K"Identifier", node)) + return nothing end """ @@ -314,12 +326,13 @@ When a module or a scope-opening function is left, this is then used to exit scopes and move back to the table below it (so scoped variables within the current scope are no longer present then). """ -function update_symbol_table_on_node_leave!(table::SymbolTableStruct, node::SyntaxNode) +function update_symbol_table_on_node_leave!(table::SymbolTableStruct, node::SyntaxNode)::Nothing if is_module(node) exit_module!(table) elseif opens_scope(node) _exit_scope!(table) end + return nothing end function get_initial_type_of_node(table::SymbolTableStruct, assignment_node::SyntaxNode)::TypeSpecifier @@ -365,4 +378,4 @@ function print_state(table::SymbolTableStruct)::String return state end -end +end # module SymbolTable diff --git a/src/SyntaxNodeHelpers.jl b/src/SyntaxNodeHelpers.jl index 0df5f9f..a6c77f8 100644 --- a/src/SyntaxNodeHelpers.jl +++ b/src/SyntaxNodeHelpers.jl @@ -1,12 +1,12 @@ module SyntaxNodeHelpers -export ancestors, is_scope_construct, apply_to_operands, extract_special_value, find_node_at_position -export SpecialValue - using JuliaSyntax: SyntaxNode, GreenNode, kind, numchildren, children, source_location, is_operator, is_infix_op_call, is_prefix_op_call, byte_range, is_leaf import JuliaSyntax: @K_str, @KSet_str +export ancestors, is_scope_construct, apply_to_operands, extract_special_value, find_node_at_position +export SpecialValue + const AnyTree = Union{SyntaxNode, GreenNode} "Returns list of ancestors for given node, excluding self, ordered by increasing distance." @@ -38,10 +38,19 @@ function apply_to_operands(node::SyntaxNode, func::Function)::Nothing return nothing end +""" Identifiers representing Infinity. """ const INF_VALUES = Set(["Inf", "Inf16", "Inf32", "Inf64"]) + +""" Identifiers representing Not-a-Number floating point values. """ const NAN_VALUES = Set(["NaN", "NaN16", "NaN32", "NaN64"]) + +""" Identifiers representing Missing values. """ const MISSING_VALUES = Set(["missing", "Missing"]) + +""" Identifiers representing Nothing values. """ const NOTHING_VALUES = Set(["nothing", "Nothing"]) + +""" Set of all special values. """ const SPECIAL_VALUES = union(INF_VALUES, NAN_VALUES, MISSING_VALUES, NOTHING_VALUES) """ @@ -85,7 +94,7 @@ Return a list of all descendant nodes of the given node that match the predicate By default visits the full tree. Use `stop_traversal=true` to stop recursing into subtree when a node matches predicate. """ function find_descendants(pred::Function, node::AnyTree, stop_traversal::Bool = false)::Vector{AnyTree} - out = [] + out = Vector{AnyTree}() if pred(node) push!(out, node) if stop_traversal @@ -104,7 +113,7 @@ end Finds deepest node containing the given `pos`. If there is no `SyntaxNode` that contains the position, the `toplevel` node is returned. """ -function find_node_at_position(node::SyntaxNode, pos::Integer)::Union{SyntaxNode,Nothing} +function find_node_at_position(node::SyntaxNode, pos::Integer)::NullableSyntaxNode # Check if the current node contains the position if ! (pos in byte_range(node)) return nothing @@ -113,7 +122,7 @@ function find_node_at_position(node::SyntaxNode, pos::Integer)::Union{SyntaxNode # Search through children to find the most specific node for child in something(children(node), []) found_child = find_node_at_position(child, pos) - if found_child !== nothing + if !isnothing(found_child) return found_child end end diff --git a/src/TypeHelpers.jl b/src/TypeHelpers.jl index 422cf90..f29eb72 100644 --- a/src/TypeHelpers.jl +++ b/src/TypeHelpers.jl @@ -24,9 +24,9 @@ Currently, possible literals in JuliaSyntax are: "Char" "CmdString" -All of these are returned as kinds, and are flagged by the is_literal function. -=# -TypeSpecifier = Union{String, Nothing} +All of these are returned as kinds, and are flagged by the is_literal function. +=# +const TypeSpecifier = Union{String, Nothing} function is_different_type(type_1::TypeSpecifier, type_2::TypeSpecifier)::Bool if isnothing(type_1) || isnothing(type_2) @@ -38,7 +38,7 @@ end """ Tries to find the type of the associated variable from a node. -For now, this only covers assignments of the form +For now, this only covers assignments of the form a = /something/. """ function get_variable_type_from_node(node::SyntaxNode)::TypeSpecifier diff --git a/src/printers/HighlightingViolationPrinter.jl b/src/printers/HighlightingViolationPrinter.jl index 7b1c146..e8ad7cb 100644 --- a/src/printers/HighlightingViolationPrinter.jl +++ b/src/printers/HighlightingViolationPrinter.jl @@ -1,14 +1,14 @@ module HighlightingViolationPrinter -import ...Analysis: Violation, severity, id, synopsis -import ..Output: shorthand, requiresfile, print_violations; using ..Output -import JuliaSyntax: SourceFile, JuliaSyntax as JS +using ...Analysis: Violation, severity, id, synopsis +using JuliaSyntax: filename, highlight, SourceFile +using ..Output struct ViolationPrinter<:Output.ViolationPrinter end -shorthand(::ViolationPrinter) = "highlighting" -requiresfile(::ViolationPrinter) = false +Output.shorthand(::ViolationPrinter) = "highlighting" +Output.requiresfile(::ViolationPrinter) = false -function print_violations(this::ViolationPrinter, outputfile::String, violations::Vector{Violation})::Nothing +function Output.print_violations(this::ViolationPrinter, outputfile::String, violations::Vector{Violation})::Nothing append_period(s::String) = endswith(s, ".") ? s : s * "." for v in violations _report_violation( @@ -29,16 +29,17 @@ end function _report_violation(sourcefile::SourceFile; index::Int, len::Int, line::Int, col::Int, severity::Int, user_msg::String, summary::String, rule_id::String)::Nothing - printstyled("\n$(JS.filename(sourcefile))($line, $col):\n"; + printstyled("\n$(filename(sourcefile))($line, $col):\n"; underline=true) - JS.highlight(stdout, sourcefile, index:index+len-1; - note=user_msg, notecolor=:yellow, - context_lines_after=0, context_lines_before=0) + highlight(stdout, sourcefile, range(index; length=len); + note=user_msg, notecolor=:yellow, + context_lines_after=0, context_lines_before=0) printstyled("\n$summary"; color=:cyan) printstyled("\nRule:"; underline=true) printstyled(" $rule_id. ") printstyled("Severity:"; underline=true) printstyled(" $severity\n") + return nothing end end # module HighlightingViolationPrinter diff --git a/src/printers/JSONViolationPrinter.jl b/src/printers/JSONViolationPrinter.jl index 743df62..b935b62 100644 --- a/src/printers/JSONViolationPrinter.jl +++ b/src/printers/JSONViolationPrinter.jl @@ -1,13 +1,13 @@ module JSONViolationPrinter -import ...Analysis: Violation, severity, id, synopsis -import ..Output: shorthand, requiresfile, print_violations; using ..Output -import JuliaSyntax: filename, source_location, SourceFile -import JSON3 +using ...Analysis: Violation, severity, id, synopsis +using JSON3 +using JuliaSyntax: filename, source_location +using ..Output struct ViolationPrinter<:Output.ViolationPrinter end -shorthand(::ViolationPrinter) = "json" -requiresfile(::ViolationPrinter) = true +Output.shorthand(::ViolationPrinter) = "json" +Output.requiresfile(::ViolationPrinter) = true Base.@kwdef struct ViolationOutput line_start::Int64 @@ -22,9 +22,9 @@ Base.@kwdef struct ViolationOutput url::String end -function print_violations(this::ViolationPrinter, outputfile::String, violations::Vector{Violation})::Nothing +function Output.print_violations(this::ViolationPrinter, outputfile::String, violations::Vector{Violation})::Nothing append_period(s::String) = endswith(s, ".") ? s : s * "." - output_violations = [] + output_violations = Vector{ViolationOutput}() for v in violations l_end, c_end = source_location(v.sourcefile, v.bufferrange.stop) push!(output_violations, ViolationOutput( diff --git a/src/printers/SimpleViolationPrinter.jl b/src/printers/SimpleViolationPrinter.jl index 1540c35..4a72599 100644 --- a/src/printers/SimpleViolationPrinter.jl +++ b/src/printers/SimpleViolationPrinter.jl @@ -1,14 +1,13 @@ module SimpleViolationPrinter -import ...Analysis: Violation, severity, id -import ..Output: shorthand, requiresfile, print_violations; using ..Output -import JuliaSyntax: SourceFile +using ...Analysis: Violation, severity, id +using ..Output struct ViolationPrinter<:Output.ViolationPrinter end -shorthand(::ViolationPrinter) = "simple" -requiresfile(::ViolationPrinter) = false +Output.shorthand(::ViolationPrinter) = "simple" +Output.requiresfile(::ViolationPrinter) = false -function print_violations(this::ViolationPrinter, outputfile::String, violations::Vector{Violation})::Nothing +function Output.print_violations(this::ViolationPrinter, outputfile::String, violations::Vector{Violation})::Nothing if length(violations) == 0 println("No violations found.") else diff --git a/test/TestConsistentLineEndings.jl b/test/TestConsistentLineEndings.jl index 008a0b8..0b33dd8 100644 --- a/test/TestConsistentLineEndings.jl +++ b/test/TestConsistentLineEndings.jl @@ -6,7 +6,6 @@ prevent these from being auto-converted out by e.g. Git or VSCode. The line endings of this file itself should always be LF, see .gitattributes =# @testitem "ConsistentLineEndings.jl" begin - include("../src/JuliaCheck.jl") using .JuliaCheck.Analysis using .JuliaCheck.Output using JuliaSyntax: SourceFile @@ -80,7 +79,7 @@ The line endings of this file itself should always be LF, see .gitattributes printer = JuliaCheck.Output.select_violation_printer("highlighting") output_file_arg = "" result = IOCapture.capture() do - violations = Analysis.run_analysis(source, checks) + violations = Analysis.run_analysis(source, checks) print_violations(printer, output_file_arg, violations) end @test replace(result.output, r"\r\n?" => "\n") == exp # In the output, we do not need to compare line endings diff --git a/test/runtests.jl b/test/runtests.jl index cddb187..e561a67 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -127,7 +127,9 @@ end normalize(text) = strip(replace(text, "\r\n" => "\n", "\\" => "/")) * "\n" cd("res") do - @testset for printer in JuliaCheck.Output.get_available_printers() + printers = JuliaCheck.Output.get_available_printers() + @test length(printers) >= 3 + @testset for printer in printers printer_cmd = JuliaCheck.Output.shorthand(printer) printer_file = "ViolationPrinter-$(printer_cmd).out" val_file = "ViolationPrinter-$(printer_cmd).val" @@ -221,9 +223,10 @@ end @testitem "JuliaCheck self" begin import IOCapture isjuliafile = f -> endswith(f, ".jl") - checkfiles = filter(isjuliafile, readdir(joinpath(dirname(@__DIR__), "checks"), join=true)) - srcfiles = filter(isjuliafile, readdir(joinpath(dirname(@__DIR__), "src"), join=true)) - files = union(checkfiles, srcfiles) + files = collect(Iterators.flatten(map( + dir -> filter(isjuliafile, readdir(joinpath(dirname(@__DIR__), dir), join=true)), + ["checks", "src", joinpath("src", "printers")] + ))) args = ["--"] for file in files