From 23f2ffd75a47005a2d704646d7ed7de7b4046249 Mon Sep 17 00:00:00 2001 From: Alexandre Bergel Date: Wed, 23 Oct 2024 11:50:06 +0200 Subject: [PATCH 01/12] Removing junk code... --- src/interface.jl | 110 +- src/linting/checks.jl | 46 +- test/rai_rules_tests.jl | 24 +- test/runtests.jl | 25 - test/static_lint_tests.jl | 2030 ------------------------------------- test/typeinf.jl | 109 -- 6 files changed, 78 insertions(+), 2266 deletions(-) delete mode 100644 test/static_lint_tests.jl delete mode 100644 test/typeinf.jl diff --git a/src/interface.jl b/src/interface.jl index b597481..4dd65f0 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -120,60 +120,53 @@ in the project will be loaded automatically (calls to `include` with complicated are not handled, see `followinclude` for details). A `FileServer` will be returned containing the `File`s of the package. """ -function lint_file(rootpath, server = setup_server(); gethints = false) - empty!(server.files) - root = loadfile(server, rootpath) - semantic_pass(root) +function lint_file(rootpath; gethints = false) + file_content_string = open(io->read(io, String), rootpath, "r") + ast = CSTParser.parse(file_content_string, true) + markers::Dict{Symbol,String} = Dict(:filename => rootpath) - for f in values(server.files) - check_all(f.cst, essential_options, getenv(f, server), markers) - end + check_all(ast, markers) + lint_rule_reports = [] - if gethints - hints = [] - for (p,f) in server.files - hints_for_file = [] - for (offset, x) in collect_hints(f.cst, getenv(f, server)) - if haserror(x) - # TODO: On some point, we should only have the LintRuleReport case - if x.meta.error isa String - push!(hints_for_file, (x, string(x.meta.error, " at offset ", offset, " of ", p))) - elseif x.meta.error isa LintRuleReport - # The next line should be deleted - push!(hints_for_file, (x, string(x.meta.error.msg, " at offset ", offset, " of ", p))) - - lint_rule_report = x.meta.error - lint_rule_report.offset = offset - - line_number, column, annotation_line = convert_offset_to_line_from_filename(lint_rule_report.offset + 1, lint_rule_report.file) - lint_rule_report.line = line_number - lint_rule_report.column = column - - # If the annotation is to disable lint, - if annotation_line == "lint-disable-line" - # then we disable it. - elseif !isnothing(annotation_line) && startswith("lint-disable-line: $(lint_rule_report.msg)", annotation_line) - # then we disable it. - else - # Else we record it. - push!(lint_rule_reports, lint_rule_report) - end - else - push!(hints_for_file, (x, string(LintCodeDescriptions[x.meta.error], " at offset ", offset, " of ", p))) - end - push!(hints_for_file, (x, string("Missing reference.", " at offset ", offset, " of ", p))) + + hints = [] # TODO TO REMOVE + hints_for_file = [] # TODO TO REMOVE + for (offset, x) in collect_lint_report(ast) + if haserror(x) + # TODO: On some point, we should only have the LintRuleReport case + if x.meta.error isa String + push!(hints_for_file, (x, string(x.meta.error, " at offset ", offset, " of ", p))) + elseif x.meta.error isa LintRuleReport + # The next line should be deleted + push!(hints_for_file, (x, string(x.meta.error.msg, " at offset ", offset, " of ", rootpath))) + + lint_rule_report = x.meta.error + lint_rule_report.offset = offset + + line_number, column, annotation_line = convert_offset_to_line_from_filename(lint_rule_report.offset + 1, lint_rule_report.file) + lint_rule_report.line = line_number + lint_rule_report.column = column + + # If the annotation is to disable lint, + if annotation_line == "lint-disable-line" + # then we disable it. + elseif !isnothing(annotation_line) && startswith("lint-disable-line: $(lint_rule_report.msg)", annotation_line) + # then we disable it. + else + # Else we record it. + push!(lint_rule_reports, lint_rule_report) end + else + push!(hints_for_file, (x, string(LintCodeDescriptions[x.meta.error], " at offset ", offset, " of ", p))) end - append!(hints, hints_for_file) + push!(hints_for_file, (x, string("Missing reference.", " at offset ", offset, " of ", rootpath))) end - return root, hints, lint_rule_reports - else - return root end + append!(hints, hints_for_file) + return "TODO", hints, lint_rule_reports end global global_server = nothing -const essential_options = LintOptions(true, false, true, true, true, true, true, true, true, false, true) const no_filters = LintCodes[] const essential_filters = [no_filters; [StaticLint.MissingReference, StaticLint.MissingFile, StaticLint.InvalidTypeDeclaration]] @@ -311,7 +304,6 @@ should_print_report(result) = result.printout_count <= MAX_REPORTED_ERRORS function _run_lint_on_dir( rootpath::String; result::LintResult=LintResult(), - server = global_server, io::Union{IO,Nothing}=stdout, io_violations::Union{IO,Nothing}=nothing, io_recommendations::Union{IO,Nothing}=nothing, @@ -325,14 +317,14 @@ function _run_lint_on_dir( for file in files filename = joinpath(root, file) if endswith(filename, ".jl") - run_lint(filename; result, server, io, io_violations, io_recommendations, filters, formatter) + run_lint(filename; result, io, io_violations, io_recommendations, filters, formatter) end end for dir in dirs p = joinpath(root, dir) !isnothing(match(r".*/\.git.*", p)) && continue - _run_lint_on_dir(p; result, server, io, io_violations, io_recommendations, filters, formatter) + _run_lint_on_dir(p; result, io, io_violations, io_recommendations, filters, formatter) end end return result @@ -411,11 +403,6 @@ print_summary( result::LintResult ) = nothing -does_file_server_need_to_be_initialized() = isnothing(StaticLint.global_server) -function initialize_file_server() - StaticLint.global_server = setup_server() - return StaticLint.global_server -end """ run_lint(rootpath::String; server = global_server, io::IO=stdout, io_violations::Union{IO,Nothing}, io_recommendations::Union{IO,Nothing}) @@ -430,30 +417,24 @@ Example of use: function run_lint( rootpath::String; result::LintResult=LintResult(), - server = global_server, io::Union{IO,Nothing}=stdout, io_violations::Union{IO,Nothing}=nothing, io_recommendations::Union{IO,Nothing}=nothing, filters::Vector{LintCodes}=essential_filters, formatter::AbstractFormatter=PlainFormat() ) - # If no server is defined, then we define it. - if does_file_server_need_to_be_initialized() - server = initialize_file_server() - end - # If already linted, then we merely exit rootpath in result.linted_files && return result # If we are running Lint on a directory - isdir(rootpath) && return _run_lint_on_dir(rootpath; result, server, io, io_violations, io_recommendations, filters, formatter) + isdir(rootpath) && return _run_lint_on_dir(rootpath; result, io, io_violations, io_recommendations, filters, formatter) # Check if we have to be run on a Julia file. Simply exit if not. # This simplify the amount of work in GitHub Action endswith(rootpath, ".jl") || return result # We are running Lint on a Julia file - _,_,lint_reports = StaticLint.lint_file(rootpath, server; gethints = true) + _,_,lint_reports = StaticLint.lint_file(rootpath; gethints = true) print_header(formatter, io, rootpath) is_recommendation(r::LintRuleReport) = r.rule isa RecommendationExtendedRule @@ -507,7 +488,6 @@ useful to test some rules that depends on the filename. function run_lint_on_text( source::String; result::LintResult=LintResult(), - server = global_server, io::Union{IO,Nothing}=stdout, filters::Vector{LintCodes}=essential_filters, formatter::AbstractFormatter=PlainFormat(), @@ -529,7 +509,7 @@ function run_lint_on_text( open(tmp_file_name, "w") do file write(file, source) flush(file) - run_lint(tmp_file_name; result, server, io, io_violations, io_recommendations, filters, formatter) + run_lint(tmp_file_name; result, io, io_violations, io_recommendations, filters, formatter) end print(io, String(take!(io_violations))) @@ -554,6 +534,7 @@ function print_datadog_report( files_count::Integer, violation_count::Integer, recommandation_count::Integer, + fatalviolations_count::Integer, ) event = Dict( :source => "StaticLint", @@ -565,6 +546,7 @@ function print_datadog_report( :files_count => files_count, :violation_count => violation_count, :recommandation_count => recommandation_count, + :fatalviolations_count => recommandation_count, ) ) println(json_output, JSON3.write(event)) @@ -708,7 +690,7 @@ function generate_report( end report_as_string = open(output_filename) do io read(io, String) end - print_datadog_report(json_output, report_as_string, lint_result.files_count, lint_result.violations_count, lint_result.recommendations_count) + print_datadog_report(json_output, report_as_string, lint_result.files_count, lint_result.violations_count, lint_result.recommendations_count, lint_result.fatalviolations_count) # If a json_filename was provided, we are writing the result in json_output. # In that case, we need to close the stream at the end. diff --git a/src/linting/checks.jl b/src/linting/checks.jl index f0ea626..db2e0ef 100644 --- a/src/linting/checks.jl +++ b/src/linting/checks.jl @@ -136,7 +136,7 @@ function fetch_value(x::EXPR, tag::Symbol) end end -function check_all(x::EXPR, opts::LintOptions, env::ExternalEnv, markers::Dict{Symbol,String}=Dict{Symbol,String}()) +function check_all(x::EXPR, markers::Dict{Symbol,String}=Dict{Symbol,String}()) # Setting up the markers if headof(x) === :const markers[:const] = fetch_value(x, :IDENTIFIER) @@ -153,37 +153,19 @@ function check_all(x::EXPR, opts::LintOptions, env::ExternalEnv, markers::Dict{S end end - # Do checks - opts.call && check_call(x, env) - opts.iter && check_loop_iter(x, env) - opts.nothingcomp && check_nothing_equality(x, env) - opts.constif && check_if_conds(x) - opts.lazy && check_lazy(x) - opts.datadecl && check_datatype_decl(x, env) - opts.typeparam && check_typeparams(x, markers) - opts.modname && check_modulename(x) - opts.pirates && check_for_pirates(x) - opts.useoffuncargs && check_farg_unused(x) - check_kw_default(x, env) - check_use_of_literal(x) - check_break_continue(x) - check_const(x) - - if opts.extended - for T in all_extended_rule_types[] - check_with_process(T, x, markers) - if haserror(x) && x.meta.error isa LintRuleReport - lint_rule_report = x.meta.error - if haskey(markers, :filename) - lint_rule_report.file = markers[:filename] - end + for T in all_extended_rule_types[] + check_with_process(T, x, markers) + if haserror(x) && x.meta.error isa LintRuleReport + lint_rule_report = x.meta.error + if haskey(markers, :filename) + lint_rule_report.file = markers[:filename] end end end if x.args !== nothing for i in 1:length(x.args) - check_all(x.args[i], opts, env, markers) + check_all(x.args[i], markers) end end @@ -723,6 +705,18 @@ function collect_hints(x::EXPR, env, missingrefs=:all, isquoted=false, errs=Tupl errs end +function collect_lint_report(x::EXPR, isquoted=false, errs=Tuple{Int,EXPR}[], pos=0) + if haserror(x) + push!(errs, (pos, x)) + end + + for i in 1:length(x) + collect_lint_report(x[i], isquoted, errs, pos) + pos += x[i].fullspan + end + + errs +end function refof_maybe_getfield(x::EXPR) if isidentifier(x) diff --git a/test/rai_rules_tests.jl b/test/rai_rules_tests.jl index 01490af..965faf7 100644 --- a/test/rai_rules_tests.jl +++ b/test/rai_rules_tests.jl @@ -940,7 +940,7 @@ end # Checking the Workflow command stream_workflowcommand_report = String(take!(stream_workflowcommand)) wc_lines = split(stream_workflowcommand_report, "\n") - @test length(wc_lines) == 4 + @test length(wc_lines) == 3 @test isempty(wc_lines[end]) @test contains(wc_lines[1], "foo.jl,line=2,col=3::Use `@spawn` instead of `@async`.") @@ -949,7 +949,7 @@ end @test json_report[:source] == "StaticLint" @test json_report[:data][:files_count] == 2 - @test json_report[:data][:violation_count] == 2 + @test json_report[:data][:violation_count] == 1 @test json_report[:data][:recommandation_count] == 1 local result @@ -963,7 +963,6 @@ end \*\*Output of the \[StaticLint\.jl code analyzer\]\(https://github\.com/RelationalAI/StaticLint\.jl\).+\*\* Report creation time \(UTC\): \H+ - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ - - \*\*Line 2, column 25:\*\* Variable has been assigned but not used, if you want to keep this variable unused then prefix it with `_`. \H+
@@ -973,7 +972,7 @@ end
- 🚨\*\*In total, 2 rule violations and 1 PR reviewer recommendation are found over 2 Julia files\*\*🚨 + 🚨\*\*In total, 1 rule violation and 1 PR reviewer recommendation are found over 2 Julia files\*\*🚨 """ result_matching = !isnothing(match(expected, result)) # DEBUG: @@ -1309,9 +1308,10 @@ end json_report = JSON3.read(String(take!(json_io))) @test json_report[:source] == "StaticLint" - @test json_report[:data][:files_count] > 3 - @test json_report[:data][:violation_count] > 10 + @test json_report[:data][:files_count] >= 2 + @test json_report[:data][:violation_count] >= 0 @test json_report[:data][:recommandation_count] >= 0 + @test json_report[:data][:fatalviolations_count] >= 0 local result open(output_file) do oo @@ -1588,12 +1588,12 @@ end end @testset "Relaxing unused bindings" begin - @test lint_test(""" - function f(a::Int64, b, c) - local x - return 42 - end - """, "Line 2, column 11: Variable has been assigned but not used") + # @test lint_test(""" + # function f(a::Int64, b, c) + # local x + # return 42 + # end + # """, "Line 2, column 11: Variable has been assigned but not used") @test !lint_has_error_test(""" function f(a::Int64, b, c) diff --git a/test/runtests.jl b/test/runtests.jl index d364be8..45f36a7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,30 +2,5 @@ using StaticLint, SymbolServer using CSTParser, Test using StaticLint: convert_offset_to_line_from_lines, scopeof, bindingof, refof, errorof, check_all, getenv -server = StaticLint.FileServer(); - -function get_ids(x, ids=[]) - if StaticLint.headof(x) === :IDENTIFIER - push!(ids, x) - elseif x.args !== nothing - for a in x.args - get_ids(a, ids) - end - end - ids -end - -parse_and_pass(s) = StaticLint.lint_string(s, server) - -function check_resolved(s) - cst = parse_and_pass(s) - IDs = get_ids(cst) - [(refof(i) !== nothing) for i in IDs] -end - -include(joinpath(@__DIR__, "static_lint_tests.jl")) - -StaticLint.global_server = nothing include(joinpath(@__DIR__, "rai_rules_tests.jl")) -StaticLint.global_server = nothing diff --git a/test/static_lint_tests.jl b/test/static_lint_tests.jl deleted file mode 100644 index 3fac912..0000000 --- a/test/static_lint_tests.jl +++ /dev/null @@ -1,2030 +0,0 @@ -@testset "utf8 offsets" begin - @test convert_offset_to_line_from_lines(1, ["# aaaa","abcdegh"]) == (1,1,nothing) - @test convert_offset_to_line_from_lines(10, ["# aaaa","abcdegh"]) == (2,3,nothing) - @test convert_offset_to_line_from_lines(12, ["# ─aaa","abcdegh"]) == (2,3,nothing) - @test convert_offset_to_line_from_lines( - 40, ["# aaaaaaaaaa", "aaaa", "bbbb", "cccc", "dddd", "eeee","ffff"] - ) == (7, 2, nothing) - @test convert_offset_to_line_from_lines( - 40, ["# ──────────", "aaaa", "bbbb", "cccc", "dddd", "eeee","ffff"] - ) == (3, 2, nothing) - @test convert_offset_to_line_from_lines(4, ["──"]) == (1,2,nothing) - @test convert_offset_to_line_from_lines(5, ["──"]) == (1,2,nothing) - @test convert_offset_to_line_from_lines(6, ["──"]) == (1,2,nothing) - @test convert_offset_to_line_from_lines(7, ["──"]) == (1,3,nothing) - @test_throws BoundsError convert_offset_to_line_from_lines(8, ["──"]) -end - -@testset "StaticLint" begin - - @testset "Basic bindings" begin - - @test check_resolved(""" -x -x = 1 -x -""") == [false, true, true] - - @test check_resolved(""" -x, y -x = y = 1 -x, y -""") == [false, false, true, true, true, true] - - @test check_resolved(""" -x, y -x, y = 1, 1 -x, y -""") == [false, false, true, true, true, true] - - @test check_resolved(""" -M -module M end -M -""") == [false, true, true] - - @test check_resolved(""" -f -f() = 0 -f -""") == [false, true, true] - - @test check_resolved(""" -f -function f end -f -""") == [false, true, true] - - @test check_resolved(""" -f -function f() end -f -""") == [false, true, true] - - @test check_resolved(""" -function f(a) -end -""") == [true, true] - - @test check_resolved(""" -f, a -function f(a) - a -end -f, a -""") == [false, false, true, true, true, true, false] - - - @test check_resolved(""" -x -let x = 1 - x -end -x -""") == [false, true, true, false] - - @test check_resolved(""" -x,y -let x = 1, y = 1 - x, y -end -x, y -""") == [false, false, true, true, true, true, false, false] - - @test check_resolved(""" -function f(a...) - f(a) -end -""") == [true, true, true, true] - - @test check_resolved(""" -for i = 1:1 -end -""") == [true] - - @test check_resolved(""" -[i for i in 1:1] -""") == [true, true] - - @test check_resolved(""" -[i for i in 1:1 if i] -""") == [true, true, true] - -# @test check_resolved(""" -# @deprecate f(a) sin(a) -# f -# """) == [true, true, true, true, true, true] - - @test check_resolved(""" -@deprecate f sin -f -""") == [true, true, true, true] - - @test check_resolved(""" -module Mod -f = 1 -end -using .Mod: f -f -""") == [true, true, true, true, true] - - @test check_resolved(""" -module Mod -module SubMod - f() = 1 -end -using .SubMod: f -f -end -""") == [true, true, true, true, true, true] - - @test check_resolved(""" -struct T - field -end -function f(arg::T) - arg.field -end -""") == [true, true, true, true, true, true, true] - - if VERSION > v"1.8-" - @test check_resolved(""" - mutable struct T - const field - end - function f(arg::T) - arg.field - end - """) == [true, true, true, true, true, true, true] - end - - @test check_resolved(""" -f(arg) = arg -""") == [1, 1, 1] - - @test check_resolved("-(r::T) where T = r") == [1, 1, 1, 1] - @test check_resolved("[k * j for j = 1:10 for k = 1:10]") == [1, 1, 1, 1] - @test check_resolved("[k * j for j in 1:10 for k in 1:10]") == [1, 1, 1, 1] - - @testset "inference" begin - @test StaticLint.CoreTypes.isfunction(bindingof(parse_and_pass("f(arg) = arg").args[1]).type) - @test StaticLint.CoreTypes.isfunction(bindingof(parse_and_pass("function f end").args[1]).type) - @test StaticLint.CoreTypes.isdatatype(bindingof(parse_and_pass("struct T end").args[1]).type) - @test StaticLint.CoreTypes.isdatatype(bindingof(parse_and_pass("mutable struct T end").args[1]).type) - @test StaticLint.CoreTypes.isdatatype(bindingof(parse_and_pass("abstract type T end").args[1]).type) - @test StaticLint.CoreTypes.isdatatype(bindingof(parse_and_pass("primitive type T 8 end").args[1]).type) - @test StaticLint.CoreTypes.isint(bindingof(parse_and_pass("x = 1").args[1].args[1]).type) - @test StaticLint.CoreTypes.isfloat(bindingof(parse_and_pass("x = 1.0").args[1].args[1]).type) - @test StaticLint.CoreTypes.isstring(bindingof(parse_and_pass("x = \"text\"").args[1].args[1]).type) - @test StaticLint.CoreTypes.ismodule(bindingof(parse_and_pass("module A end").args[1]).type) - @test StaticLint.CoreTypes.ismodule(bindingof(parse_and_pass("baremodule A end").args[1]).type) - - # @test parse_and_pass("function f(x::Int) x end")[1][2][3].binding.t == StaticLint.getsymbolserver(server)["Core"].vals["Function"] - let cst = parse_and_pass(""" - struct T end - function f(x::T) x end - """) - @test StaticLint.CoreTypes.isdatatype(bindingof(cst.args[1]).type) - @test StaticLint.CoreTypes.isfunction(bindingof(cst.args[2]).type) - @test bindingof(cst.args[2].args[1].args[2]).type == bindingof(cst.args[1]) - @test refof(cst.args[2].args[2].args[1]) == bindingof(cst.args[2].args[1].args[2]) - end - let cst = parse_and_pass(""" - struct T end - T() = 1 - function f(x::T) x end - """) - @test StaticLint.CoreTypes.isdatatype(bindingof(cst.args[1]).type) - @test StaticLint.CoreTypes.isfunction(bindingof(cst.args[3]).type) - @test bindingof(cst.args[3].args[1].args[2]).type == bindingof(cst.args[1]) - @test refof(cst.args[3].args[2].args[1]) == bindingof(cst.args[3].args[1].args[2]) - end - - let cst = parse_and_pass(""" - struct T end - t = T() - """) - @test StaticLint.CoreTypes.isdatatype(bindingof(cst.args[1]).type) - @test bindingof(cst.args[2].args[1]).type == bindingof(cst.args[1]) - end - - let cst = parse_and_pass(""" - module A - module B - x = 1 - end - module C - import ..B - B.x - end - end - """) - @test refof(cst.args[1].args[3].args[2].args[3].args[2].args[2].args[1]) == bindingof(cst[1].args[3].args[1].args[3].args[1].args[1]) - end - - let cst = parse_and_pass(""" - struct T0 - x - end - struct T1 - field::T0 - end - function f(arg::T1) - arg.field.x - end - """); - @test refof(cst.args[3].args[2].args[1].args[1].args[1]) == bindingof(cst.args[3].args[1].args[2]) - @test refof(cst.args[3].args[2].args[1].args[1].args[2].args[1]) == bindingof(cst.args[2].args[3].args[1]) - @test refof(cst.args[3].args[2].args[1].args[2].args[1]) == bindingof(cst.args[1].args[3].args[1]) - end - - let cst = parse_and_pass("""raw\"whatever\"""") - @test refof(cst.args[1].args[1]) !== nothing - end - - let cst = parse_and_pass(""" - macro mac_str() end - mac"whatever" - """) - @test refof(cst.args[2].args[1]) == bindingof(cst.args[1]) - end - - let cst = parse_and_pass("[i * j for i = 1:10 for j = i:10]") - @test refof(cst.args[1].args[1].args[1].args[1].args[2].args[2].args[2]) == bindingof(cst.args[1].args[1].args[1].args[2].args[1]) - end - - let cst = parse_and_pass("[i * j for i = 1:10, j = 1:10 for k = i:10]") - @test refof(cst.args[1].args[1].args[1].args[1].args[2].args[2].args[2]) == bindingof(cst.args[1].args[1].args[1].args[2].args[1]) - end - - let cst = parse_and_pass(""" - module Reparse - end - using .Reparse, CSTParser - """) - @test refof(cst.args[2].args[1].args[2]).val == bindingof(cst[1]) - end - - let cst = parse_and_pass(""" - module A - A - end - """) - @test scopeof(cst).names["A"] == scopeof(cst.args[1]).names["A"] - @test refof(cst.args[1].args[2]) == bindingof(cst.args[1]) - @test refof(cst.args[1].args[3].args[1]) == bindingof(cst.args[1]) - end - # let cst = parse_and_pass(""" - # using Test: @test - # """) - # @test bindingof(cst[1][4]) !== nothing - # end - let cst = parse_and_pass(""" - sin(1,2,3) - """) - @test startswith(errorof(cst.args[1]), "Possible method call error") - end - let cst = parse_and_pass(""" - for i in length(1) end - for i in 1.1 end - for i in 1 end - for i in 1:1 end - """) - @test errorof(cst.args[1].args[1]) === StaticLint.IncorrectIterSpec - @test errorof(cst.args[2].args[1]) === StaticLint.IncorrectIterSpec - @test errorof(cst.args[3].args[1]) === StaticLint.IncorrectIterSpec - @test errorof(cst.args[4].args[1]) === nothing - end - - let cst = parse_and_pass(""" - [i for i in length(1) end] - [i for i in 1.1 end] - [i for i in 1 end] - [i for i in 1:1 end] - """) - @test errorof(cst[1][2][3]) === StaticLint.IncorrectIterSpec - @test errorof(cst[2][2][3]) === StaticLint.IncorrectIterSpec - @test errorof(cst[3][2][3]) === StaticLint.IncorrectIterSpec - @test errorof(cst[4][2][3]) === nothing - end - - for cst in parse_and_pass.(["a == nothing", "nothing == a"]) - @test errorof(cst[1][2]) === StaticLint.NothingEquality - end - for cst in parse_and_pass.(["a != nothing", "nothing != a"]) - @test errorof(cst[1][2]) === StaticLint.NothingNotEq - end - - let cst = parse_and_pass(""" - struct Graph - children:: T - end - - function test() - g = Graph() - f = g.children - end""") - @test cst.args[2].args[2].args[2].args[2].args[2].args[1] in bindingof(cst.args[1].args[3].args[1]).refs - end - - let cst = parse_and_pass(""" - __source__ - __module__ - macro m() - __source__ - __module__ - end""") - @test refof(cst[1]) === nothing - @test refof(cst[2]) === nothing - @test refof(cst[3][3][1]) !== nothing - @test refof(cst[3][3][2]) !== nothing - end - - let cst = parse_and_pass(""" - struct Foo - x::DataType - y::Float64 - end - (;x, y) = Foo(1,2) - x - y - """) - mx = cst.args[3].meta - @test mx.ref.type.name.name.name == :DataType - my = cst.args[4].meta - @test my.ref.type.name.name.name == :Float64 - end - end - - @testset "macros" begin - @test check_resolved(""" - @enum(E,a,b) - E - a - b - """) == [true, true, true, true, true, true, true] - end - - @test check_resolved(""" - @enum E a b - E - a - b - """) == [true, true, true, true, true, true, true] - - @test check_resolved(""" - @enum E begin - a - b - end - E - a - b - """) == [true, true, true, true, true, true, true] - end - - @testset "tuple args" begin - let cst = parse_and_pass(""" - function f((arg1, arg2)) - arg1, arg2 - end""") - @test StaticLint.hasref(cst[1][3][1][1]) - @test StaticLint.hasref(cst[1][3][1][3]) - end - - let cst = parse_and_pass(""" - function f((arg1, arg2) = (1,2)) - arg1, arg2 - end""") - @test StaticLint.hasref(cst[1][3][1][1]) - @test StaticLint.hasref(cst[1][3][1][3]) - end - - let cst = parse_and_pass(""" - function f((arg1, arg2)::Tuple{Int,Int}) - arg1, arg2 - end""") - @test StaticLint.hasref(cst[1][3][1][1]) - @test StaticLint.hasref(cst[1][3][1][3]) - end - end - - @testset "type params check" begin - let cst = parse_and_pass(""" - f() where T - f() where {T,S} - f() where {T<:Any} - """) - @test StaticLint.errorof(cst.args[1].args[2]) == StaticLint.UnusedTypeParameter - @test StaticLint.errorof(cst.args[2].args[2]) == StaticLint.UnusedTypeParameter - @test StaticLint.errorof(cst.args[2].args[3]) == StaticLint.UnusedTypeParameter - @test StaticLint.errorof(cst.args[3].args[2]) == StaticLint.UnusedTypeParameter - end - let cst = parse_and_pass(""" - f(x::T) where T - f(x::T,y::S) where {T,S} - f(x::T) where {T<:Any} - """) - @test !StaticLint.haserror(cst.args[1].args[2]) - @test !StaticLint.haserror(cst.args[2].args[2]) - @test !StaticLint.haserror(cst.args[2].args[3]) - @test !StaticLint.haserror(cst.args[3].args[2]) - end - end - - - @testset "overwrites_imported_function" begin - let cst = parse_and_pass(""" - import Base:sin - using Base:cos - sin(x) = 1 - cos(x) = 1 - Base.tan(x) = 1 - """) - @test StaticLint.overwrites_imported_function(refof(cst[3][1][1])) - @test !StaticLint.overwrites_imported_function(refof(cst[4][1][1])) - @test StaticLint.overwrites_imported_function(refof(cst[5][1][1][3][1])) - end - end - - @testset "pirates" begin - let cst = parse_and_pass(""" - import Base:sin - struct T end - sin(x::Int) = 1 - sin(x::T) = 1 - sin(x::Array{T}) = 1 - """) - StaticLint.check_for_pirates(cst.args[3]) - StaticLint.check_for_pirates(cst.args[4]) - @test errorof(cst.args[3]) === StaticLint.TypePiracy - @test errorof(cst.args[4]) === nothing - end - let cst = parse_and_pass(""" - struct AreaIterator{T} - array::AbstractMatrix{T} - radius::Int - end - Base.eltype(::Type{AreaIterator{T}}) where T = Tuple{T, AbstractVector{T}} - """) - StaticLint.check_for_pirates(cst[2]) - @test errorof(cst[2]) === nothing - end - let cst = parse_and_pass(""" - import Base:sin - abstract type T end - sin(x::Array{T}) = 1 - sin(x::Array{<:T}) = 1 - sin(x::Array{Number}) = 1 - sin(x::Array{<:Number}) = 1 - """) - @test errorof(cst[3]) === nothing - @test errorof(cst[4]) === nothing - @test errorof(cst[5]) === StaticLint.TypePiracy - @test errorof(cst[6]) === StaticLint.TypePiracy - end - let cst = parse_and_pass(""" - abstract type At end - struct Ty end - Base.eltype(::Type{Ty{T}} where {T}) = 1 - Base.length(s::Ty{T} where T <: At) = 1 - """) - @test StaticLint.check_for_pirates(cst[3]) === nothing - @test StaticLint.check_for_pirates(cst[4]) === nothing - end - - let cst = parse_and_pass(""" - !=(a,b) = true - Base.:!=(a,b) = true - !=(a::T,b::T) = true - !=(a::T,b::T) where T= true - """) - @test errorof(cst[1]) === StaticLint.NotEqDef - @test errorof(cst[2]) === StaticLint.NotEqDef - @test errorof(cst[3]) === StaticLint.NotEqDef - @test errorof(cst[4]) === StaticLint.NotEqDef - end - end - - @testset "check_call" begin - let cst = parse_and_pass(""" - sin(1) - sin(1,2) - """) - @test StaticLint.errorof(cst.args[1]) === nothing - # @test StaticLint.errorof(cst.args[2]) == StaticLint.IncorrectCallArgs - @test startswith(errorof(cst[2]), "Possible method call error") - - end - - let cst = parse_and_pass(""" - Base.sin(a,b) = 1 - function Base.sin(a,b) - 1 - end - """) - @test StaticLint.errorof(cst.args[1].args[1]) === nothing - @test StaticLint.errorof(cst.args[2].args[1]) === nothing - end - - let cst = parse_and_pass(""" - f(x) = 1 - f(1, 2) - """) - # @test StaticLint.errorof(cst.args[2]) === StaticLint.IncorrectCallArgs - @test startswith(errorof(cst[2]), "Possible method call error") - end - - let cst = parse_and_pass(""" - view([1], 1, 2, 3) - """) - @test StaticLint.errorof(cst.args[1]) === nothing - end - - let cst = parse_and_pass(""" - f(a...) = 1 - f(1) - f(1, 2) - """) - @test StaticLint.errorof(cst.args[2]) === nothing - @test StaticLint.errorof(cst.args[3]) === nothing - end - let cst = parse_and_pass(""" - function func(a, b) - func(a...) - end - """) - m_counts = StaticLint.func_nargs(cst.args[1]) - call_counts = StaticLint.call_nargs(cst.args[1].args[2].args[1]) - @test startswith(StaticLint.errorof(cst.args[1].args[2].args[1]), - "Splatting (`...`) should be used with extreme caution. Splatting from dynamically sized containers could result in severe performance degradation.") - end - let cst = parse_and_pass(""" - function func(@nospecialize args...) end - func(1, 2) - """) - @test StaticLint.func_nargs(cst.args[1]) == (0, typemax(Int), String[], false) - @test StaticLint.errorof(cst.args[2]) === nothing - end - let cst = parse_and_pass(""" - argtail(x, rest...) = 1 - tail(x::Tuple) = argtail(x...) - """) - @test StaticLint.func_nargs(cst[1]) == (1, typemax(Int), String[], false) - @test StaticLint.errorof(cst[2]) === nothing - end - let cst = parse_and_pass(""" - func(arg::Vararg{T,N}) where N = arg - func(a,b) - """) - - @test StaticLint.func_nargs(cst[1]) == (0, typemax(Int), String[], false) - @test StaticLint.errorof(cst[2]) === nothing - end - let cst = parse_and_pass(""" - function f(a, b; kw = kw) end - f(1,2, kw = 1) - """) - @test StaticLint.errorof(cst[2]) === nothing - end - let cst = parse_and_pass(""" - func(a,b,c,d) = 1 - func(a..., 2) - """) - StaticLint.call_nargs(cst[2]) - @test StaticLint.errorof(cst[2]) === nothing - end - let cst = parse_and_pass(""" - @kwdef struct A - x::Float64 - end - A(x = 5.0) - """) - @test StaticLint.errorof(cst[2]) === nothing - end - let cst = parse_and_pass(""" - import Base: sin - \"\"\" - docs - \"\"\" - sin - sin(a,b) = 1 - sin(1) - """) - # Checks that documented symbols are skipped - @test isempty(StaticLint.collect_hints(cst, StaticLint.getenv(server.files[""], server))) - end - let cst = parse_and_pass(""" - import Base: sin - sin(a,b) = 1 - sin(1) - """) - # Checks that documented symbols are skipped - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - end - let cst = parse_and_pass(""" - function f(a::F)::Bool where {F} a end - """) - # ensure we strip all type decl code from around signature - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - end - end - - @testset "check_modulename" begin - let cst = parse_and_pass(""" - module Mod1 - module Mod11 - end - end - module Mod2 - module Mod2 - end - end - """) - StaticLint.check_modulename(cst.args[1]) - StaticLint.check_modulename(cst.args[1].args[3].args[1]) - StaticLint.check_modulename(cst.args[2]) - StaticLint.check_modulename(cst.args[2].args[3].args[1]) - - @test StaticLint.errorof(cst.args[1].args[2]) === nothing - @test StaticLint.errorof(cst.args[1].args[3].args[1].args[2]) === nothing - @test StaticLint.errorof(cst.args[2].args[2]) === nothing - @test StaticLint.errorof(cst.args[2].args[3].args[1].args[2]) === StaticLint.InvalidModuleName - end - end - - if !(VERSION < v"1.3") - @testset "non-std var syntax" begin - let cst = parse_and_pass(""" - var"name" = 1 - var"func"(arg) = arg - function var"func1"() end - name - func - func1 - struct AnyType - var"anything" - end - anything(x::AnyType) = x.var"anything" - """) - StaticLint.collect_hints(cst, getenv(server.files[""], server)) - @test all(n in keys(cst.meta.scope.names) for n in ("name", "func")) - @test StaticLint.hasref(cst[4]) - @test StaticLint.hasref(cst[5]) - @test StaticLint.hasref(cst[6]) - @test cst.args[8].args[2].args[1].args[2].args[1] in bindingof(cst.args[7].args[3].args[1]).refs - end - end - end - - # if false # Not to be run, requires JuMP - # @testset "JuMP macros" begin - # let cst = parse_and_pass(""" - # using JuMP - # model = Model() - # some_bound = 1 - # @variable(model, x0) - # @variable(model, x1, somekw=1) - # @variable(model, x2 <= 1) - # @variable(model, x3 >= 1) - # @variable(model, 1 <= x4) - # @variable(model, 1 >= x5) - # @variable(model, x6 >= some_bound) - # # @variable(model, some_bound >= x7) - # """) - # @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - # end - - # let cst = parse_and_pass(""" - # using JuMP - # model = Model() - # some_bound = 1 - # @variable model x0 - # @variable model x1 somekw=1 - # @variable model x2 <= 1 - # @variable model x3 >= 1 - # @variable model 1 <= x4 - # @variable model 1 >= x5 - # @variable model x6 >= some_bound - # # @variable(model, some_bound >= x7) - # """) - # @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - # end - - # let cst = parse_and_pass(""" - # using JuMP - # model = Model() - # some_bound = 1 - # @variable(model, some_bound >= x7) - # """) - # @test !StaticLint.hasref(cst[4][5][3]) - # end - - # let cst = parse_and_pass(""" - # using JuMP - # model = Model() - # some_bound = 1 - # @expression(model, ex, some_bound >= 1) - # """) - # @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - # end - - # let cst = parse_and_pass(""" - # using JuMP - # model = Model() - # @expression(model, expr, 1 == 1) - # @constraint(model, con1, expr) - # @constraint model con2 expr - # """) - # @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - # end - # end - # end - - # @testset "stdcall" begin - # let cst = parse_and_pass(""" - # ccall(:GetCurrentProcess, stdcall, Ptr{Cvoid}, ())""") - # StaticLint.collect_hints(cst, getenv(server.files[""], server)) - # isdefined(Main, :Infiltrator) && Main.infiltrate(@__MODULE__, Base.@locals, @__FILE__, @__LINE__) - # @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - # end - # let cst = parse_and_pass(""" - # stdcall - # """) - # @test !StaticLint.hasref(cst[1]) - # end - # end - - @testset "check_if_conds" begin - let cst = parse_and_pass(""" - if true end - """) - StaticLint.check_if_conds(cst.args[1]) - @test cst.args[1].args[1].meta.error == StaticLint.ConstIfCondition - end - let cst = parse_and_pass(""" - if x = 1 end - """) - StaticLint.check_if_conds(cst.args[1]) - @test cst.args[1].args[1].meta.error == StaticLint.EqInIfConditional - end - let cst = parse_and_pass(""" - if a || x = 1 end - """) - StaticLint.check_if_conds(cst.args[1]) - @test cst.args[1].args[1].meta.error == StaticLint.EqInIfConditional - end - let cst = parse_and_pass(""" - if x = 1 && b end - """) - StaticLint.check_if_conds(cst.args[1]) - @test cst.args[1].args[1].meta.error == StaticLint.EqInIfConditional - end - end - - - @testset "check_farg_unused" begin - let cst = parse_and_pass("function f(arg1, arg2) arg1 end") - StaticLint.check_farg_unused(cst[1]) - @test StaticLint.errorof(CSTParser.get_sig(cst[1])[3]) === nothing - @test StaticLint.errorof(CSTParser.get_sig(cst[1])[5]) === StaticLint.UnusedFunctionArgument - end - let cst = parse_and_pass("function f(arg1::T, arg2::T) arg1 end") - StaticLint.check_farg_unused(cst[1]) - @test StaticLint.errorof(CSTParser.get_sig(cst[1])[3]) === nothing - @test StaticLint.errorof(CSTParser.get_sig(cst[1])[5]) === StaticLint.UnusedFunctionArgument - end - let cst = parse_and_pass("function f(arg1, arg2::T, arg3 = 1, arg4::T = 1) end") - StaticLint.check_farg_unused(cst.args[1]) - @test StaticLint.errorof(CSTParser.get_sig(cst.args[1]).args[2]) === StaticLint.UnusedFunctionArgument - @test StaticLint.errorof(CSTParser.get_sig(cst.args[1]).args[3]) === StaticLint.UnusedFunctionArgument - @test StaticLint.errorof(CSTParser.get_sig(cst.args[1]).args[4].args[1]) === StaticLint.UnusedFunctionArgument - @test StaticLint.errorof(CSTParser.get_sig(cst.args[1]).args[5].args[1]) === StaticLint.UnusedFunctionArgument - end - let cst = parse_and_pass("function f(arg) arg = 1 end") - StaticLint.check_farg_unused(cst[1]) - @test StaticLint.errorof(CSTParser.get_sig(cst[1])[3]) === StaticLint.UnusedFunctionArgument - end - let cst = parse_and_pass( - """function f(arg) - x = arg - arg = x - end""") - StaticLint.check_farg_unused(cst[1]) - @test StaticLint.errorof(CSTParser.get_sig(cst[1])[3]) === nothing - end - let cst = parse_and_pass("function f(arg) 1 end") - StaticLint.check_farg_unused(cst[1]) - @test StaticLint.errorof(CSTParser.get_sig(cst[1])[3]) === nothing - end - let cst = parse_and_pass("f(arg) = true") - StaticLint.check_farg_unused(cst[1]) - @test StaticLint.errorof(CSTParser.get_sig(cst[1])[3]) === nothing - end - let cst = parse_and_pass("func(@nospecialize(arg)) = arg") - StaticLint.check_farg_unused(cst[1]) - @test cst[1].args[1].args[2].meta.error === nothing - end - let cst = parse_and_pass(""" - function f(x,y,z) - @. begin - x = z - y = z - end - end - """) - StaticLint.check_farg_unused(cst[1]) - @test StaticLint.errorof(CSTParser.get_sig(cst[1])[3]) === nothing - @test StaticLint.errorof(CSTParser.get_sig(cst[1])[5]) === nothing - end - end - - @testset "check redefinition of const" begin - let cst = parse_and_pass(""" - T = 1 - struct T end - """) - @test cst[2].meta.error == StaticLint.CannotDeclareConst - end - let cst = parse_and_pass(""" - struct T end - T = 1 - """) - @test cst[2].meta.error == StaticLint.InvalidRedefofConst - end - let cst = parse_and_pass(""" - struct T end - T() = 1 - """) - @test cst[2].meta.error === nothing - end - end - - @testset "hoisting of inner constructors" begin - let cst = parse_and_pass(""" - struct ASDF - x::Int - y::Int - ASDF(x::Int) = new(x, 1) - end - ASDF(1) - """) - # Check inner constructor is hoisted - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - end - end - - @testset "using statements" begin # e.g. `using StaticLint: StaticLint` - let cst = parse_and_pass("using Base.Filesystem: Filesystem") - @test StaticLint.hasref(cst.args[1].args[1].args[2].args[1]) - end - let cst = parse_and_pass("using Base: Ordering") - @test StaticLint.hasbinding(cst.args[1].args[1].args[2].args[1]) - end - let cst = parse_and_pass(""" - module Outer - module Inner - x = 1 - export x - end - using .Inner - end - using .Outer: x, rand - """) - @test StaticLint.hasbinding(cst.args[2].args[1].args[2].args[1]) - @test StaticLint.hasbinding(cst.args[2].args[1].args[3].args[1]) - end - end - - @testset "don't report unknown getfields when a custom getproperty is defined" begin # e.g. `using StaticLint: StaticLint` - let cst = parse_and_pass(""" - struct T end - Base.getproperty(x::T, s) = 1 - T - """) - @test StaticLint.has_getproperty_method(bindingof(cst.args[1])) - @test StaticLint.has_getproperty_method(refof(cst.args[3])) - end - let cst = parse_and_pass(""" - struct T - f1 - f2 - end - Base.getproperty(x::T, s) = (x,s) - f(x::T) = x.f3 - """) - @test !StaticLint.hasref(cst.args[3].args[2].args[1].args[2].args[1]) - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - end - let cst = parse_and_pass(""" - struct T{S} - f1 - f2 - end - Base.getproperty(x::T{Int}, s) = (x,s) - f(x::T) = x.f3 - """) - @test !StaticLint.hasref(cst.args[3].args[2].args[1].args[2].args[1]) - @test StaticLint.is_type_of_call_to_getproperty(cst.args[2].args[1].args[2].args[2].args[1]) - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - end - - let cst = parse_and_pass("f(x::Module) = x.parent1") - @test StaticLint.has_getproperty_method(server.external_env.symbols[:Core][:Module], getenv(server.files[""], server)) - @test !StaticLint.has_getproperty_method(server.external_env.symbols[:Core][:DataType], getenv(server.files[""], server)) - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - end - let cst = parse_and_pass("f(x::DataType) = x.sdf") - @test !isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - end - end - @testset "using of self" begin # e.g. `using StaticLint: StaticLint` - let cst = parse_and_pass(""" - function f(a::rand) a end - function f(a::Base.rand) a end - function f(a::Int) a end - Base.Int32(x) = 1 - function f(a::Int32) a end - Base.fetch(x) = 1 - function f(a::fetch) a end - """) - @test errorof(cst.args[1].args[1].args[2]) === StaticLint.InvalidTypeDeclaration - @test errorof(cst.args[2].args[1].args[2]) === StaticLint.InvalidTypeDeclaration - @test errorof(cst.args[3].args[1].args[2]) === nothing - @test errorof(cst.args[5].args[1].args[2]) === nothing - @test errorof(cst.args[7].args[1].args[2]) === StaticLint.InvalidTypeDeclaration - end - - @testset "interpret @eval" begin # e.g. `using StaticLint: StaticLint` - let cst = parse_and_pass(""" - let - @eval adf = 1 - end - """) - @test StaticLint.scopehasbinding(scopeof(cst), "adf") - @test !StaticLint.scopehasbinding(scopeof(cst[1]), "adf") - end - let cst = parse_and_pass(""" - let - @eval a,d,f = 1,2,3 - end - """) - @test StaticLint.scopehasbinding(scopeof(cst), "a") - @test StaticLint.scopehasbinding(scopeof(cst), "d") - @test StaticLint.scopehasbinding(scopeof(cst), "f") - @test !StaticLint.scopehasbinding(scopeof(cst[1]), "a") - @test !StaticLint.scopehasbinding(scopeof(cst[1]), "d") - @test !StaticLint.scopehasbinding(scopeof(cst[1]), "f") - end - let cst = parse_and_pass(""" - let - @eval a = 1 - @eval d = 2 - @eval f = 3 - end - """) - @test StaticLint.scopehasbinding(scopeof(cst), "a") - @test StaticLint.scopehasbinding(scopeof(cst), "d") - @test StaticLint.scopehasbinding(scopeof(cst), "f") - @test !StaticLint.scopehasbinding(scopeof(cst.args[1]), "a") - @test !StaticLint.scopehasbinding(scopeof(cst.args[1]), "d") - @test !StaticLint.scopehasbinding(scopeof(cst.args[1]), "f") - end - - let cst = parse_and_pass(""" - let name = :adf - @eval \$name = 1 - end - """) - @test StaticLint.scopehasbinding(scopeof(cst), "adf") - @test !StaticLint.scopehasbinding(scopeof(cst.args[1]), "adf") - end - let cst = parse_and_pass(""" - let name = [:adf] - @eval \$name = 1 - end - """) - @test !StaticLint.scopehasbinding(scopeof(cst), "adf") - @test !StaticLint.scopehasbinding(scopeof(cst.args[1]), "adf") - end - - let cst = parse_and_pass(""" - for name = [:adf, :asdf, :asdfs] - @eval \$name = 1 - end - """) - @test StaticLint.scopehasbinding(scopeof(cst), "adf") - @test StaticLint.scopehasbinding(scopeof(cst), "asdf") - @test StaticLint.scopehasbinding(scopeof(cst), "asdfs") - end - let cst = parse_and_pass(""" - for name = (:adf, :asdf, :asdfs) - @eval \$name = 1 - end - """) - @test StaticLint.scopehasbinding(scopeof(cst), "adf") - @test StaticLint.scopehasbinding(scopeof(cst), "asdf") - @test StaticLint.scopehasbinding(scopeof(cst), "asdfs") - end - let cst = parse_and_pass(""" - let name = :adf - @eval \$name(x) = 1 - end - adf(1,2) - """) - @test StaticLint.scopehasbinding(scopeof(cst), "adf") - @test !StaticLint.scopehasbinding(scopeof(cst.args[1]), "adf") - # @test errorof(cst.args[2]) === StaticLint.IncorrectCallArgs - @test startswith(errorof(cst[2]), "Possible method call error") - end - let cst = parse_and_pass(""" - for name in (:sdf, :asdf) - @eval \$name(x) = 1 - end - sdf(1,2) - """) - @test StaticLint.scopehasbinding(scopeof(cst), "sdf") - @test !StaticLint.scopehasbinding(scopeof(cst.args[1]), "asdf") - # @test errorof(cst[2]) === StaticLint.IncorrectCallArgs - @test startswith(errorof(cst[2]), "Possible method call error") - - end - end - end - - @testset "check for " begin # e.g. `using StaticLint: StaticLint` - let cst = parse_and_pass(""" - module A - module B - struct T end - end - using .B - function T(t::B.T) - end - end - """) - @test bindingof(cst.args[1].args[3].args[3]) != refof(cst.args[1].args[3].args[3].args[1].args[2].args[2].args[2].args[1]) - @test bindingof(cst.args[1].args[3].args[1].args[3].args[1]) == refof(cst.args[1].args[3].args[3][2][3][3][3][1]) - end - end - @testset "misc" begin # e.g. `using StaticLint: StaticLint` - let cst = parse_and_pass(""" - import Base: Bool - function Bool(x) x end - ^(z::Complex, n::Bool) = n ? z : one(z) - """) - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - end - let cst = parse_and_pass(""" - (rand(d::Vector{T})::T) where {T} = 1 - """) - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - end - end - @testset "Test self" begin - empty!(server.files) - f = StaticLint.loadfile(server, joinpath(@__DIR__, "..", "src", "StaticLint.jl")) - StaticLint.semantic_pass(f) - end - - let cst = parse_and_pass(""" - using Base:@irrational - @irrational ase 0.45343 π - ase - """) - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - end - - @testset "quoted getfield" begin - let cst = parse_and_pass("Base.:sin") - @test isempty(StaticLint.collect_hints(cst[1], getenv(server.files[""], server))) - end - @testset "quoted getfield" begin - let cst = parse_and_pass("Base.:sin") - @test isempty(StaticLint.collect_hints(cst.args[1], getenv(server.files[""], server))) - end - - let cst = parse_and_pass(""" - sin(1,1) - Base.sin(1,1) - Base.:sin(1,1) - """) - @test errorof(cst.args[1]) === errorof(cst.args[2]) === errorof(cst.args[3]) - end - end - @testset "overloading" begin - # overloading of a function that happens to be exported into the current scope. - let cst = parse_and_pass(""" - Base.sin() = nothing - sin() - """) - @test haskey(cst.meta.scope.names, "sin") # - @test first(cst.meta.scope.names["sin"].refs) == server.external_env.symbols[:Base][:sin] - @test isempty(StaticLint.collect_hints(cst[2], getenv(server.files[""], server))) - end - # As above but for user defined function - let cst = parse_and_pass(""" - module M - f(x) = nothing - end - M.f(a,b) = nothing - M.f(1,2) - """) - @test !haskey(cst.meta.scope.names, "f") - @test errorof(cst.args[3]) === nothing - end - - let cst = parse_and_pass(""" - sin(1,1) - Base.sin(1,1) - Base.:sin(1,1) - """) - @test errorof(cst[1]) === errorof(cst[2]) === errorof(cst[3]) - end - end - # Non exported function is overloaded - let cst = parse_and_pass(""" - Base.argtail() = nothing - Base.argtail() - """) - @test !haskey(cst.meta.scope.names, "argtail") # - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - end - # As above but for user defined function - let cst = parse_and_pass(""" - module M - ff(x) = nothing - end - M.ff() = nothing - M.ff() - """) - @test !haskey(cst.meta.scope.names, "ff") - @test isempty(StaticLint.collect_hints(cst[3], getenv(server.files[""], server))) - end - - let cst = parse_and_pass(""" - import Base: argtail - Base.argtail() = nothing - Base.argtail() - argtail() - """) - @test cst.meta.scope.names["argtail"] === bindingof(cst[1][2][3][1]) - @test StaticLint.get_method(cst.meta.scope.names["argtail"].refs[2]) isa CSTParser.EXPR - @test cst[3][1][3][1].meta.ref == cst.meta.scope.names["argtail"] - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - end - end - - @testset "on demand resolving of export statements" begin - let cst = parse_and_pass(""" - module TopModule - abstract type T end - export T - module SubModule - using ..TopModule - T - end - end""") - @test refof(cst.args[1].args[3].args[3].args[3].args[2]) !== nothing - end - end - - - @testset "check kw default definition" begin - function kw_default_ok(s) - cst = parse_and_pass(s) - @test errorof(cst.args[1].args[2].args[2]) === nothing - end - function kw_default_notok(s) - cst = parse_and_pass(s) - @test errorof(cst.args[1].args[2].args[2]) == StaticLint.KwDefaultMismatch - end - - kw_default_ok("f(x::Float64 = 0.1)") - kw_default_ok("f(x::Float64 = f())") - kw_default_ok("f(x::Float32 = f())") - kw_default_ok("f(x::Float32 = 3f0") - kw_default_ok("f(x::Float32 = 3_0f0") - kw_default_ok("f(x::Float32 = 0f00") - kw_default_ok("f(x::Float32 = -0f02") - kw_default_ok("f(x::Float32 = Inf32") - kw_default_ok("f(x::Float32 = 30f3") - kw_default_ok("f(x::String = \"1\")") - kw_default_ok("f(x::String = f())") - kw_default_ok("f(x::Symbol = :x") - kw_default_ok("f(x::Symbol = f()") - kw_default_ok("f(x::Char = 'a'") - kw_default_ok("f(x::Bool = true") - kw_default_ok("f(x::Bool = false") - kw_default_ok("f(x::UInt8 = 0b0100_0010") - kw_default_ok("f(x::UInt16 = 0b0000_0000_0000") - kw_default_ok("f(x::UInt32 = 0b00000000000000000000000000000000") - kw_default_ok("f(x::UInt8 = 0o000") - kw_default_ok("f(x::UInt16 = 0o0_0_0_0_0_0") - kw_default_ok("f(x::UInt32 = 0o000000000") - kw_default_ok("f(x::UInt64 = 0o000_000_000_000_0") - kw_default_ok("f(x::UInt8 = 0x0") - kw_default_ok("f(x::UInt16 = 0x0000") - kw_default_ok("f(x::UInt32 = 0x00000") - kw_default_ok("f(x::UInt32 = -0x00000") - kw_default_ok("f(x::UInt64 = 0x0000_0000_0") - kw_default_ok("f(x::UInt128 = 0x00000000_00000000_00000000_00000000") - kw_default_ok("f(x::UInt128 = 0x00000000_00000000_00000000_00000000") - if Sys.WORD_SIZE == 64 - kw_default_ok("f(x::Int64 = 0") - kw_default_ok("f(x::UInt = 0x0000_0000_0") - else - kw_default_ok("f(x::Int32 = 0") - kw_default_ok("f(x::UInt = 0x0000_0") - end - kw_default_ok("f(x::Int = 1)") - kw_default_ok("f(x::Int = f())") - kw_default_ok("f(x::Int8 = Int8(0)") - kw_default_ok("f(x::Int8 = convert(Int8,0)") - - if Sys.WORD_SIZE == 64 - kw_default_notok("f(x::Int8 = 0") - kw_default_notok("f(x::Int16 = 0") - kw_default_notok("f(x::Int32 = 0") - kw_default_notok("f(x::Int64 = 0x0000_0000_0") - kw_default_notok("f(x::Int128 = 0") - else - kw_default_notok("f(x::Int8 = 0") - kw_default_notok("f(x::Int16 = 0") - kw_default_notok("f(x::Int32 = 0x0000_0") - kw_default_notok("f(x::Int64 = 0") - kw_default_notok("f(x::Int128 = 0") - end - kw_default_notok("f(x::Int8 = 0000_0000") - kw_default_notok("f(x::Int16 = 0000_0000") - kw_default_notok("f(x::Int128 = 0000_0000") - kw_default_notok("f(x::Float64 = 1)") - kw_default_notok("f(x::Float32 = 3.4") - kw_default_notok("f(x::Float32 = -23.") - kw_default_notok("f(x::Int = 0.1)") - kw_default_notok("f(x::String = 0.1)") - kw_default_notok("f(x::Symbol = \"a\"") - kw_default_notok("f(x::Char = \"a\"") - kw_default_notok("f(x::Bool = 1") - kw_default_notok("f(x::Bool = 0x01") - kw_default_notok("f(x::UInt8 = 0b000000000") - kw_default_notok("f(x::UInt16 = 0b0000_0000_0000_0000_0") - kw_default_notok("f(x::UInt32 = 0b0") - kw_default_notok("f(x::UInt64 = 0b0_0") - kw_default_notok("f(x::UInt128 = 0b0") - kw_default_notok("f(x::UInt8 = 0o0000") - kw_default_notok("f(x::UInt16 = 0o0") - kw_default_notok("f(x::UInt32 = 0o00000000000000") - kw_default_notok("f(x::UInt64 = 0o0_0") - kw_default_notok("f(x::UInt128 = 0o00") - kw_default_notok("f(x::UInt8 = 0x000") - kw_default_notok("f(x::UInt16 = 0x00000") - kw_default_notok("f(x::UInt32 = 0x0000_00_000") - kw_default_notok("f(x::UInt64 = 0x000_0_0") - kw_default_notok("f(x::UInt128 = 0x000000") - end - - @testset "check_use_of_literal" begin - let cst = parse_and_pass(""" - module \"a\" end - abstract type \"\"\"123\"\"\" end - primitive type 1 8 end - struct 1.0 end - mutable struct 'a' end - 1 = 1 - f(true = 1) - 123::123 - 123 isa false - """) - @test errorof(cst.args[1].args[2]) === StaticLint.InappropriateUseOfLiteral - @test errorof(cst.args[2].args[1]) === StaticLint.InappropriateUseOfLiteral - @test errorof(cst.args[3].args[1]) === StaticLint.InappropriateUseOfLiteral - @test errorof(cst.args[4].args[2]) === StaticLint.InappropriateUseOfLiteral - @test errorof(cst.args[5].args[2]) === StaticLint.InappropriateUseOfLiteral - @test errorof(cst.args[6].args[1]) === StaticLint.InappropriateUseOfLiteral - @test errorof(cst.args[7].args[2].args[1]) === StaticLint.InappropriateUseOfLiteral - @test errorof(cst.args[8].args[2]) === StaticLint.InappropriateUseOfLiteral - @test errorof(cst.args[9].args[3]) === StaticLint.InappropriateUseOfLiteral - end - end - - @testset "check_break_continue" begin - let cst = parse_and_pass(""" - for i = 1:10 - continue - end - break - """) - @test errorof(cst.args[1].args[2].args[1]) === nothing - @test errorof(cst.args[2]) === StaticLint.ShouldBeInALoop - end - end - - @testset "@." begin - let cst = parse_and_pass("@. a + b") - @test StaticLint.hasref(cst.args[1].args[1]) - end - end - - @testset "using" begin - cst = parse_and_pass("using Base") - @test StaticLint.hasbinding(cst.args[1].args[1].args[1]) - - cst = parse_and_pass("using Base.Meta") - @test !StaticLint.hasbinding(cst.args[1].args[1].args[1]) - @test StaticLint.hasbinding(cst.args[1].args[1].args[2]) - @test haskey(cst.meta.scope.modules, :Meta) - - cst = parse_and_pass("using Core.Compiler.Pair") - @test !StaticLint.hasbinding(cst.args[1].args[1].args[1]) - @test !StaticLint.hasbinding(cst.args[1].args[1].args[2]) - @test StaticLint.hasbinding(cst.args[1].args[1].args[3]) - - cst = parse_and_pass("using Base.UUID, Base.any") - @test StaticLint.hasbinding(cst.args[1].args[1].args[2]) - @test StaticLint.hasbinding(cst.args[1].args[2].args[2]) - - cst = parse_and_pass("using Base.Meta: quot, lower") - @test StaticLint.hasbinding(cst.args[1].args[1].args[2].args[1]) - @test StaticLint.hasbinding(cst.args[1].args[1].args[3].args[1]) - - cst = parse_and_pass("using Base.Meta: quot, lower") - end - - @testset "issue 1609" begin - let - cst1 = parse_and_pass("function g(@nospecialize(x), y) x + y end") - cst2 = parse_and_pass("function g(@nospecialize(x), y) y end") - @test !StaticLint.haserror(cst1.args[1].args[1].args[2].args[3]) - @test StaticLint.haserror(cst2.args[1].args[1].args[2].args[3]) - end - end - @testset "j-vsc issue 1835" begin - let - cst = parse_and_pass("""const x::T = x - local const x = 1""") - @test errorof(cst.args[1]) === (VERSION < v"1.8.0-DEV.1500" ? StaticLint.TypeDeclOnGlobalVariable : nothing) - @test errorof(cst.args[2]) === StaticLint.UnsupportedConstLocalVariable - end - end - - @testset "issue 1609" begin - let - cst1 = parse_and_pass("function g(@nospecialize(x), y) x + y end") - cst2 = parse_and_pass("function g(@nospecialize(x) = 1) x end") - cst3 = parse_and_pass("function g(@nospecialize(x) = 1, y = 2) x + y end") - cst4 = parse_and_pass("function g(@nospecialize(x), y) y end") - @test !StaticLint.haserror(cst1.args[1].args[1].args[2].args[3]) - @test !StaticLint.haserror(cst2.args[1].args[1].args[2].args[1]) - @test !StaticLint.haserror(cst3.args[1].args[1].args[2].args[1]) - @test StaticLint.haserror(cst4.args[1].args[1].args[2].args[3]) - end - end - - @testset "issue #226" begin - cst = parse_and_pass("function my_function(::Any...) end") - @test !StaticLint.haserror(cst.args[1].args[1].args[2]) - end - - @testset "issue #218" begin - cst = parse_and_pass(""" - struct Asdf end - - function foo(x) - if x > 0 - ret = Asdf - else - ret = "hello" - end - end - - function foo(x) - if x > 0 - ret = Asdf() - else - ret = "hello" - end - end""") - @test errorof(cst.args[2].args[2].args[1].args[3].args[1].args[1]) !== StaticLint.InvalidRedefofConst - @test errorof(cst.args[3].args[2].args[1].args[3].args[1].args[1]) !== StaticLint.InvalidRedefofConst - end - - if VERSION > v"1.5-" - @testset "issue #210" begin - cst = parse_and_pass("""h()::@NamedTuple{a::Int,b::String} = (a=1, b = "s")""") - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - end - end - if isdefined(Base, Symbol("@kwdef")) - @testset "Base.@kwdef" begin - cst = parse_and_pass(""" - Base.@kwdef struct T - arg = 1 - end""") - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - end - end - @testset "type inference by use" begin - cst = parse_and_pass(""" - f(x::String) = true - function g(x) - f(x) - end""") - @test bindingof(cst.args[2].args[1].args[2]).type !== nothing - - cst = parse_and_pass(""" - f(x::String) = true - f(x::Char) = true - function g(x) - f(x) - end""") - @test bindingof(cst.args[3].args[1].args[2]).type === nothing - - cst = parse_and_pass(""" - f(x::String) = true - f1(x::String) = true - function g(x) - f(x) - f1(x) - end""") - @test bindingof(cst.args[3].args[1].args[2]).type !== nothing - - cst = parse_and_pass(""" - f(x::String) = true - f1(x::Char) = true - function g(x) - f(x) - f1(x) - end""") - @test bindingof(cst.args[3].args[1].args[2]).type === nothing - - cst = parse_and_pass(""" - f(x::String) = true - f1(x) = true - function g(x) - f(x) - f1(x) - end""") - @test bindingof(cst.args[3].args[1].args[2]).type !== nothing - end -end - -@testset "add eval method to modules/toplevel scope" begin - cst = parse_and_pass(""" - module M - expr = :(a + b) - eval(expr) - end - """) - @test !StaticLint.haserror(cst.args[1].args[3].args[2]) - - cst = parse_and_pass(""" - expr = :(a + b) - eval(expr) - """) - @test !StaticLint.haserror(cst.args[2]) -end - -@testset "reparse" begin - cst = parse_and_pass(""" - x = 1 - function f(arg) - x - end - """) - @test StaticLint.hasref(cst.args[2].args[2].args[1]) - StaticLint.clear_meta(cst[2]) - @test !StaticLint.hasref(cst.args[2].args[2].args[1]) - StaticLint.semantic_pass(server.files[""], CSTParser.EXPR[cst[2]]) - @test StaticLint.hasref(cst.args[2].args[2].args[1]) -end - -@testset "duplicate function argument" begin - cst = parse_and_pass(""" - f(a,a) = a - """) - @test errorof(cst[1][1][5]) == StaticLint.DuplicateFuncArgName -end - -@testset "type alias bindings" begin - cst = parse_and_pass(""" - T{S} = Vector{S} - """) - @test haskey(cst.meta.scope.names, "T") - @test haskey(cst[1].meta.scope.names, "S") -end - -@testset ":call w/ :parameters traverse order" begin - cst = parse_and_pass(""" - function f(arg; kw = arg) - arg * kw - end - """) - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) -end - -@testset "handle shadow bindings on method" begin - cst = parse_and_pass(""" - f(x) = 1 - g = f - g(1) - """) - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) -end - -@testset "documented symbol resolving" begin - cst = parse_and_pass(""" - \"\"\" - doc - \"\"\" - func - func(x) = 1 - """) - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) - - cst = parse_and_pass(""" - \"\"\" - doc - \"\"\" - func(a,b)::Int - func(x, b) = 1 - """) - @test isempty(StaticLint.collect_hints(cst, getenv(server.files[""], server))) -end - -@testset "unused bindings" begin - cst = parse_and_pass(""" - function f(arg, arg2) - arg*arg2 - arg3 = 1 - end - """) - @test errorof(cst[1][3][2][1]) !== nothing - - cst = parse_and_pass(""" - function f() - arg = false - while arg - if arg - end - arg = true - end - end - """) - @test isempty(StaticLint.collect_hints(cst, server)) - - cst = parse_and_pass(""" - function f(arg) - arg - while true - arg = 1 - end - end - """) - @test isempty(StaticLint.collect_hints(cst, server)) - - cst = parse_and_pass(""" - function f(arg) - arg - while true - while true - arg = 1 - end - end - end - """) - @test isempty(StaticLint.collect_hints(cst, server)) - - cst = parse_and_pass(""" - function f() - (a = 1, b = 2) - end - """) - @test isempty(StaticLint.collect_hints(cst, server)) - - cst = parse_and_pass(""" - function f() - arg = 0 - if 1 - while true - arg = 1 - end - end - end - """) - @test isempty(StaticLint.collect_hints(cst, server)) -end - -@testset "unwrap sig" begin - cst = parse_and_pass(""" - function multiply!(x::T, y::Integer) where {T} end - multiply!(1, 3) - """) - @test errorof(cst[2]) === nothing - - cst = parse_and_pass(""" - function multiply!(x::T, y::Integer)::T where {T} end - multiply!(1, 3) - """) - @test errorof(cst[2]) === nothing - - @test StaticLint.haserror(parse_and_pass("function f(z::T)::Nothing where T end")[1].args[1].args[1].args[1].args[2]) - @test StaticLint.haserror(parse_and_pass("function f(z::T) where T end")[1].args[1].args[1].args[2]) -end - -@testset "clear .type refs" begin - cst = parse_and_pass(""" - struct T end - function f(x::T) - end - """) - @test bindingof(cst[2][2][3]).type == bindingof(cst[1]) - StaticLint.clear_meta(cst[1]) - @test bindingof(cst[2][2][3]).type === nothing -end - -@testset "clear .type refs" begin - cst = parse_and_pass(""" - struct T{S,R} where S <: Number where R <: Number - end - """) - @test isempty(StaticLint.collect_hints(cst, server)) - - cst = parse_and_pass(""" - struct T{S,R} <: Number where S <: Number - x::S - end - """) - @test isempty(StaticLint.collect_hints(cst, server)) -end - -include("typeinf.jl") - -@testset "where type param infer" begin - cst = parse_and_pass(""" - foo(u::Union) = 1 - function foo(x::T) where {T} - x + foo(T) - end - """) - - @test cst[2].meta.scope.names["T"].type isa SymbolServer.DataTypeStore - @test isempty(StaticLint.collect_hints(cst, server)) -end - -@testset "where type param infer" begin - cst = parse_and_pass(""" - bar(u::Union) = 1 - foo(x::T, y::S, q::V) where {T, S <: V} where {V <: Integer} = x + y + q + bar(S) + bar(T) + bar(V) - """) - - @test cst[2].meta.scope.names["T"].type isa SymbolServer.DataTypeStore - @test cst[2].meta.scope.names["S"].type isa SymbolServer.DataTypeStore - @test cst[2].meta.scope.names["V"].type isa SymbolServer.DataTypeStore - @test isempty(StaticLint.collect_hints(cst, server)) -end - -@testset "softscope" begin - cst = parse_and_pass(""" - function foo() - x = 1 - x - if rand(Bool) - x = 2 - end - x - while rand(Bool) - x = 3 - end - x - for _ in 1:2 - x = 4 - y = 1 - end - x - end - """) - - # check soft-scope bindings are lifted to parent scope - @test refof(cst[1][3][2]) == bindingof(cst[1][3][1][1]) - @test refof(cst[1][3][4]) == bindingof(cst[1][3][3][3][1][1]) - @test refof(cst[1][3][6]) == bindingof(cst[1][3][5][3][1][1]) - @test refof(cst[1][3][8]) == bindingof(cst[1][3][7][3][1][1]) - - # check binding made in soft-scope with no matching binidng in parent scope isn't lifted - @test !haskey(scopeof(cst[1]).names, "y") - @test haskey(scopeof(cst[1][3][7]).names, "y") - - - @test length(StaticLint.loose_refs(bindingof(cst[1][3][1][1]))) == 8 - @test length(StaticLint.loose_refs(bindingof(cst[1][3][3][3][1][1]))) == 8 - @test length(StaticLint.loose_refs(bindingof(cst[1][3][5][3][1][1]))) == 8 - @test length(StaticLint.loose_refs(bindingof(cst[1][3][7][3][1][1]))) == 8 - - cst = parse_and_pass(""" - function foo() - for _ in 1:2 - x = 1 - x - end - x - x = 1 - x - end - """) - @test length(StaticLint.loose_refs(bindingof(cst[1][3][1][3][1][1]))) == 2 - @test length(StaticLint.loose_refs(bindingof(cst[1][3][3][1]))) == 2 -end - -# @testset "test workspace packages" begin -# empty!(server.files) -# s1 = """ -# module WorkspaceMod -# inner_sym = 1 -# exported_sym = 1 -# export exported_sym -# end""" -# f1 = StaticLint.File("workspacemod.jl", s1, CSTParser.parse(s1, true), nothing, server) -# StaticLint.setroot(f1, f1) -# StaticLint.setfile(server, f1.path, f1) -# StaticLint.semantic_pass(f1) -# server.workspacepackages["WorkspaceMod"] = f1 -# s2 = """ -# using WorkspaceMod -# exported_sym -# WorkspaceMod.inner_sym -# """ -# f2 = StaticLint.File("someotherfile.jl", s2, CSTParser.parse(s2, true), nothing, server) -# StaticLint.setroot(f2, f2) -# StaticLint.setfile(server, f2.path, f2) -# StaticLint.semantic_pass(f2) -# @test StaticLint.hasref(StaticLint.getcst(f2)[1][2][1]) -# @test StaticLint.hasref(StaticLint.getcst(f2)[2]) -# @test StaticLint.hasref(StaticLint.getcst(f2)[3][3][1]) -# end -@testset "#1218" begin - cst = parse_and_pass("""function foo(a; p) a+p end - foo(1, p = true)""") - @test isempty(StaticLint.collect_hints(cst, server)) - - cst = parse_and_pass("""function foo(a; p) a end - foo(1, p = true)""") - @test cst[1][2][4][1].meta.error != false - - cst = parse_and_pass("""function foo(a; p::Bool) a+p end - foo(1, p = true)""") - @test isempty(StaticLint.collect_hints(cst, server)) - - cst = parse_and_pass("""function foo(a; p::Bool) a end - foo(1, p = true)""") - @test cst[1][2][4][1].meta.error != false -end - - -if Meta.parse("import a as b", raise = false).head !== :error - @testset "import as ..." begin - cst = parse_and_pass("""import Base as base""") - @test StaticLint.hasbinding(cst[1][2][3]) - @test !StaticLint.hasbinding(cst[1][2][1][1]) - - # incomplete expressinon should not error - cst = parse_and_pass("""import Base as""") - end -end - - -@testset "#1218" begin - cst = parse_and_pass(""" - module Sup - function myfunc end - module SubA - import ..myfunc - myfunc(x::Int) = println("hello Int: ", x) # Cannot define function ; it already has a value. - end # module - - end - """) - @test isempty(StaticLint.collect_hints(cst, server)) - -end - - -@testset "macrocall bindings: #2187" begin - cst = parse_and_pass(""" - function f(url = 1, file = 1) - @info "Downloading" source = url dest = file - return nothing - end - """) - @test !isempty(StaticLint.collect_hints(cst, server)) -end - -@testset "aliased import: #974" begin - cst = parse_and_pass(""" - const CC = Core.Compiler - import .CC: div - """) - @test isempty(StaticLint.collect_hints(cst, server)) - - cst = parse_and_pass(""" - const C = Core - import .C: div - """) - @test isempty(StaticLint.collect_hints(cst, server)) -end - -@testset "kwarg refs" begin - cst = parse_and_pass(""" - function foo(aaa, bbb; ccc) - return aaa + bbb + ccc - end - """) - for (_, b) in cst.args[1].meta.scope.names - @test length(b.refs) == 2 - end - - cst = parse_and_pass(""" - function foo(aaa, bbb::Foo; ccc::Bar) - return aaa + bbb + ccc - end - """) - for (_, b) in cst.args[1].meta.scope.names - @test length(b.refs) == 2 - end - - cst = parse_and_pass(""" - function foo(aaa, bbb=1; ccc=2) - return aaa + bbb + ccc - end - """) - for (_, b) in cst.args[1].meta.scope.names - @test length(b.refs) == 2 - end - cst = parse_and_pass(""" - function foo(aaa, bbb::Foo=1; ccc::Bar=2) - return aaa + bbb + ccc - end - """) - for (_, b) in cst.args[1].meta.scope.names - @test length(b.refs) == 2 - end -end - -@testset "iteration over 1:length(...)" begin - cst = parse_and_pass("arr = []; [1 for _ in 1:length(arr)]") - @test isempty(StaticLint.collect_hints(cst, server)) - cst = parse_and_pass("arr = []; [arr[i] for i in 1:length(arr)]") - @test length(StaticLint.collect_hints(cst, server)) == 2 - cst = parse_and_pass("arr = []; [i for i in 1:length(arr)]") - @test length(StaticLint.collect_hints(cst, server)) == 0 - - cst = parse_and_pass(""" - arr = [] - for _ in 1:length(arr) - end - """) - @test isempty(StaticLint.collect_hints(cst, server)) - cst = parse_and_pass(""" - arr = [] - for i in 1:length(arr) - arr[i] - end - """) - @test length(StaticLint.collect_hints(cst, server)) == 2 - cst = parse_and_pass(""" - arr = [] - for i in 1:length(arr) - println(i) - end - """) - @test length(StaticLint.collect_hints(cst, server)) == 0 - - cst = parse_and_pass(""" - arr = [] - for _ in 1:length(arr), _ in 1:length(arr) - end - """) - @test isempty(StaticLint.collect_hints(cst, server)) - cst = parse_and_pass(""" - arr = [] - for i in 1:length(arr), j in 1:length(arr) - arr[i] + arr[j] - end - """) - @test length(StaticLint.collect_hints(cst, server)) == 4 - cst = parse_and_pass(""" - arr = [] - for i in 1:length(arr), j in 1:length(arr) - println(i + j) - end - """) - @test length(StaticLint.collect_hints(cst, server)) == 0 - - cst = parse_and_pass(""" - function f(arr::Vector) - for i in 1:length(arr), j in 1:length(arr) - arr[i] + arr[j] - end - end - """) - @test length(StaticLint.collect_hints(cst, server)) == 0 - - cst = parse_and_pass(""" - function f(arr::Array) - for i in 1:length(arr), j in 1:length(arr) - arr[i] + arr[j] - end - end - """) - @test length(StaticLint.collect_hints(cst, server)) == 0 - - cst = parse_and_pass(""" - function f(arr::Matrix) - for i in 1:length(arr), j in 1:length(arr) - arr[i] + arr[j] - end - end - """) - @test length(StaticLint.collect_hints(cst, server)) == 0 - - cst = parse_and_pass(""" - function f(arr::Array{T,N}) where T where N - for i in 1:length(arr), j in 1:length(arr) - arr[i] + arr[j] - end - end - """) - @test length(StaticLint.collect_hints(cst, server)) == 0 - - cst = parse_and_pass(""" - function f(arr::AbstractArray) - for i in 1:length(arr), j in 1:length(arr) - arr[i] + arr[j] - end - end - """) - @test length(StaticLint.collect_hints(cst, server)) == 4 - - cst = parse_and_pass(""" - function f(arr) - for i in 1:length(arr), j in 1:length(arr) - arr[i] + arr[j] - end - end - """) - @test length(StaticLint.collect_hints(cst, server)) == 4 -end - -@testset "assigned but not used with loops" begin - cst = parse_and_pass(""" - function a!(v) - next = 0 - for i in eachindex(v) - current = next - next = sin(current) - while true - current = next - next = sin(current) - end - v[i] = current - end - end - """) - @test isempty(StaticLint.collect_hints(cst, server)) - cst = parse_and_pass(""" - function f(v) - next = 0 - for _ in v - foo = next - for _ in v - next = foo - end - foo = sin(next) - end - end - """) - @test isempty(StaticLint.collect_hints(cst, server)) -end - -@testset "macro definition" begin - cst = parse_and_pass(""" - module JumpToMacroDoesNotWork - export @mymacro - - macro mymacro() - end - end - - JumpToMacroDoesNotWork.@mymacro(1+1) - """) - m = cst.args[end].args[1].args[2].args[1] - methods = Set() - for r in m.meta.ref.refs - m = StaticLint.get_method(r) - if m !== nothing - push!(methods, m) - end - end - - @test !isempty(methods) -end diff --git a/test/typeinf.jl b/test/typeinf.jl deleted file mode 100644 index bd5bead..0000000 --- a/test/typeinf.jl +++ /dev/null @@ -1,109 +0,0 @@ -@testset "type inference by use" begin -cst = parse_and_pass(""" -struct T -end - -struct S -end - -f(x::T) = 1 -g(x::S) = 1 - -function ex1(x) - f(x) -end - -function ex2(x) - f(x) - g(x) -end - -function ex3(x) - if 1 - f(x) - else - f(x) - end -end - -function ex4(x) - x - if 1 - f(x) - end -end - -function ex5(x) - if 1 - f(x) - else - g(x) - end -end - -function ex6(x) - if 1 - y = x - f(y) - else - g(x) - end -end -"""); - -T = cst.meta.scope.names["T"] -S = cst.meta.scope.names["S"] - -@test cst.meta.scope.names["ex1"].val.meta.scope.names["x"].type == T -@test cst.meta.scope.names["ex2"].val.meta.scope.names["x"].type === nothing -@test cst.meta.scope.names["ex3"].val.meta.scope.names["x"].type === nothing -@test cst.meta.scope.names["ex4"].val.meta.scope.names["x"].type === nothing -@test cst.meta.scope.names["ex5"].val.meta.scope.names["x"].type === nothing -@test cst.meta.scope.names["ex6"].val.meta.scope.names["y"].type === T -end - - -@testset "loop iterator inference" begin -cst = parse_and_pass(""" -begin -abstract type T end -X = Int[] -Y = T[] -end - -for x in 1 end -for x in "abc" end -for x in 1:10 end -for x in 1.0:10.0 end -for x in Int[1,2,3] end -for x in X end -for y in Y end -"""); - -@test cst.args[2].meta.scope.names["x"].type === nothing -@test StaticLint.CoreTypes.ischar(cst.args[3].meta.scope.names["x"].type) -@test StaticLint.CoreTypes.isint(cst.args[4].meta.scope.names["x"].type) -@test StaticLint.CoreTypes.isfloat(cst.args[5].meta.scope.names["x"].type) -@test StaticLint.CoreTypes.isint(cst.args[6].meta.scope.names["x"].type) -@test StaticLint.CoreTypes.isint(cst.args[7].meta.scope.names["x"].type) -@test StaticLint.CoreTypes.isint(cst.args[7].meta.scope.names["x"].type) -@test cst.args[8].meta.scope.names["y"].type == cst.meta.scope.names["T"] -end - -@testset "Vector{T} infer" begin -cst = parse_and_pass(""" -struct T - t1 -end -struct S - s1::Vector{T} -end - -function f(s::S) - t = s.s1[1] - t # This should be inferred as T -end -""") - -@test cst[3].meta.scope.names["t"].type == cst.meta.scope.names["T"] -end From 41be69b6aa8bdaeac45d0e0f40c464dac26d4a31 Mon Sep 17 00:00:00 2001 From: Alexandre Bergel Date: Wed, 23 Oct 2024 11:58:01 +0200 Subject: [PATCH 02/12] Cutting cutting cutting... --- src/StaticLint.jl | 2 +- src/interface.jl | 109 +++++++++------------------------------- test/rai_rules_tests.jl | 6 +-- 3 files changed, 26 insertions(+), 91 deletions(-) diff --git a/src/StaticLint.jl b/src/StaticLint.jl index 2e0a7a0..be530e2 100644 --- a/src/StaticLint.jl +++ b/src/StaticLint.jl @@ -405,5 +405,5 @@ include("utils.jl") include("interface.jl") export MarkdownFormat, PlainFormat -export run_lint, essential_filters +export run_lint end diff --git a/src/interface.jl b/src/interface.jl index 4dd65f0..22d1abf 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -69,47 +69,6 @@ function has_values(l::LintResult, a, b, c) l.recommendations_count == c end -function setup_server( - env = dirname(SymbolServer.Pkg.Types.Context().env.project_file), - depot = first(SymbolServer.Pkg.depots()), - cache = joinpath(dirname(pathof(SymbolServer)), "..", "store") -) - server = StaticLint.FileServer() - ssi = SymbolServerInstance(depot, cache) - _, symbols = SymbolServer.getstore(ssi, env) - extended_methods = SymbolServer.collect_extended_methods(symbols) - server.external_env = ExternalEnv(symbols, extended_methods, Symbol[]) - server -end - -""" - lint_string(s, server; gethints = false) - -Parse a string and run a semantic pass over it. This will mark scopes, bindings, -references, and lint hints. An annotated `EXPR` is returned or, if `gethints = true`, -it is paired with a collected list of errors/hints. -""" -function lint_string(s::String, server = setup_server(); gethints = false) - empty!(server.files) - f = File("", s, CSTParser.parse(s, true), nothing, server) - env = getenv(f, server) - setroot(f, f) - setfile(server, "", f) - semantic_pass(f) - check_all(f.cst, LintOptions(), env) - if gethints - hints = [] - for (offset, x) in collect_hints(f.cst, env) - if haserror(x) - push!(hints, (x, LintCodeDescriptions[x.meta.error])) - push!(hints, (x, "Missing reference.", " at offset ", offset)) - end - end - return f.cst, hints - else - return f.cst - end -end """ lint_file(rootpath, server) @@ -120,7 +79,7 @@ in the project will be loaded automatically (calls to `include` with complicated are not handled, see `followinclude` for details). A `FileServer` will be returned containing the `File`s of the package. """ -function lint_file(rootpath; gethints = false) +function lint_file(rootpath) file_content_string = open(io->read(io, String), rootpath, "r") ast = CSTParser.parse(file_content_string, true) @@ -129,48 +88,30 @@ function lint_file(rootpath; gethints = false) lint_rule_reports = [] - hints = [] # TODO TO REMOVE - hints_for_file = [] # TODO TO REMOVE for (offset, x) in collect_lint_report(ast) if haserror(x) - # TODO: On some point, we should only have the LintRuleReport case - if x.meta.error isa String - push!(hints_for_file, (x, string(x.meta.error, " at offset ", offset, " of ", p))) - elseif x.meta.error isa LintRuleReport - # The next line should be deleted - push!(hints_for_file, (x, string(x.meta.error.msg, " at offset ", offset, " of ", rootpath))) - - lint_rule_report = x.meta.error - lint_rule_report.offset = offset - - line_number, column, annotation_line = convert_offset_to_line_from_filename(lint_rule_report.offset + 1, lint_rule_report.file) - lint_rule_report.line = line_number - lint_rule_report.column = column - - # If the annotation is to disable lint, - if annotation_line == "lint-disable-line" - # then we disable it. - elseif !isnothing(annotation_line) && startswith("lint-disable-line: $(lint_rule_report.msg)", annotation_line) - # then we disable it. - else - # Else we record it. - push!(lint_rule_reports, lint_rule_report) - end + # The next line should be deleted + lint_rule_report = x.meta.error + lint_rule_report.offset = offset + + line_number, column, annotation_line = convert_offset_to_line_from_filename(lint_rule_report.offset + 1, lint_rule_report.file) + lint_rule_report.line = line_number + lint_rule_report.column = column + + # If the annotation is to disable lint, + if annotation_line == "lint-disable-line" + # then we disable it. + elseif !isnothing(annotation_line) && startswith("lint-disable-line: $(lint_rule_report.msg)", annotation_line) + # then we disable it. else - push!(hints_for_file, (x, string(LintCodeDescriptions[x.meta.error], " at offset ", offset, " of ", p))) + # Else we record it. + push!(lint_rule_reports, lint_rule_report) end - push!(hints_for_file, (x, string("Missing reference.", " at offset ", offset, " of ", rootpath))) end end - append!(hints, hints_for_file) - return "TODO", hints, lint_rule_reports + return "TODO", 0, lint_rule_reports end -global global_server = nothing - -const no_filters = LintCodes[] -const essential_filters = [no_filters; [StaticLint.MissingReference, StaticLint.MissingFile, StaticLint.InvalidTypeDeclaration]] - # Return (index_line, index_column, annotation) for a given offset in a source function convert_offset_to_line_from_filename(offset::Union{Int64, Int32}, filename::String) all_lines = open(io->readlines(io), filename) @@ -307,7 +248,6 @@ function _run_lint_on_dir( io::Union{IO,Nothing}=stdout, io_violations::Union{IO,Nothing}=nothing, io_recommendations::Union{IO,Nothing}=nothing, - filters::Vector{LintCodes}=essential_filters, formatter::AbstractFormatter=PlainFormat() ) # Exit if we are in .git @@ -317,14 +257,14 @@ function _run_lint_on_dir( for file in files filename = joinpath(root, file) if endswith(filename, ".jl") - run_lint(filename; result, io, io_violations, io_recommendations, filters, formatter) + run_lint(filename; result, io, io_violations, io_recommendations, formatter) end end for dir in dirs p = joinpath(root, dir) !isnothing(match(r".*/\.git.*", p)) && continue - _run_lint_on_dir(p; result, io, io_violations, io_recommendations, filters, formatter) + _run_lint_on_dir(p; result, io, io_violations, io_recommendations, formatter) end end return result @@ -405,7 +345,7 @@ print_summary( """ - run_lint(rootpath::String; server = global_server, io::IO=stdout, io_violations::Union{IO,Nothing}, io_recommendations::Union{IO,Nothing}) + run_lint(rootpath::String; io::IO=stdout, io_violations::Union{IO,Nothing}, io_recommendations::Union{IO,Nothing}) Run lint rules on a file `rootpath`, which must be an existing non-folder file. Return a LintResult. @@ -420,21 +360,20 @@ function run_lint( io::Union{IO,Nothing}=stdout, io_violations::Union{IO,Nothing}=nothing, io_recommendations::Union{IO,Nothing}=nothing, - filters::Vector{LintCodes}=essential_filters, formatter::AbstractFormatter=PlainFormat() ) # If already linted, then we merely exit rootpath in result.linted_files && return result # If we are running Lint on a directory - isdir(rootpath) && return _run_lint_on_dir(rootpath; result, io, io_violations, io_recommendations, filters, formatter) + isdir(rootpath) && return _run_lint_on_dir(rootpath; result, io, io_violations, io_recommendations, formatter) # Check if we have to be run on a Julia file. Simply exit if not. # This simplify the amount of work in GitHub Action endswith(rootpath, ".jl") || return result # We are running Lint on a Julia file - _,_,lint_reports = StaticLint.lint_file(rootpath; gethints = true) + _,_,lint_reports = StaticLint.lint_file(rootpath) print_header(formatter, io, rootpath) is_recommendation(r::LintRuleReport) = r.rule isa RecommendationExtendedRule @@ -489,7 +428,6 @@ function run_lint_on_text( source::String; result::LintResult=LintResult(), io::Union{IO,Nothing}=stdout, - filters::Vector{LintCodes}=essential_filters, formatter::AbstractFormatter=PlainFormat(), directory::String = "" # temporary directory to be created. If empty, let Julia decide ) @@ -509,7 +447,7 @@ function run_lint_on_text( open(tmp_file_name, "w") do file write(file, source) flush(file) - run_lint(tmp_file_name; result, io, io_violations, io_recommendations, filters, formatter) + run_lint(tmp_file_name; result, io, io_violations, io_recommendations, formatter) end print(io, String(take!(io_violations))) @@ -648,7 +586,6 @@ function generate_report( io = output_io, io_violations = io_violations, io_recommendations = io_recommendations, - filters = essential_filters, formatter ) end diff --git a/test/rai_rules_tests.jl b/test/rai_rules_tests.jl index 965faf7..40b04cf 100644 --- a/test/rai_rules_tests.jl +++ b/test/rai_rules_tests.jl @@ -798,7 +798,7 @@ end @testset "Plain 02" begin io = IOBuffer() - run_lint_on_text(source; io=io, filters=StaticLint.essential_filters) + run_lint_on_text(source; io=io) result = String(take!(io)) expected = r""" @@ -812,7 +812,7 @@ end @testset "Markdown 02" begin io = IOBuffer() - run_lint_on_text(source; io=io, filters=StaticLint.essential_filters, formatter=MarkdownFormat()) + run_lint_on_text(source; io=io, formatter=MarkdownFormat()) result = String(take!(io)) expected = r""" @@ -828,7 +828,6 @@ end run_lint_on_text( source; io, - filters=StaticLint.essential_filters, formatter, directory="/src/Compiler/") result = String(take!(io)) @@ -845,7 +844,6 @@ end run_lint_on_text( source; io, - filters=StaticLint.essential_filters, formatter, directory="src/Compiler/") result = String(take!(io)) From c79c84c22cf33262c7ee075b73942c652f18d370 Mon Sep 17 00:00:00 2001 From: Alexandre Bergel Date: Wed, 23 Oct 2024 12:24:04 +0200 Subject: [PATCH 03/12] Cleaning cleaning... --- src/StaticLint.jl | 408 +------------------------------- src/StaticLint2.jl | 409 +++++++++++++++++++++++++++++++++ src/linting/checks.jl | 76 +++--- src/linting/extended_checks.jl | 83 +++++++ test/runtests.jl | 2 +- 5 files changed, 539 insertions(+), 439 deletions(-) create mode 100644 src/StaticLint2.jl diff --git a/src/StaticLint.jl b/src/StaticLint.jl index be530e2..580be3e 100644 --- a/src/StaticLint.jl +++ b/src/StaticLint.jl @@ -1,409 +1,17 @@ module StaticLint -include("exception_types.jl") +using CSTParser: CSTParser, EXPR +import InteractiveUtils -using SymbolServer, CSTParser - -using CSTParser: EXPR, isidentifier, setparent!, valof, headof, hastrivia, parentof, isoperator, ispunctuation, to_codeobject -# CST utils -using CSTParser: is_getfield, isassignment, isdeclaration, isbracketed, iskwarg, iscall, iscurly, isunarycall, isunarysyntax, isbinarycall, isbinarysyntax, issplat, defines_function, is_getfield_w_quotenode, iswhere, iskeyword, isstringliteral, isparameters, isnonstdid, istuple -using SymbolServer: VarRef - -const noname = EXPR(:noname, nothing, nothing, 0, 0, nothing, nothing, nothing) - -include("coretypes.jl") -include("bindings.jl") -include("scope.jl") -include("subtypes.jl") -include("methodmatching.jl") - -const LARGE_FILE_LIMIT = 2_000_000 # bytes - -mutable struct Meta - binding::Union{Nothing,Binding} - scope::Union{Nothing,Scope} - ref::Union{Nothing,Binding,SymbolServer.SymStore} +mutable struct LintMeta error -end -Meta() = Meta(nothing, nothing, nothing, nothing) - -function Base.show(io::IO, m::Meta) - m.binding !== nothing && show(io, m.binding) - m.ref !== nothing && printstyled(io, " * ", color = :red) - m.scope !== nothing && printstyled(io, " new scope", color = :green) - m.error !== nothing && printstyled(io, " lint ", color = :red) -end -hasmeta(x::EXPR) = x.meta isa Meta -hasbinding(m::Meta) = m.binding isa Binding -hasref(m::Meta) = m.ref !== nothing -hasscope(m::Meta) = m.scope isa Scope -scopeof(m::Meta) = m.scope -bindingof(m::Meta) = m.binding - - -""" - ExternalEnv - -Holds a representation of an environment cached by SymbolServer. -""" -mutable struct ExternalEnv - symbols::SymbolServer.EnvStore - extended_methods::Dict{SymbolServer.VarRef,Vector{SymbolServer.VarRef}} - project_deps::Vector{Symbol} -end - -abstract type State end -mutable struct Toplevel{T} <: State - file::T - included_files::Vector{String} - scope::Scope - in_modified_expr::Bool - modified_exprs::Union{Nothing,Vector{EXPR}} - delayed::Vector{EXPR} - resolveonly::Vector{EXPR} - env::ExternalEnv - server - flags::Int -end - -Toplevel(file, included_files, scope, in_modified_expr, modified_exprs, delayed, resolveonly, env, server) = - Toplevel(file, included_files, scope, in_modified_expr, modified_exprs, delayed, resolveonly, env, server, 0) - -function (state::Toplevel)(x::EXPR) - resolve_import(x, state) - mark_bindings!(x, state) - add_binding(x, state) - mark_globals(x, state) - handle_macro(x, state) - s0 = scopes(x, state) - resolve_ref(x, state) - - # DO NOT FOLLOW INCLUDE ANYMORE - # followinclude(x, state) - - old_in_modified_expr = state.in_modified_expr - if state.modified_exprs !== nothing && x in state.modified_exprs - state.in_modified_expr = true - end - if CSTParser.defines_function(x) || CSTParser.defines_macro(x) || headof(x) === :export - if state.in_modified_expr - push!(state.delayed, x) - else - push!(state.resolveonly, x) - end - else - old = flag!(state, x) - traverse(x, state) - state.flags = old - end - - state.in_modified_expr = old_in_modified_expr - state.scope != s0 && (state.scope = s0) - return state.scope -end - -mutable struct Delayed <: State - scope::Scope - env::ExternalEnv - server - flags::Int -end - -Delayed(scope, env, server) = Delayed(scope, env, server, 0) - -function (state::Delayed)(x::EXPR) - mark_bindings!(x, state) - add_binding(x, state) - mark_globals(x, state) - handle_macro(x, state) - s0 = scopes(x, state) - resolve_ref(x, state) - - old = flag!(state, x) - traverse(x, state) - state.flags = old - if state.scope != s0 - for b in values(state.scope.names) - infer_type_by_use(b, state.env) - check_unused_binding(b, state.scope) - end - state.scope = s0 - end - return state.scope -end - -mutable struct ResolveOnly <: State - scope::Scope - env::ExternalEnv - server -end - -function (state::ResolveOnly)(x::EXPR) - if hasscope(x) - s0 = state.scope - state.scope = scopeof(x) - else - s0 = state.scope - end - resolve_ref(x, state) - - traverse(x, state) - if state.scope != s0 - state.scope = s0 - end - return state.scope -end - -# feature flags that can disable or enable functionality further down in the CST -const NO_NEW_BINDINGS = 0x1 - -function flag!(state, x::EXPR) - old = state.flags - if CSTParser.ismacrocall(x) && (valof(x.args[1]) == "@." || valof(x.args[1]) == "@__dot__") - state.flags |= NO_NEW_BINDINGS - end - return old -end - -""" - semantic_pass(file, modified_expr=nothing) - -Performs a semantic pass across a project from the entry point `file`. A first pass traverses the top-level scope after which secondary passes handle delayed scopes (e.g. functions). These secondary passes can be, optionally, very light and only seek to resovle references (e.g. link symbols to bindings). This can be done by supplying a list of expressions on which the full secondary pass should be made (`modified_expr`), all others will receive the light-touch version. -""" -function semantic_pass(file, modified_expr = nothing) - server = file.server - env = getenv(file, server) - setscope!(getcst(file), Scope(nothing, getcst(file), Dict(), Dict{Symbol,Any}(:Base => env.symbols[:Base], :Core => env.symbols[:Core]), nothing)) - state = Toplevel(file, [getpath(file)], scopeof(getcst(file)), modified_expr === nothing, modified_expr, EXPR[], EXPR[], env, server) - state(getcst(file)) - for x in state.delayed - if hasscope(x) - traverse(x, Delayed(scopeof(x), env, server)) - for (k, b) in scopeof(x).names - infer_type_by_use(b, env) - check_unused_binding(b, scopeof(x)) - end - else - traverse(x, Delayed(retrieve_delayed_scope(x), env, server)) - end - end - if state.resolveonly !== nothing - for x in state.resolveonly - if hasscope(x) - traverse(x, ResolveOnly(scopeof(x), env, server)) - else - traverse(x, ResolveOnly(retrieve_delayed_scope(x), env, server)) - end - end - end -end - -""" - traverse(x, state) - -Iterates across the child nodes of an EXPR in execution order (rather than -storage order) calling `state` on each node. -""" -function traverse(x::EXPR, state) - if (isassignment(x) && !(CSTParser.is_func_call(x.args[1]) || CSTParser.iscurly(x.args[1]))) || CSTParser.isdeclaration(x) - state(x.args[2]) - state(x.args[1]) - elseif CSTParser.iswhere(x) - for i = 2:length(x.args) - state(x.args[i]) - end - state(x.args[1]) - elseif headof(x) === :generator || headof(x) === :filter - @inbounds for i = 2:length(x.args) - state(x.args[i]) - end - state(x.args[1]) - elseif headof(x) === :call && length(x.args) > 1 && headof(x.args[2]) === :parameters - state(x.args[1]) - @inbounds for i = 3:length(x.args) - state(x.args[i]) - end - state(x.args[2]) - elseif x.args !== nothing && length(x.args) > 0 - @inbounds for i = 1:length(x.args) - state(x.args[i]) - end - end -end - -function check_filesize(x, path) - nb = try - filesize(path) - catch - seterror!(x, FileNotAvailable) - return false - end - - toobig = nb > LARGE_FILE_LIMIT - if toobig - seterror!(x, FileTooBig) - end - return !toobig + LintMeta() = new(nothing) + LintMeta(v) = new(v) end -""" - followinclude(x, state) +# include("linting/checks.jl") -Checks whether the arguments of a call to `include` can be resolved to a path. -If successful it checks whether a file with that path is loaded on the server -or a file exists on the disc that can be loaded. -If this is successful it traverses the code associated with the loaded file. -""" -function followinclude(x, state::State) - # this runs on the `include` symbol instead of a function call so that we - # can be sure the ref has already been resolved - isinclude = isincludet = false - p = x - if isidentifier(x) && hasref(x) - r = x.meta.ref +include("linting/extended_checks.jl") - if is_in_fexpr(x, iscall) - p = get_parent_fexpr(x, iscall) - if r == refof_call_func(p) - isinclude = r.name == SymbolServer.VarRef(SymbolServer.VarRef(nothing, :Base), :include) - isincludet = r.name == SymbolServer.VarRef(SymbolServer.VarRef(nothing, :Revise), :includet) - end - end - end - - if !(isinclude || isincludet) - return - end - - x = p - - init_path = path = get_path(x, state) - if isempty(path) - elseif isabspath(path) - if hasfile(state.server, path) - elseif canloadfile(state.server, path) - if check_filesize(x, path) - loadfile(state.server, path) - else - return - end - else - path = "" - end - elseif !isempty(getpath(state.file)) && isabspath(joinpath(dirname(getpath(state.file)), path)) - # Relative path from current - if hasfile(state.server, joinpath(dirname(getpath(state.file)), path)) - path = joinpath(dirname(getpath(state.file)), path) - elseif canloadfile(state.server, joinpath(dirname(getpath(state.file)), path)) - path = joinpath(dirname(getpath(state.file)), path) - if check_filesize(x, path) - loadfile(state.server, path) - else - return - end - else - path = "" - end - elseif !isempty((basepath = _is_in_basedir(getpath(state.file)); basepath)) - # Special handling for include method used within Base - path = joinpath(basepath, path) - if hasfile(state.server, path) - # skip - elseif canloadfile(state.server, path) - loadfile(state.server, path) - else - path = "" - end - else - path = "" - end - if hasfile(state.server, path) - if path in state.included_files - seterror!(x, IncludeLoop) - return - end - f = getfile(state.server, path) - - if f.cst.fullspan > LARGE_FILE_LIMIT - seterror!(x, FileTooBig) - return - end - oldfile = state.file - state.file = f - push!(state.included_files, getpath(state.file)) - setroot(state.file, getroot(oldfile)) - setscope!(getcst(state.file), nothing) - state(getcst(state.file)) - state.file = oldfile - pop!(state.included_files) - elseif !is_in_fexpr(x, CSTParser.defines_function) && !isempty(init_path) - seterror!(x, MissingFile) - end -end - -""" - get_path(x::EXPR) - -Usually called on the argument to `include` calls, and attempts to determine -the path of the file to be included. Has limited support for `joinpath` calls. -""" -function get_path(x::EXPR, state) - if CSTParser.iscall(x) && length(x.args) == 2 - parg = x.args[2] - - if CSTParser.isstringliteral(parg) - if occursin("\0", valof(parg)) - seterror!(parg, IncludePathContainsNULL) - return "" - end - path = CSTParser.str_value(parg) - path = normpath(path) - Base.containsnul(path) && throw(SLInvalidPath("Couldn't convert '$x' into a valid path. Got '$path'")) - return path - elseif CSTParser.ismacrocall(parg) && valof(parg.args[1]) == "@raw_str" && CSTParser.isstringliteral(parg.args[3]) - if occursin("\0", valof(parg.args[3])) - seterror!(parg.args[3], IncludePathContainsNULL) - return "" - end - path = normpath(CSTParser.str_value(parg.args[3])) - Base.containsnul(path) && throw(SLInvalidPath("Couldn't convert '$x' into a valid path. Got '$path'")) - return path - elseif CSTParser.iscall(parg) && isidentifier(parg.args[1]) && valofid(parg.args[1]) == "joinpath" - path_elements = String[] - - for i = 2:length(parg.args) - arg = parg[i] - if _is_macrocall_to_BaseDIR(arg) # Assumes @__DIR__ points to Base macro. - push!(path_elements, dirname(getpath(state.file))) - elseif CSTParser.isstringliteral(arg) - if occursin("\0", valof(arg)) - seterror!(arg, IncludePathContainsNULL) - return "" - end - push!(path_elements, string(valof(arg))) - else - return "" - end - end - isempty(path_elements) && return "" - - path = normpath(joinpath(path_elements...)) - Base.containsnul(path) && throw(SLInvalidPath("Couldn't convert '$x' into a valid path. Got '$path'")) - return path - end - end - return "" -end - -include("server.jl") -include("imports.jl") -include("references.jl") -include("macros.jl") -include("linting/checks.jl") -include("type_inf.jl") -include("utils.jl") include("interface.jl") - -export MarkdownFormat, PlainFormat -export run_lint -end +end \ No newline at end of file diff --git a/src/StaticLint2.jl b/src/StaticLint2.jl new file mode 100644 index 0000000..de43620 --- /dev/null +++ b/src/StaticLint2.jl @@ -0,0 +1,409 @@ +module StaticLint2 + +include("exception_types.jl") + +using SymbolServer, CSTParser + +using CSTParser: EXPR, isidentifier, setparent!, valof, headof, hastrivia, parentof, isoperator, ispunctuation, to_codeobject +# CST utils +using CSTParser: is_getfield, isassignment, isdeclaration, isbracketed, iskwarg, iscall, iscurly, isunarycall, isunarysyntax, isbinarycall, isbinarysyntax, issplat, defines_function, is_getfield_w_quotenode, iswhere, iskeyword, isstringliteral, isparameters, isnonstdid, istuple +using SymbolServer: VarRef + +const noname = EXPR(:noname, nothing, nothing, 0, 0, nothing, nothing, nothing) + +include("coretypes.jl") +include("bindings.jl") +include("scope.jl") +include("subtypes.jl") +include("methodmatching.jl") + +const LARGE_FILE_LIMIT = 2_000_000 # bytes + +mutable struct Meta + binding::Union{Nothing,Binding} + scope::Union{Nothing,Scope} + ref::Union{Nothing,Binding,SymbolServer.SymStore} + error +end +Meta() = Meta(nothing, nothing, nothing, nothing) + +function Base.show(io::IO, m::Meta) + m.binding !== nothing && show(io, m.binding) + m.ref !== nothing && printstyled(io, " * ", color = :red) + m.scope !== nothing && printstyled(io, " new scope", color = :green) + m.error !== nothing && printstyled(io, " lint ", color = :red) +end +hasmeta(x::EXPR) = x.meta isa Meta +hasbinding(m::Meta) = m.binding isa Binding +hasref(m::Meta) = m.ref !== nothing +hasscope(m::Meta) = m.scope isa Scope +scopeof(m::Meta) = m.scope +bindingof(m::Meta) = m.binding + + +""" + ExternalEnv + +Holds a representation of an environment cached by SymbolServer. +""" +mutable struct ExternalEnv + symbols::SymbolServer.EnvStore + extended_methods::Dict{SymbolServer.VarRef,Vector{SymbolServer.VarRef}} + project_deps::Vector{Symbol} +end + +abstract type State end +mutable struct Toplevel{T} <: State + file::T + included_files::Vector{String} + scope::Scope + in_modified_expr::Bool + modified_exprs::Union{Nothing,Vector{EXPR}} + delayed::Vector{EXPR} + resolveonly::Vector{EXPR} + env::ExternalEnv + server + flags::Int +end + +Toplevel(file, included_files, scope, in_modified_expr, modified_exprs, delayed, resolveonly, env, server) = + Toplevel(file, included_files, scope, in_modified_expr, modified_exprs, delayed, resolveonly, env, server, 0) + +function (state::Toplevel)(x::EXPR) + resolve_import(x, state) + mark_bindings!(x, state) + add_binding(x, state) + mark_globals(x, state) + handle_macro(x, state) + s0 = scopes(x, state) + resolve_ref(x, state) + + # DO NOT FOLLOW INCLUDE ANYMORE + # followinclude(x, state) + + old_in_modified_expr = state.in_modified_expr + if state.modified_exprs !== nothing && x in state.modified_exprs + state.in_modified_expr = true + end + if CSTParser.defines_function(x) || CSTParser.defines_macro(x) || headof(x) === :export + if state.in_modified_expr + push!(state.delayed, x) + else + push!(state.resolveonly, x) + end + else + old = flag!(state, x) + traverse(x, state) + state.flags = old + end + + state.in_modified_expr = old_in_modified_expr + state.scope != s0 && (state.scope = s0) + return state.scope +end + +mutable struct Delayed <: State + scope::Scope + env::ExternalEnv + server + flags::Int +end + +Delayed(scope, env, server) = Delayed(scope, env, server, 0) + +function (state::Delayed)(x::EXPR) + mark_bindings!(x, state) + add_binding(x, state) + mark_globals(x, state) + handle_macro(x, state) + s0 = scopes(x, state) + resolve_ref(x, state) + + old = flag!(state, x) + traverse(x, state) + state.flags = old + if state.scope != s0 + for b in values(state.scope.names) + infer_type_by_use(b, state.env) + check_unused_binding(b, state.scope) + end + state.scope = s0 + end + return state.scope +end + +mutable struct ResolveOnly <: State + scope::Scope + env::ExternalEnv + server +end + +function (state::ResolveOnly)(x::EXPR) + if hasscope(x) + s0 = state.scope + state.scope = scopeof(x) + else + s0 = state.scope + end + resolve_ref(x, state) + + traverse(x, state) + if state.scope != s0 + state.scope = s0 + end + return state.scope +end + +# feature flags that can disable or enable functionality further down in the CST +const NO_NEW_BINDINGS = 0x1 + +function flag!(state, x::EXPR) + old = state.flags + if CSTParser.ismacrocall(x) && (valof(x.args[1]) == "@." || valof(x.args[1]) == "@__dot__") + state.flags |= NO_NEW_BINDINGS + end + return old +end + +""" + semantic_pass(file, modified_expr=nothing) + +Performs a semantic pass across a project from the entry point `file`. A first pass traverses the top-level scope after which secondary passes handle delayed scopes (e.g. functions). These secondary passes can be, optionally, very light and only seek to resovle references (e.g. link symbols to bindings). This can be done by supplying a list of expressions on which the full secondary pass should be made (`modified_expr`), all others will receive the light-touch version. +""" +function semantic_pass(file, modified_expr = nothing) + server = file.server + env = getenv(file, server) + setscope!(getcst(file), Scope(nothing, getcst(file), Dict(), Dict{Symbol,Any}(:Base => env.symbols[:Base], :Core => env.symbols[:Core]), nothing)) + state = Toplevel(file, [getpath(file)], scopeof(getcst(file)), modified_expr === nothing, modified_expr, EXPR[], EXPR[], env, server) + state(getcst(file)) + for x in state.delayed + if hasscope(x) + traverse(x, Delayed(scopeof(x), env, server)) + for (k, b) in scopeof(x).names + infer_type_by_use(b, env) + check_unused_binding(b, scopeof(x)) + end + else + traverse(x, Delayed(retrieve_delayed_scope(x), env, server)) + end + end + if state.resolveonly !== nothing + for x in state.resolveonly + if hasscope(x) + traverse(x, ResolveOnly(scopeof(x), env, server)) + else + traverse(x, ResolveOnly(retrieve_delayed_scope(x), env, server)) + end + end + end +end + +""" + traverse(x, state) + +Iterates across the child nodes of an EXPR in execution order (rather than +storage order) calling `state` on each node. +""" +function traverse(x::EXPR, state) + if (isassignment(x) && !(CSTParser.is_func_call(x.args[1]) || CSTParser.iscurly(x.args[1]))) || CSTParser.isdeclaration(x) + state(x.args[2]) + state(x.args[1]) + elseif CSTParser.iswhere(x) + for i = 2:length(x.args) + state(x.args[i]) + end + state(x.args[1]) + elseif headof(x) === :generator || headof(x) === :filter + @inbounds for i = 2:length(x.args) + state(x.args[i]) + end + state(x.args[1]) + elseif headof(x) === :call && length(x.args) > 1 && headof(x.args[2]) === :parameters + state(x.args[1]) + @inbounds for i = 3:length(x.args) + state(x.args[i]) + end + state(x.args[2]) + elseif x.args !== nothing && length(x.args) > 0 + @inbounds for i = 1:length(x.args) + state(x.args[i]) + end + end +end + +function check_filesize(x, path) + nb = try + filesize(path) + catch + seterror!(x, FileNotAvailable) + return false + end + + toobig = nb > LARGE_FILE_LIMIT + if toobig + seterror!(x, FileTooBig) + end + return !toobig +end + +""" + followinclude(x, state) + +Checks whether the arguments of a call to `include` can be resolved to a path. +If successful it checks whether a file with that path is loaded on the server +or a file exists on the disc that can be loaded. +If this is successful it traverses the code associated with the loaded file. +""" +function followinclude(x, state::State) + # this runs on the `include` symbol instead of a function call so that we + # can be sure the ref has already been resolved + isinclude = isincludet = false + p = x + if isidentifier(x) && hasref(x) + r = x.meta.ref + + if is_in_fexpr(x, iscall) + p = get_parent_fexpr(x, iscall) + if r == refof_call_func(p) + isinclude = r.name == SymbolServer.VarRef(SymbolServer.VarRef(nothing, :Base), :include) + isincludet = r.name == SymbolServer.VarRef(SymbolServer.VarRef(nothing, :Revise), :includet) + end + end + end + + if !(isinclude || isincludet) + return + end + + x = p + + init_path = path = get_path(x, state) + if isempty(path) + elseif isabspath(path) + if hasfile(state.server, path) + elseif canloadfile(state.server, path) + if check_filesize(x, path) + loadfile(state.server, path) + else + return + end + else + path = "" + end + elseif !isempty(getpath(state.file)) && isabspath(joinpath(dirname(getpath(state.file)), path)) + # Relative path from current + if hasfile(state.server, joinpath(dirname(getpath(state.file)), path)) + path = joinpath(dirname(getpath(state.file)), path) + elseif canloadfile(state.server, joinpath(dirname(getpath(state.file)), path)) + path = joinpath(dirname(getpath(state.file)), path) + if check_filesize(x, path) + loadfile(state.server, path) + else + return + end + else + path = "" + end + elseif !isempty((basepath = _is_in_basedir(getpath(state.file)); basepath)) + # Special handling for include method used within Base + path = joinpath(basepath, path) + if hasfile(state.server, path) + # skip + elseif canloadfile(state.server, path) + loadfile(state.server, path) + else + path = "" + end + else + path = "" + end + if hasfile(state.server, path) + if path in state.included_files + seterror!(x, IncludeLoop) + return + end + f = getfile(state.server, path) + + if f.cst.fullspan > LARGE_FILE_LIMIT + seterror!(x, FileTooBig) + return + end + oldfile = state.file + state.file = f + push!(state.included_files, getpath(state.file)) + setroot(state.file, getroot(oldfile)) + setscope!(getcst(state.file), nothing) + state(getcst(state.file)) + state.file = oldfile + pop!(state.included_files) + elseif !is_in_fexpr(x, CSTParser.defines_function) && !isempty(init_path) + seterror!(x, MissingFile) + end +end + +""" + get_path(x::EXPR) + +Usually called on the argument to `include` calls, and attempts to determine +the path of the file to be included. Has limited support for `joinpath` calls. +""" +function get_path(x::EXPR, state) + if CSTParser.iscall(x) && length(x.args) == 2 + parg = x.args[2] + + if CSTParser.isstringliteral(parg) + if occursin("\0", valof(parg)) + seterror!(parg, IncludePathContainsNULL) + return "" + end + path = CSTParser.str_value(parg) + path = normpath(path) + Base.containsnul(path) && throw(SLInvalidPath("Couldn't convert '$x' into a valid path. Got '$path'")) + return path + elseif CSTParser.ismacrocall(parg) && valof(parg.args[1]) == "@raw_str" && CSTParser.isstringliteral(parg.args[3]) + if occursin("\0", valof(parg.args[3])) + seterror!(parg.args[3], IncludePathContainsNULL) + return "" + end + path = normpath(CSTParser.str_value(parg.args[3])) + Base.containsnul(path) && throw(SLInvalidPath("Couldn't convert '$x' into a valid path. Got '$path'")) + return path + elseif CSTParser.iscall(parg) && isidentifier(parg.args[1]) && valofid(parg.args[1]) == "joinpath" + path_elements = String[] + + for i = 2:length(parg.args) + arg = parg[i] + if _is_macrocall_to_BaseDIR(arg) # Assumes @__DIR__ points to Base macro. + push!(path_elements, dirname(getpath(state.file))) + elseif CSTParser.isstringliteral(arg) + if occursin("\0", valof(arg)) + seterror!(arg, IncludePathContainsNULL) + return "" + end + push!(path_elements, string(valof(arg))) + else + return "" + end + end + isempty(path_elements) && return "" + + path = normpath(joinpath(path_elements...)) + Base.containsnul(path) && throw(SLInvalidPath("Couldn't convert '$x' into a valid path. Got '$path'")) + return path + end + end + return "" +end + +include("server.jl") +include("imports.jl") +include("references.jl") +include("macros.jl") +include("linting/checks.jl") +include("type_inf.jl") +include("utils.jl") +include("interface.jl") + +export MarkdownFormat, PlainFormat +export run_lint +end diff --git a/src/linting/checks.jl b/src/linting/checks.jl index db2e0ef..da6fd06 100644 --- a/src/linting/checks.jl +++ b/src/linting/checks.jl @@ -136,44 +136,44 @@ function fetch_value(x::EXPR, tag::Symbol) end end -function check_all(x::EXPR, markers::Dict{Symbol,String}=Dict{Symbol,String}()) - # Setting up the markers - if headof(x) === :const - markers[:const] = fetch_value(x, :IDENTIFIER) - end - - if headof(x) === :function - markers[:function] = fetch_value(x, :IDENTIFIER) - end - - if headof(x) === :macrocall - id = fetch_value(x, :IDENTIFIER) - if !isnothing(id) - markers[:macrocall] = id - end - end - - for T in all_extended_rule_types[] - check_with_process(T, x, markers) - if haserror(x) && x.meta.error isa LintRuleReport - lint_rule_report = x.meta.error - if haskey(markers, :filename) - lint_rule_report.file = markers[:filename] - end - end - end - - if x.args !== nothing - for i in 1:length(x.args) - check_all(x.args[i], markers) - end - end - - # Do some cleaning - headof(x) === :const && delete!(markers, :const) - headof(x) === :function && delete!(markers, :function) - headof(x) === :macrocall && delete!(markers, :macrocall) -end +# function check_all(x::EXPR, markers::Dict{Symbol,String}=Dict{Symbol,String}()) +# # Setting up the markers +# if headof(x) === :const +# markers[:const] = fetch_value(x, :IDENTIFIER) +# end + +# if headof(x) === :function +# markers[:function] = fetch_value(x, :IDENTIFIER) +# end + +# if headof(x) === :macrocall +# id = fetch_value(x, :IDENTIFIER) +# if !isnothing(id) +# markers[:macrocall] = id +# end +# end + +# for T in all_extended_rule_types[] +# check_with_process(T, x, markers) +# if haserror(x) && x.meta.error isa LintRuleReport +# lint_rule_report = x.meta.error +# if haskey(markers, :filename) +# lint_rule_report.file = markers[:filename] +# end +# end +# end + +# if x.args !== nothing +# for i in 1:length(x.args) +# check_all(x.args[i], markers) +# end +# end + +# # Do some cleaning +# headof(x) === :const && delete!(markers, :const) +# headof(x) === :function && delete!(markers, :function) +# headof(x) === :macrocall && delete!(markers, :macrocall) +# end function _typeof(x, state) if x isa EXPR diff --git a/src/linting/extended_checks.jl b/src/linting/extended_checks.jl index b2015a2..f15cd3f 100644 --- a/src/linting/extended_checks.jl +++ b/src/linting/extended_checks.jl @@ -19,6 +19,89 @@ ################################################################################# # UTILITY FUNCTIONS ################################################################################# +headof(x::EXPR) = x.head +valof(x::EXPR) = x.val +# kindof(t::Tokens.AbstractToken) = t.kind +parentof(x::EXPR) = x.parent +errorof(x::EXPR) = errorof(x.meta) +errorof(x) = x +haserror(m::LintMeta) = m.error !== nothing +haserror(x::EXPR) = hasmeta(x) && haserror(x.meta) +hasmeta(x::EXPR) = x.meta isa LintMeta + +function seterror!(x::EXPR, e) + if !hasmeta(x) + x.meta = LintMeta() + end + x.meta.error = e +end + +function fetch_value(x::EXPR, tag::Symbol) + if headof(x) == tag + return x.val + else + isnothing(x.args) && return nothing + for i in 1:length(x.args) + r = fetch_value(x.args[i], tag) + isnothing(r) || return r + end + return nothing + end +end +function collect_lint_report(x::EXPR, isquoted=false, errs=Tuple{Int,EXPR}[], pos=0) + if haserror(x) + push!(errs, (pos, x)) + end + + for i in 1:length(x) + collect_lint_report(x[i], isquoted, errs, pos) + pos += x[i].fullspan + end + + errs +end +function check_all(x::EXPR, markers::Dict{Symbol,String}=Dict{Symbol,String}()) + # Setting up the markers + if headof(x) === :const + markers[:const] = fetch_value(x, :IDENTIFIER) + end + + if headof(x) === :function + isdefined(Main, :Infiltrator) && Main.infiltrate(@__MODULE__, Base.@locals, @__FILE__, @__LINE__) + markers[:function] = fetch_value(x, :IDENTIFIER) + end + + if headof(x) === :macrocall + id = fetch_value(x, :IDENTIFIER) + if !isnothing(id) + markers[:macrocall] = id + end + end + + for T in all_extended_rule_types[] + check_with_process(T, x, markers) + # isdefined(Main, :Infiltrator) && Main.infiltrate(@__MODULE__, Base.@locals, @__FILE__, @__LINE__) + if haserror(x) && x.meta.error isa LintRuleReport + lint_rule_report = x.meta.error + if haskey(markers, :filename) + lint_rule_report.file = markers[:filename] + end + end + end + + if x.args !== nothing + for i in 1:length(x.args) + check_all(x.args[i], markers) + end + end + + # Do some cleaning + headof(x) === :const && delete!(markers, :const) + headof(x) === :function && delete!(markers, :function) + headof(x) === :macrocall && delete!(markers, :macrocall) +end + + function is_named_hole_variable(x::CSTParser.EXPR) return x.head == :IDENTIFIER && startswith(x.val, "hole_variable") && diff --git a/test/runtests.jl b/test/runtests.jl index 45f36a7..e65a1f7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,6 @@ using StaticLint, SymbolServer using CSTParser, Test -using StaticLint: convert_offset_to_line_from_lines, scopeof, bindingof, refof, errorof, check_all, getenv +using StaticLint: convert_offset_to_line_from_lines, check_all include(joinpath(@__DIR__, "rai_rules_tests.jl")) From dfd6dc4c689e5def71a84137b07aab8e78eef82f Mon Sep 17 00:00:00 2001 From: Alexandre Bergel Date: Wed, 23 Oct 2024 12:27:17 +0200 Subject: [PATCH 04/12] Cleaning cleaning... --- src/StaticLint2.jl | 409 ----------- src/bindings.jl | 405 ----------- src/coretypes.jl | 47 -- src/exception_types.jl | 7 - src/imports.jl | 175 ----- src/linting/checks.jl | 1181 -------------------------------- src/linting/extended_checks.jl | 1 - src/macros.jl | 268 -------- src/methodmatching.jl | 236 ------- src/references.jl | 300 -------- src/scope.jl | 157 ----- src/server.jl | 101 --- src/subtypes.jl | 70 -- src/type_inf.jl | 323 --------- src/utils.jl | 337 --------- 15 files changed, 4017 deletions(-) delete mode 100644 src/StaticLint2.jl delete mode 100644 src/bindings.jl delete mode 100644 src/coretypes.jl delete mode 100644 src/exception_types.jl delete mode 100644 src/imports.jl delete mode 100644 src/linting/checks.jl delete mode 100644 src/macros.jl delete mode 100644 src/methodmatching.jl delete mode 100644 src/references.jl delete mode 100644 src/scope.jl delete mode 100644 src/server.jl delete mode 100644 src/subtypes.jl delete mode 100644 src/type_inf.jl delete mode 100644 src/utils.jl diff --git a/src/StaticLint2.jl b/src/StaticLint2.jl deleted file mode 100644 index de43620..0000000 --- a/src/StaticLint2.jl +++ /dev/null @@ -1,409 +0,0 @@ -module StaticLint2 - -include("exception_types.jl") - -using SymbolServer, CSTParser - -using CSTParser: EXPR, isidentifier, setparent!, valof, headof, hastrivia, parentof, isoperator, ispunctuation, to_codeobject -# CST utils -using CSTParser: is_getfield, isassignment, isdeclaration, isbracketed, iskwarg, iscall, iscurly, isunarycall, isunarysyntax, isbinarycall, isbinarysyntax, issplat, defines_function, is_getfield_w_quotenode, iswhere, iskeyword, isstringliteral, isparameters, isnonstdid, istuple -using SymbolServer: VarRef - -const noname = EXPR(:noname, nothing, nothing, 0, 0, nothing, nothing, nothing) - -include("coretypes.jl") -include("bindings.jl") -include("scope.jl") -include("subtypes.jl") -include("methodmatching.jl") - -const LARGE_FILE_LIMIT = 2_000_000 # bytes - -mutable struct Meta - binding::Union{Nothing,Binding} - scope::Union{Nothing,Scope} - ref::Union{Nothing,Binding,SymbolServer.SymStore} - error -end -Meta() = Meta(nothing, nothing, nothing, nothing) - -function Base.show(io::IO, m::Meta) - m.binding !== nothing && show(io, m.binding) - m.ref !== nothing && printstyled(io, " * ", color = :red) - m.scope !== nothing && printstyled(io, " new scope", color = :green) - m.error !== nothing && printstyled(io, " lint ", color = :red) -end -hasmeta(x::EXPR) = x.meta isa Meta -hasbinding(m::Meta) = m.binding isa Binding -hasref(m::Meta) = m.ref !== nothing -hasscope(m::Meta) = m.scope isa Scope -scopeof(m::Meta) = m.scope -bindingof(m::Meta) = m.binding - - -""" - ExternalEnv - -Holds a representation of an environment cached by SymbolServer. -""" -mutable struct ExternalEnv - symbols::SymbolServer.EnvStore - extended_methods::Dict{SymbolServer.VarRef,Vector{SymbolServer.VarRef}} - project_deps::Vector{Symbol} -end - -abstract type State end -mutable struct Toplevel{T} <: State - file::T - included_files::Vector{String} - scope::Scope - in_modified_expr::Bool - modified_exprs::Union{Nothing,Vector{EXPR}} - delayed::Vector{EXPR} - resolveonly::Vector{EXPR} - env::ExternalEnv - server - flags::Int -end - -Toplevel(file, included_files, scope, in_modified_expr, modified_exprs, delayed, resolveonly, env, server) = - Toplevel(file, included_files, scope, in_modified_expr, modified_exprs, delayed, resolveonly, env, server, 0) - -function (state::Toplevel)(x::EXPR) - resolve_import(x, state) - mark_bindings!(x, state) - add_binding(x, state) - mark_globals(x, state) - handle_macro(x, state) - s0 = scopes(x, state) - resolve_ref(x, state) - - # DO NOT FOLLOW INCLUDE ANYMORE - # followinclude(x, state) - - old_in_modified_expr = state.in_modified_expr - if state.modified_exprs !== nothing && x in state.modified_exprs - state.in_modified_expr = true - end - if CSTParser.defines_function(x) || CSTParser.defines_macro(x) || headof(x) === :export - if state.in_modified_expr - push!(state.delayed, x) - else - push!(state.resolveonly, x) - end - else - old = flag!(state, x) - traverse(x, state) - state.flags = old - end - - state.in_modified_expr = old_in_modified_expr - state.scope != s0 && (state.scope = s0) - return state.scope -end - -mutable struct Delayed <: State - scope::Scope - env::ExternalEnv - server - flags::Int -end - -Delayed(scope, env, server) = Delayed(scope, env, server, 0) - -function (state::Delayed)(x::EXPR) - mark_bindings!(x, state) - add_binding(x, state) - mark_globals(x, state) - handle_macro(x, state) - s0 = scopes(x, state) - resolve_ref(x, state) - - old = flag!(state, x) - traverse(x, state) - state.flags = old - if state.scope != s0 - for b in values(state.scope.names) - infer_type_by_use(b, state.env) - check_unused_binding(b, state.scope) - end - state.scope = s0 - end - return state.scope -end - -mutable struct ResolveOnly <: State - scope::Scope - env::ExternalEnv - server -end - -function (state::ResolveOnly)(x::EXPR) - if hasscope(x) - s0 = state.scope - state.scope = scopeof(x) - else - s0 = state.scope - end - resolve_ref(x, state) - - traverse(x, state) - if state.scope != s0 - state.scope = s0 - end - return state.scope -end - -# feature flags that can disable or enable functionality further down in the CST -const NO_NEW_BINDINGS = 0x1 - -function flag!(state, x::EXPR) - old = state.flags - if CSTParser.ismacrocall(x) && (valof(x.args[1]) == "@." || valof(x.args[1]) == "@__dot__") - state.flags |= NO_NEW_BINDINGS - end - return old -end - -""" - semantic_pass(file, modified_expr=nothing) - -Performs a semantic pass across a project from the entry point `file`. A first pass traverses the top-level scope after which secondary passes handle delayed scopes (e.g. functions). These secondary passes can be, optionally, very light and only seek to resovle references (e.g. link symbols to bindings). This can be done by supplying a list of expressions on which the full secondary pass should be made (`modified_expr`), all others will receive the light-touch version. -""" -function semantic_pass(file, modified_expr = nothing) - server = file.server - env = getenv(file, server) - setscope!(getcst(file), Scope(nothing, getcst(file), Dict(), Dict{Symbol,Any}(:Base => env.symbols[:Base], :Core => env.symbols[:Core]), nothing)) - state = Toplevel(file, [getpath(file)], scopeof(getcst(file)), modified_expr === nothing, modified_expr, EXPR[], EXPR[], env, server) - state(getcst(file)) - for x in state.delayed - if hasscope(x) - traverse(x, Delayed(scopeof(x), env, server)) - for (k, b) in scopeof(x).names - infer_type_by_use(b, env) - check_unused_binding(b, scopeof(x)) - end - else - traverse(x, Delayed(retrieve_delayed_scope(x), env, server)) - end - end - if state.resolveonly !== nothing - for x in state.resolveonly - if hasscope(x) - traverse(x, ResolveOnly(scopeof(x), env, server)) - else - traverse(x, ResolveOnly(retrieve_delayed_scope(x), env, server)) - end - end - end -end - -""" - traverse(x, state) - -Iterates across the child nodes of an EXPR in execution order (rather than -storage order) calling `state` on each node. -""" -function traverse(x::EXPR, state) - if (isassignment(x) && !(CSTParser.is_func_call(x.args[1]) || CSTParser.iscurly(x.args[1]))) || CSTParser.isdeclaration(x) - state(x.args[2]) - state(x.args[1]) - elseif CSTParser.iswhere(x) - for i = 2:length(x.args) - state(x.args[i]) - end - state(x.args[1]) - elseif headof(x) === :generator || headof(x) === :filter - @inbounds for i = 2:length(x.args) - state(x.args[i]) - end - state(x.args[1]) - elseif headof(x) === :call && length(x.args) > 1 && headof(x.args[2]) === :parameters - state(x.args[1]) - @inbounds for i = 3:length(x.args) - state(x.args[i]) - end - state(x.args[2]) - elseif x.args !== nothing && length(x.args) > 0 - @inbounds for i = 1:length(x.args) - state(x.args[i]) - end - end -end - -function check_filesize(x, path) - nb = try - filesize(path) - catch - seterror!(x, FileNotAvailable) - return false - end - - toobig = nb > LARGE_FILE_LIMIT - if toobig - seterror!(x, FileTooBig) - end - return !toobig -end - -""" - followinclude(x, state) - -Checks whether the arguments of a call to `include` can be resolved to a path. -If successful it checks whether a file with that path is loaded on the server -or a file exists on the disc that can be loaded. -If this is successful it traverses the code associated with the loaded file. -""" -function followinclude(x, state::State) - # this runs on the `include` symbol instead of a function call so that we - # can be sure the ref has already been resolved - isinclude = isincludet = false - p = x - if isidentifier(x) && hasref(x) - r = x.meta.ref - - if is_in_fexpr(x, iscall) - p = get_parent_fexpr(x, iscall) - if r == refof_call_func(p) - isinclude = r.name == SymbolServer.VarRef(SymbolServer.VarRef(nothing, :Base), :include) - isincludet = r.name == SymbolServer.VarRef(SymbolServer.VarRef(nothing, :Revise), :includet) - end - end - end - - if !(isinclude || isincludet) - return - end - - x = p - - init_path = path = get_path(x, state) - if isempty(path) - elseif isabspath(path) - if hasfile(state.server, path) - elseif canloadfile(state.server, path) - if check_filesize(x, path) - loadfile(state.server, path) - else - return - end - else - path = "" - end - elseif !isempty(getpath(state.file)) && isabspath(joinpath(dirname(getpath(state.file)), path)) - # Relative path from current - if hasfile(state.server, joinpath(dirname(getpath(state.file)), path)) - path = joinpath(dirname(getpath(state.file)), path) - elseif canloadfile(state.server, joinpath(dirname(getpath(state.file)), path)) - path = joinpath(dirname(getpath(state.file)), path) - if check_filesize(x, path) - loadfile(state.server, path) - else - return - end - else - path = "" - end - elseif !isempty((basepath = _is_in_basedir(getpath(state.file)); basepath)) - # Special handling for include method used within Base - path = joinpath(basepath, path) - if hasfile(state.server, path) - # skip - elseif canloadfile(state.server, path) - loadfile(state.server, path) - else - path = "" - end - else - path = "" - end - if hasfile(state.server, path) - if path in state.included_files - seterror!(x, IncludeLoop) - return - end - f = getfile(state.server, path) - - if f.cst.fullspan > LARGE_FILE_LIMIT - seterror!(x, FileTooBig) - return - end - oldfile = state.file - state.file = f - push!(state.included_files, getpath(state.file)) - setroot(state.file, getroot(oldfile)) - setscope!(getcst(state.file), nothing) - state(getcst(state.file)) - state.file = oldfile - pop!(state.included_files) - elseif !is_in_fexpr(x, CSTParser.defines_function) && !isempty(init_path) - seterror!(x, MissingFile) - end -end - -""" - get_path(x::EXPR) - -Usually called on the argument to `include` calls, and attempts to determine -the path of the file to be included. Has limited support for `joinpath` calls. -""" -function get_path(x::EXPR, state) - if CSTParser.iscall(x) && length(x.args) == 2 - parg = x.args[2] - - if CSTParser.isstringliteral(parg) - if occursin("\0", valof(parg)) - seterror!(parg, IncludePathContainsNULL) - return "" - end - path = CSTParser.str_value(parg) - path = normpath(path) - Base.containsnul(path) && throw(SLInvalidPath("Couldn't convert '$x' into a valid path. Got '$path'")) - return path - elseif CSTParser.ismacrocall(parg) && valof(parg.args[1]) == "@raw_str" && CSTParser.isstringliteral(parg.args[3]) - if occursin("\0", valof(parg.args[3])) - seterror!(parg.args[3], IncludePathContainsNULL) - return "" - end - path = normpath(CSTParser.str_value(parg.args[3])) - Base.containsnul(path) && throw(SLInvalidPath("Couldn't convert '$x' into a valid path. Got '$path'")) - return path - elseif CSTParser.iscall(parg) && isidentifier(parg.args[1]) && valofid(parg.args[1]) == "joinpath" - path_elements = String[] - - for i = 2:length(parg.args) - arg = parg[i] - if _is_macrocall_to_BaseDIR(arg) # Assumes @__DIR__ points to Base macro. - push!(path_elements, dirname(getpath(state.file))) - elseif CSTParser.isstringliteral(arg) - if occursin("\0", valof(arg)) - seterror!(arg, IncludePathContainsNULL) - return "" - end - push!(path_elements, string(valof(arg))) - else - return "" - end - end - isempty(path_elements) && return "" - - path = normpath(joinpath(path_elements...)) - Base.containsnul(path) && throw(SLInvalidPath("Couldn't convert '$x' into a valid path. Got '$path'")) - return path - end - end - return "" -end - -include("server.jl") -include("imports.jl") -include("references.jl") -include("macros.jl") -include("linting/checks.jl") -include("type_inf.jl") -include("utils.jl") -include("interface.jl") - -export MarkdownFormat, PlainFormat -export run_lint -end diff --git a/src/bindings.jl b/src/bindings.jl deleted file mode 100644 index f7a7dcc..0000000 --- a/src/bindings.jl +++ /dev/null @@ -1,405 +0,0 @@ -""" -Bindings indicate that an `EXPR` _may_ introduce a new name into the current scope/namespace. -Struct fields: -* `name`: the `EXPR` that defines the unqualifed name of the binding. -* `val`: what the binding points to, either a `Binding` (indicating ..), `EXPR` (this is generally the expression that defines the value) or `SymStore`. -* `type`: the type of the binding, either a `Binding`, `EXPR`, or `SymStore`. -* `refs`: a list containing all references that have been made to the binding. -""" -mutable struct Binding - name::EXPR - val::Union{Binding,EXPR,SymbolServer.SymStore,Nothing} - type::Union{Binding,SymbolServer.SymStore,Nothing} - refs::Vector{Any} -end -Binding(x::EXPR) = Binding(CSTParser.get_name(x), x, nothing, []) - -function Base.show(io::IO, b::Binding) - printstyled(io, " Binding(", to_codeobject(b.name), - b.type === nothing ? "" : ":: ", - b.refs isa Vector ? "($(length(b.refs)) refs))" : ")", color=:blue) -end - - -hasbinding(x::EXPR) = hasmeta(x) && hasbinding(x.meta) -bindingof(x) = nothing -bindingof(x::EXPR) = bindingof(x.meta) - - -hasref(x::EXPR) = hasmeta(x) && hasref(x.meta) -refof(x::EXPR) = hasmeta(x) ? x.meta.ref : nothing - - -function gotoobjectofref(x::EXPR) - r = refof(x) - if r isa SymbolServer.SymStore - return r - elseif r isa Binding - - end -end - - -""" - mark_bindings!(x::EXPR, state) - -Checks whether the expression `x` should introduce new names and marks them as needed. Generally this marks expressions that would introdce names to the current scope (i.e. that x sits in) but in cases marks expressions that will add names to lower scopes. This is done when it is not knowable that a child node of `x` will introduce a new name without the context of where it sits in `x` -for example the arguments of the signature of a function definition. -""" -function mark_bindings!(x::EXPR, state) - if hasbinding(x) - return - end - if !hasmeta(x) - x.meta = Meta() - end - if isassignment(x) - if CSTParser.is_func_call(x.args[1]) - name = CSTParser.get_name(x) - mark_binding!(x) - mark_sig_args!(x.args[1]) - elseif CSTParser.iscurly(x.args[1]) - mark_typealias_bindings!(x) - elseif !is_getfield(x.args[1]) && state.flags & NO_NEW_BINDINGS == 0 - mark_binding!(x.args[1], x) - end - elseif CSTParser.defines_anon_function(x) - mark_binding!(x.args[1], x) - elseif CSTParser.iswhere(x) - for i = 2:length(x.args) - mark_binding!(x.args[i]) - end - elseif headof(x) === :for - markiterbinding!(x.args[2]) - elseif headof(x) === :generator || headof(x) === :filter - for i = 2:length(x.args) - markiterbinding!(x.args[i]) - end - elseif headof(x) === :do - for i in 1:length(x.args[2].args) - mark_binding!(x.args[2].args[i]) - end - elseif headof(x) === :function || headof(x) === :macro - name = CSTParser.get_name(x) - x.meta.binding = Binding(name, x, CoreTypes.Function, []) - if isidentifier(name) && headof(x) === :macro - setref!(name, bindingof(x)) - end - mark_sig_args!(CSTParser.get_sig(x)) - elseif CSTParser.defines_module(x) - x.meta.binding = Binding(x.args[2], x, CoreTypes.Module, []) - setref!(x.args[2], bindingof(x)) - elseif headof(x) === :try && isidentifier(x.args[2]) - mark_binding!(x.args[2]) - setref!(x.args[2], bindingof(x.args[2])) - elseif CSTParser.defines_datatype(x) - name = CSTParser.get_name(x) - x.meta.binding = Binding(name, x, CoreTypes.DataType, []) - kwdef = parentof(x) isa EXPR && _points_to_Base_macro(parentof(x).args[1], Symbol("@kwdef"), state) - if isidentifier(name) - setref!(name, bindingof(x)) - end - mark_parameters(CSTParser.get_sig(x)) - if CSTParser.defines_struct(x) # mark field block - for arg in x.args[3].args - CSTParser.defines_function(arg) && continue - if kwdef && CSTParser.isassignment(arg) || arg.head === :const - arg = arg.args[1] - end - mark_binding!(arg) - end - end - elseif headof(x) === :local - for i = 1:length(x.args) - if isidentifier(x.args[i]) - mark_binding!(x.args[i]) - setref!(x.args[i], bindingof(x.args[i])) - end - end - end -end - - -function mark_binding!(x::EXPR, val=x) - if CSTParser.iskwarg(x) || (CSTParser.isdeclaration(x) && CSTParser.istuple(x.args[1])) - mark_binding!(x.args[1], x) - elseif CSTParser.istuple(x) || CSTParser.isparameters(x) - for arg in x.args - mark_binding!(arg, val) - end - elseif CSTParser.isbracketed(x) - mark_binding!(CSTParser.rem_invis(x), val) - elseif CSTParser.issplat(x) - mark_binding!(x.args[1], x) - elseif !(isunarysyntax(x) && valof(headof(x)) == "::") - if !hasmeta(x) - x.meta = Meta() - end - x.meta.binding = Binding(CSTParser.get_name(x), val, nothing, []) - end - return x -end - -function mark_parameters(sig::EXPR, params = String[]) - if CSTParser.issubtypedecl(sig) - mark_parameters(sig.args[1], params) - elseif iswhere(sig) - for i = 2:length(sig.args) - x = mark_binding!(sig.args[i]) - val = valof(bindingof(x).name) - if val isa String - push!(params, val) - end - end - mark_parameters(sig.args[1], params) - elseif CSTParser.iscurly(sig) - for i = 2:length(sig.args) - x = mark_binding!(sig.args[i]) - if bindingof(x) isa Binding && valof(bindingof(x).name) in params - # Don't mark a new binding if a parameter has already been - # introduced from a :where - x.meta.binding = nothing - end - end - end - sig -end - - -function markiterbinding!(iter::EXPR) - if CSTParser.isassignment(iter) - mark_binding!(iter.args[1], iter) - elseif CSTParser.iscall(iter) && CSTParser.isoperator(iter.args[1]) && (valof(iter.args[1]) == "in" || valof(iter.args[1]) == "∈") - mark_binding!(iter.args[2], iter) - elseif headof(iter) === :block - for i = 1:length(iter.args) - markiterbinding!(iter.args[i]) - end - end - return iter -end - -function mark_sig_args!(x::EXPR) - if CSTParser.iscall(x) || CSTParser.istuple(x) - if x.args !== nothing && length(x.args) > 0 - if CSTParser.isbracketed(x.args[1]) && length(x.args[1].args) > 0 && CSTParser.isdeclaration(x.args[1].args[1]) - mark_binding!(x.args[1].args[1]) - end - for i = (CSTParser.iscall(x) ? 2 : 1):length(x.args) - a = x.args[i] - if CSTParser.isparameters(a) - for j = 1:length(a.args) - aa = a.args[j] - mark_binding!(aa) - end - elseif CSTParser.ismacrocall(a) && CSTParser.isidentifier(a.args[1]) && valofid(a.args[1]) == "@nospecialize" && length(a.args) == 3 - mark_binding!(a.args[3]) - else - mark_binding!(a) - end - end - end - elseif CSTParser.iswhere(x) - for i in 2:length(x.args) - mark_binding!(x.args[i]) - end - mark_sig_args!(x.args[1]) - elseif CSTParser.isbracketed(x) - mark_sig_args!(x.args[1]) - elseif CSTParser.isdeclaration(x) - mark_sig_args!(x.args[1]) - elseif CSTParser.isbinarycall(x) - mark_binding!(x.args[1]) - mark_binding!(x.args[2]) - elseif CSTParser.isunarycall(x) && length(x.args) == 2 && (CSTParser.isbracketed(x.args[2]) || CSTParser.isdeclaration(x.args[2])) - mark_binding!(x.args[2]) - end -end - -function mark_typealias_bindings!(x::EXPR) - if !hasmeta(x) - x.meta = Meta() - end - x.meta.binding = Binding(CSTParser.get_name(x.args[1]), x, CoreTypes.DataType, []) - setscope!(x, Scope(x)) - for i = 2:length(x.args[1].args) - arg = x.args[1].args[i] - if isidentifier(arg) - mark_binding!(arg) - elseif CSTParser.issubtypedecl(arg) && isidentifier(arg.args[1]) - mark_binding!(arg.args[1]) - end - end - return x -end - -function is_in_funcdef(x) - if !(parentof(x) isa EXPR) - return false - elseif CSTParser.iswhere(parentof(x)) || CSTParser.isbracketed(parentof(x)) - return is_in_funcdef(parentof(x)) - elseif headof(parentof(x)) === :function || CSTParser.isassignment(parentof(x)) - return true - else - return false - end -end - -rem_wheres_subs_decls(x::EXPR) = (iswhere(x) || isdeclaration(x) || CSTParser.issubtypedecl(x)) ? rem_wheres_subs_decls(x.args[1]) : x - -function _in_func_or_struct_def(x::EXPR) - # only called in :where - # check 1st arg contains a call (or op call) - ex = rem_wheres_subs_decls(x.args[1]) - is_in_fexpr(x, CSTParser.defines_struct) || ((CSTParser.iscall(ex) || CSTParser.is_getfield(ex) || CSTParser.isunarycall(ex)) && is_in_funcdef(x)) -end - -""" - add_binding(x, state, scope=state.scope) - -Add the binding of `x` to the current scope. Special handling is required for: -* macros: to prefix the `@` -* functions: These are added to the top-level scope unless this syntax is used to define a closure within a function. If a function with the same name already exists in the scope then it is not replaced. This enables the `refs` list of the Binding of that 'root method' to hold a method table, the name of the new function will resolve to the binding of the root method (to get a list of actual methods -`[get_method(ref) for ref in binding.refs if get_method(ref) !== nothing]`). For example -```julia -[1] f() = 1 -[2] f(x) = 2 -``` -[1] is the root method and the name of [2] resolves to the binding of [1]. Functions declared with qualified names require special handling, there are comments in the source. - -Some simple type inference is run. -""" -function add_binding(x, state, scope=state.scope) - if bindingof(x) isa Binding - b = bindingof(x) - if isidentifier(b.name) - name = valofid(b.name) - elseif CSTParser.ismacroname(b.name) # must be getfield - name = string(to_codeobject(b.name)) - elseif isoperator(b.name) - name = valof(b.name) - else - return - end - # check for global marker - if isglobal(name, scope) - scope = _get_global_scope(state.scope) - end - - if CSTParser.defines_macro(x) - scope.names[string("@", name)] = b - mn = CSTParser.get_name(x) - if isidentifier(mn) - setref!(mn, b) - end - elseif defines_function(x) - # TODO: Need to do check that we're not in a closure. - tls = retrieve_toplevel_or_func_scope(scope) - tls === nothing && return @warn "top-level scope not retrieved" - if name_is_getfield(b.name) - resolve_ref(parentof(parentof(b.name)).args[1], scope, state) - lhs_ref = refof_maybe_getfield(parentof(parentof(b.name)).args[1]) - if lhs_ref isa SymbolServer.ModuleStore && haskey(lhs_ref.vals, Symbol(name)) - # Overloading - if haskey(tls.names, name) && eventually_overloads(tls.names[name], lhs_ref.vals[Symbol(name)], state) - # Though we're explicitly naming a function for overloading, it has already been imported to the toplevel scope. - if !hasref(b.name) - setref!(b.name, tls.names[name]) # Add ref to previous overload - overload_method(tls, b, VarRef(lhs_ref.name, Symbol(name))) - end - # Do nothing, get_name(x) will resolve to the root method - elseif isexportedby(name, lhs_ref) - # Name is already available - tls.names[name] = b - if !hasref(b.name) # Is this an appropriate indicator that we've not marked the overload? - push!(b.refs, maybe_lookup(lhs_ref[Symbol(name)], state)) - setref!(b.name, b) # we actually set the rhs of the qualified name to point to this binding - end - else - # Mark as overloaded so that calls to `M.f()` resolve properly. - overload_method(tls, b, VarRef(lhs_ref.name, Symbol(name))) # Add to overloaded list but not scope. - end - elseif lhs_ref isa Binding && CoreTypes.ismodule(lhs_ref.type) - if hasscope(lhs_ref.val) && haskey(scopeof(lhs_ref.val).names, name) - # Don't need to do anything, name will resolve - end - end - else - if scopehasbinding(tls, name) - - existing_binding = tls.names[name] - if existing_binding isa Binding && (existing_binding.val isa Binding || existing_binding.val isa SymbolServer.FunctionStore || existing_binding.val isa SymbolServer.DataTypeStore) - # Should possibly be a while statement - # If the .val is as above the Binding likely won't have a proper type attached - # so lets use the .val instead. - existing_binding = existing_binding.val - end - if (existing_binding isa Binding && ((CoreTypes.isfunction(existing_binding.type) || CoreTypes.isdatatype(existing_binding.type))) || existing_binding isa SymbolServer.FunctionStore || existing_binding isa SymbolServer.DataTypeStore) - # do nothing name of `x` will resolve to the root method - else - seterror!(x, CannotDefineFuncAlreadyHasValue) - end - else - scope.names[name] = b - if !hasref(b.name) - setref!(b.name, b) - end - end - if CSTParser.defines_struct(scope.expr) && parentof(scope) isa Scope - # hoist binding for inner constructor to parent scope - return add_binding(x, state, parentof(scope)) - end - end - elseif scopehasbinding(scope, name) - # TODO: some checks about rebinding of consts - check_const_decl(name, b, scope) - - scope.names[name] = b - elseif is_soft_scope(scope) && parentof(scope) isa Scope && isidentifier(b.name) && scopehasbinding(parentof(scope), valofid(b.name)) && !enforce_hard_scope(x, scope) - add_binding(x, state, scope.parent) - else - scope.names[name] = b - end - infer_type(b, scope, state) - elseif bindingof(x) isa SymbolServer.SymStore - scope.names[valofid(x)] = bindingof(x) - end -end - -function enforce_hard_scope(x::EXPR, scope) - scope.expr.head === :for && is_in_fexpr(x, x-> x == scope.expr.args[1]) -end - -name_is_getfield(x) = parentof(x) isa EXPR && parentof(parentof(x)) isa EXPR && CSTParser.is_getfield_w_quotenode(parentof(parentof(x))) - - -""" -eventually_overloads(b, x, state) - - -""" -eventually_overloads(b::Binding, ss::SymbolServer.SymStore, state) = b.val == ss || (b.refs !== nothing && length(b.refs) > 0 && first(b.refs) == ss) -eventually_overloads(b::Binding, ss::SymbolServer.VarRef, state) = eventually_overloads(b, maybe_lookup(ss, state), state) -eventually_overloads(b, ss, state) = false - -isglobal(name, scope) = false -isglobal(name::String, scope) = scope !== nothing && scopehasbinding(scope, "#globals") && name in scope.names["#globals"].refs - -function mark_globals(x::EXPR, state) - if headof(x) === :global - if !scopehasbinding(state.scope, "#globals") - state.scope.names["#globals"] = Binding(EXPR(:IDENTIFIER, EXPR[], nothing, 0, 0, "#globals", nothing, nothing), nothing, nothing, []) - end - for i = 2:length(x.args) - if isidentifier(x.args[i]) && !scopehasbinding(state.scope, valofid(x.args[i])) - push!(state.scope.names["#globals"].refs, valofid(x.args[i])) - end - end - end -end - -function name_extends_imported_method(b::Binding) - if CoreTypes.isfunction(b.type) && CSTParser.hasparent(b.name) && CSTParser.is_getfield(parentof(b.name)) - if refof_maybe_getfield(parentof(b.name)[1]) !== nothing - - end - end -end diff --git a/src/coretypes.jl b/src/coretypes.jl deleted file mode 100644 index 293f7a1..0000000 --- a/src/coretypes.jl +++ /dev/null @@ -1,47 +0,0 @@ -baremodule CoreTypes # Convenience -using ..SymbolServer -using Base: ==, @static - -const Any = SymbolServer.stdlibs[:Core][:Any] -const DataType = SymbolServer.stdlibs[:Core][:DataType] -const Function = SymbolServer.stdlibs[:Core][:Function] -const Module = SymbolServer.stdlibs[:Core][:Module] -const String = SymbolServer.stdlibs[:Core][:String] -const Char = SymbolServer.stdlibs[:Core][:Char] -const Symbol = SymbolServer.stdlibs[:Core][:Symbol] -const Bool = SymbolServer.stdlibs[:Core][:Bool] -const Int = SymbolServer.stdlibs[:Core][:Int] -const UInt8 = SymbolServer.stdlibs[:Core][:UInt8] -const UInt16 = SymbolServer.stdlibs[:Core][:UInt16] -const UInt32 = SymbolServer.stdlibs[:Core][:UInt32] -const UInt64 = SymbolServer.stdlibs[:Core][:UInt64] -const Float64 = SymbolServer.stdlibs[:Core][:Float64] -const Vararg = SymbolServer.FakeTypeName(Core.Vararg) - -iscoretype(x, name) = false -iscoretype(x::SymbolServer.VarRef, name) = x isa SymbolServer.DataTypeStore && x.name.name == name && x.name isa SymbolServer.VarRef && x.name.parent.name == :Core -iscoretype(x::SymbolServer.DataTypeStore, name) = x isa SymbolServer.DataTypeStore && x.name.name.name == name && x.name.name isa SymbolServer.VarRef && x.name.name.parent.name == :Core -isdatatype(x) = iscoretype(x, :DataType) -isfunction(x) = iscoretype(x, :Function) -ismodule(x) = iscoretype(x, :Module) -isstring(x) = iscoretype(x, :String) -ischar(x) = iscoretype(x, :Char) -issymbol(x) = iscoretype(x, :Symbol) -@static if Core.Int == Core.Int64 - isint(x) = iscoretype(x, :Int64) -else - isint(x) = iscoretype(x, :Int32) -end -isfloat(x) = iscoretype(x, :Float64) -isvector(x) = iscoretype(x, :Vector) -isarray(x) = iscoretype(x, :Array) -isva(x::SymbolServer.FakeUnionAll) = isva(x.body) -@static if Core.Vararg isa Core.Type - function isva(x) - return (x isa SymbolServer.FakeTypeName && x.name.name == :Vararg && - x.name.parent isa SymbolServer.VarRef && x.name.parent.name == :Core) - end -else - isva(x) = x isa SymbolServer.FakeTypeofVararg -end -end diff --git a/src/exception_types.jl b/src/exception_types.jl deleted file mode 100644 index 6f1df07..0000000 --- a/src/exception_types.jl +++ /dev/null @@ -1,7 +0,0 @@ -struct SLInvalidPath <: Exception - msg::AbstractString -end - -function Base.showerror(io::IO, ex::SLInvalidPath) - print(io, ex.msg) -end \ No newline at end of file diff --git a/src/imports.jl b/src/imports.jl deleted file mode 100644 index a23c92e..0000000 --- a/src/imports.jl +++ /dev/null @@ -1,175 +0,0 @@ -function resolve_import_block(x::EXPR, state::State, root, usinged, markfinal=true) - if x.head == :as - resolve_import_block(x.args[1], state, root, usinged, markfinal) - if x.args[2].meta === nothing - x.args[2].meta = Meta() - end - if hasbinding(last(x.args[1].args)) && CSTParser.isidentifier(x.args[2]) - lhsbinding = bindingof(last(x.args[1].args)) - x.args[2].meta.binding = Binding(x.args[2], lhsbinding.val, lhsbinding.type, lhsbinding.refs) - setref!(x.args[2], bindingof(x.args[2])) - last(x.args[1].args).meta.binding = nothing - end - return - end - n = length(x.args) - for i = 1:length(x.args) - arg = x.args[i] - if isoperator(arg) && valof(arg) == "." - # Leading dots. Can only be leading elements. - if root == getsymbols(state) - root = state.scope - elseif root isa Scope && parentof(root) !== nothing - root = parentof(root) - else - return - end - elseif isidentifier(arg) || (i == n && (CSTParser.ismacroname(arg) || isoperator(arg))) - root = maybe_lookup(hasref(arg) ? refof(arg) : _get_field(root, arg, state), state) - setref!(arg, root) - if i == n - markfinal && _mark_import_arg(arg, root, state, usinged) - return refof(arg) - end - else - return - end - end -end - -function resolve_import(x::EXPR, state::State, root=getsymbols(state)) - if headof(x) === :using || headof(x) === :import - usinged = headof(x) === :using - if length(x.args) > 0 && isoperator(headof(x.args[1])) && valof(headof(x.args[1])) == ":" - root = resolve_import_block(x.args[1].args[1], state, root, false, false) - for i = 2:length(x.args[1].args) - resolve_import_block(x.args[1].args[i], state, root, usinged) - end - else - for i = 1:length(x.args) - resolve_import_block(x.args[i], state, root, usinged) - end - end - end -end - -function _mark_import_arg(arg, par, state, usinged) - if par !== nothing && CSTParser.is_id_or_macroname(arg) - if par isa Binding # mark reference to binding - push!(par.refs, arg) - end - if par isa SymbolServer.VarRef - par = SymbolServer._lookup(par, getsymbols(state), true) - !(par isa SymbolServer.SymStore) && return - end - if bindingof(arg) === nothing - if !hasmeta(arg) - arg.meta = Meta() - end - arg.meta.binding = Binding(arg, par, _typeof(par, state), []) - setref!(arg, bindingof(arg)) - end - - if usinged - if par isa SymbolServer.ModuleStore - add_to_imported_modules(state.scope, Symbol(valofid(arg)), par) - elseif par isa Binding && par.val isa SymbolServer.ModuleStore - add_to_imported_modules(state.scope, Symbol(valofid(arg)), par.val) - elseif par isa Binding && par.val isa EXPR && CSTParser.defines_module(par.val) - add_to_imported_modules(state.scope, Symbol(valofid(arg)), scopeof(par.val)) - elseif par isa Binding && par.val isa Binding && par.val.val isa EXPR && CSTParser.defines_module(par.val.val) - add_to_imported_modules(state.scope, Symbol(valofid(arg)), scopeof(par.val.val)) - end - end - end -end - -function has_workspace_package(server, name) - haskey(server.workspacepackages, name) && - hasscope(getcst(server.workspacepackages[name])) && - haskey(scopeof(getcst(server.workspacepackages[name])).names, name) && - scopeof(getcst(server.workspacepackages[name])).names[name] isa Binding && - scopeof(getcst(server.workspacepackages[name])).names[name].val isa EXPR && - CSTParser.defines_module(scopeof(getcst(server.workspacepackages[name])).names[name].val) -end - -function add_to_imported_modules(scope::Scope, name::Symbol, val) - if scope.modules isa Dict - scope.modules[name] = val - else - Dict(name => val) - end -end -no_modules_above(s::Scope) = !CSTParser.defines_module(s.expr) || s.parent === nothing || no_modules_above(s.parent) -function get_named_toplevel_module(s, name) - return nothing -end -function get_named_toplevel_module(s::Scope, name::String) - if CSTParser.defines_module(s.expr) - m_name = CSTParser.get_name(s.expr) - if ((headof(m_name) === :IDENTIFIER && valof(m_name) == name) || headof(m_name) === :NONSTDIDENTIFIER && length(m_name.args) == 2 && valof(m_name.args[2]) == name) && no_modules_above(s) - return s.expr - end - end - if s.parent isa Scope - return get_named_toplevel_module(s.parent, name) - end - return nothing -end -function _get_field(par, arg, state) - arg_str_rep = CSTParser.str_value(arg) - if par isa SymbolServer.EnvStore - if (arg_scope = retrieve_scope(arg)) !== nothing && (tlm = get_named_toplevel_module(arg_scope, arg_str_rep)) !== nothing && hasbinding(tlm) - return bindingof(tlm) - # elseif has_workspace_package(state.server, arg_str_rep) - # return scopeof(getcst(state.server.workspacepackages[arg_str_rep])).names[arg_str_rep] - elseif haskey(par, Symbol(arg_str_rep)) - if isempty(state.env.project_deps) || Symbol(arg_str_rep) in state.env.project_deps - return par[Symbol(arg_str_rep)] - end - end - elseif par isa SymbolServer.ModuleStore # imported module - if Symbol(arg_str_rep) === par.name.name - return par - elseif haskey(par, Symbol(arg_str_rep)) - par = par[Symbol(arg_str_rep)] - if par isa SymbolServer.VarRef # reference to dependency - return SymbolServer._lookup(par, getsymbols(state), true) - end - return par - end - for used_module_name in par.used_modules - used_module = maybe_lookup(par[used_module_name], state) - if used_module !== nothing && isexportedby(Symbol(arg_str_rep), used_module) - return used_module[Symbol(arg_str_rep)] - end - end - elseif par isa Scope - if scopehasbinding(par, arg_str_rep) - return par.names[arg_str_rep] - elseif par.modules !== nothing - for used_module in values(par.modules) - if used_module isa SymbolServer.ModuleStore && isexportedby(Symbol(arg_str_rep), used_module) - return maybe_lookup(used_module[Symbol(arg_str_rep)], state) - elseif used_module isa Scope && scope_exports(used_module, arg_str_rep, state) - return used_module.names[arg_str_rep] - end - end - end - elseif par isa Binding - if par.val isa Binding - return _get_field(par.val, arg, state) - elseif par.val isa EXPR && CSTParser.defines_module(par.val) && scopeof(par.val) isa Scope - return _get_field(scopeof(par.val), arg, state) - elseif par.val isa EXPR && isassignment(par.val) - if hasref(par.val.args[2]) - return _get_field(refof(par.val.args[2]), arg, state) - elseif is_getfield_w_quotenode(par.val.args[2]) - return _get_field(refof_maybe_getfield(par.val.args[2]), arg, state) - end - elseif par.val isa SymbolServer.ModuleStore - return _get_field(par.val, arg, state) - end - end - return -end diff --git a/src/linting/checks.jl b/src/linting/checks.jl deleted file mode 100644 index da6fd06..0000000 --- a/src/linting/checks.jl +++ /dev/null @@ -1,1181 +0,0 @@ -import InteractiveUtils - -@enum( - LintCodes, - - MissingRef, - IncorrectCallArgs, - IncorrectIterSpec, - NothingEquality, - NothingNotEq, - ConstIfCondition, - EqInIfConditional, - PointlessOR, - PointlessAND, - UnusedBinding, - InvalidTypeDeclaration, - UnusedTypeParameter, - IncludeLoop, - MissingFile, - InvalidModuleName, - TypePiracy, - UnusedFunctionArgument, - CannotDeclareConst, - InvalidRedefofConst, - NotEqDef, - KwDefaultMismatch, - InappropriateUseOfLiteral, - ShouldBeInALoop, - TypeDeclOnGlobalVariable, - UnsupportedConstLocalVariable, - UnassignedKeywordArgument, - CannotDefineFuncAlreadyHasValue, - DuplicateFuncArgName, - IncludePathContainsNULL, - IndexFromLength, - FileTooBig, - FileNotAvailable, - ProhibitedAsyncMacro, - ProhibitedNThreads, - MissingReference, - ProhibitedFinalizer, - ProhibitedCCall, - ProhibitedPointerFromObjref, - ExtendedCode, - ) - -include("extended_checks.jl") - -const LintCodeDescriptions = Dict{LintCodes,String}( - IncorrectCallArgs => "Possible method call error.", - IncorrectIterSpec => "A loop iterator has been used that will likely error.", - NothingEquality => "Compare against `nothing` using `isnothing` or `===`.", - NothingNotEq => "Compare against `nothing` using `!isnothing` or `!==`.", - ConstIfCondition => "A boolean literal has been used as the conditional of an if statement - it will either always or never run.", - EqInIfConditional => "Unbracketed assignment in if conditional statements is not allowed, did you mean to use ==?", - PointlessOR => "The first argument of a `||` call is a boolean literal.", - PointlessAND => "The first argument of a `&&` call is a boolean literal.", - UnusedBinding => "Variable has been assigned but not used, if you want to keep this variable unused then prefix it with `_`.", - InvalidTypeDeclaration => "A non-DataType has been used in a type declaration statement.", - UnusedTypeParameter => "A DataType parameter has been specified but not used.", - IncludeLoop => "Loop detected, this file has already been included.", - MissingFile => "The included file can not be found.", - InvalidModuleName => "Module name matches that of its parent.", - TypePiracy => "An imported function has been extended without using module defined typed arguments.", - UnusedFunctionArgument => "An argument is included in a function signature but not used within its body.", - CannotDeclareConst => "Cannot declare constant; it already has a value.", - InvalidRedefofConst => "Invalid redefinition of constant.", - NotEqDef => "`!=` is defined as `const != = !(==)` and should not be overloaded. Overload `==` instead.", - KwDefaultMismatch => "The default value provided does not match the specified argument type.", - InappropriateUseOfLiteral => "You really shouldn't be using a literal value here.", - ShouldBeInALoop => "`break` or `continue` used outside loop.", - TypeDeclOnGlobalVariable => "Type declarations on global variables are not yet supported.", - UnsupportedConstLocalVariable => "Unsupported `const` declaration on local variable.", - UnassignedKeywordArgument => "Keyword argument not assigned.", - CannotDefineFuncAlreadyHasValue => "Cannot define function ; it already has a value.", - DuplicateFuncArgName => "Function argument name not unique.", - IncludePathContainsNULL => "Cannot include file, path contains NULL characters.", - IndexFromLength => "Indexing with indices obtained from `length`, `size` etc is discouraged. Use `eachindex` or `axes` instead.", - FileTooBig => "File too big, not following include.", - FileNotAvailable => "File not available.", - MissingReference => "Missing reference.", -) - -haserror(m::Meta) = m.error !== nothing -haserror(x::EXPR) = hasmeta(x) && haserror(x.meta) -function errorof(x::EXPR) - if hasmeta(x) - return x.meta.error isa LintRuleReport ? x.meta.error.msg : x.meta.error - end - return nothing -end - -function seterror!(x::EXPR, e) - if !hasmeta(x) - x.meta = Meta() - end - x.meta.error = e -end - -const default_options = (true, true, true, true, true, true, true, true, true, true, true) - -struct LintOptions - call::Bool - iter::Bool - nothingcomp::Bool - constif::Bool - lazy::Bool - datadecl::Bool - typeparam::Bool - modname::Bool - pirates::Bool - useoffuncargs::Bool - extended::Bool -end -LintOptions() = LintOptions(default_options...) -LintOptions(::Colon) = LintOptions(fill(true, length(default_options))...) - -LintOptions(options::Vararg{Union{Bool,Nothing},length(default_options)}) = - LintOptions(something.(options, default_options)...) - - -function fetch_value(x::SymbolServer.VarRef, tag::Symbol) - return x.name -end - -function fetch_value(x::EXPR, tag::Symbol) - if headof(x) == tag - return x.val - else - isnothing(x.args) && return nothing - for i in 1:length(x.args) - r = fetch_value(x.args[i], tag) - isnothing(r) || return r - end - return nothing - end -end - -# function check_all(x::EXPR, markers::Dict{Symbol,String}=Dict{Symbol,String}()) -# # Setting up the markers -# if headof(x) === :const -# markers[:const] = fetch_value(x, :IDENTIFIER) -# end - -# if headof(x) === :function -# markers[:function] = fetch_value(x, :IDENTIFIER) -# end - -# if headof(x) === :macrocall -# id = fetch_value(x, :IDENTIFIER) -# if !isnothing(id) -# markers[:macrocall] = id -# end -# end - -# for T in all_extended_rule_types[] -# check_with_process(T, x, markers) -# if haserror(x) && x.meta.error isa LintRuleReport -# lint_rule_report = x.meta.error -# if haskey(markers, :filename) -# lint_rule_report.file = markers[:filename] -# end -# end -# end - -# if x.args !== nothing -# for i in 1:length(x.args) -# check_all(x.args[i], markers) -# end -# end - -# # Do some cleaning -# headof(x) === :const && delete!(markers, :const) -# headof(x) === :function && delete!(markers, :function) -# headof(x) === :macrocall && delete!(markers, :macrocall) -# end - -function _typeof(x, state) - if x isa EXPR - if headof(x) in (:abstract, :primitive, :struct) - return CoreTypes.DataType - elseif CSTParser.defines_module(x) - return CoreTypes.Module - elseif CSTParser.defines_function(x) - return CoreTypes.Function - end - elseif x isa SymbolServer.DataTypeStore - return CoreTypes.DataType - elseif x isa SymbolServer.FunctionStore - return CoreTypes.Function - end -end - -# Call -function struct_nargs(x::EXPR) - # struct defs wrapped in macros are likely to have some arbirtary additional constructors, so lets allow anything - parentof(x) isa EXPR && CSTParser.ismacrocall(parentof(x)) && return 0, typemax(Int), Symbol[], true - minargs, maxargs, kws, kwsplat = 0, 0, Symbol[], false - args = x.args[3] - length(args.args) == 0 && return 0, typemax(Int), kws, kwsplat - inner_constructor = findfirst(a -> CSTParser.defines_function(a), args.args) - if inner_constructor !== nothing - return func_nargs(args.args[inner_constructor]) - else - minargs = maxargs = length(args.args) - end - return minargs, maxargs, kws, kwsplat -end - -function func_nargs(x::EXPR) - minargs, maxargs, kws, kwsplat = 0, 0, Symbol[], false - sig = CSTParser.rem_wheres_decls(CSTParser.get_sig(x)) - - if sig.args !== nothing - for i = 2:length(sig.args) - arg = unwrap_nospecialize(sig.args[i]) - if isparameters(arg) - for j = 1:length(arg.args) - arg1 = arg.args[j] - if iskwarg(arg1) - push!(kws, Symbol(CSTParser.str_value(CSTParser.get_arg_name(arg1.args[1])))) - elseif isidentifier(arg1) || isdeclaration(arg1) - push!(kws, Symbol(CSTParser.str_value(CSTParser.get_arg_name(arg1)))) - elseif issplat(arg1) - kwsplat = true - end - end - elseif iskwarg(arg) - if issplat(arg.args[1]) - maxargs = typemax(Int) - else - maxargs !== typemax(Int) && (maxargs += 1) - end - elseif issplat(arg) || - (isdeclaration(arg) && - ((isidentifier(arg.args[2]) && valofid(arg.args[2]) == "Vararg") || - (iscurly(arg.args[2]) && isidentifier(arg.args[2].args[1]) && valofid(arg.args[2].args[1]) == "Vararg"))) - maxargs = typemax(Int) - else - minargs += 1 - maxargs !== typemax(Int) && (maxargs += 1) - end - end - end - - return minargs, maxargs, kws, kwsplat -end - -function func_nargs(m::SymbolServer.MethodStore) - minargs, maxargs, kws, kwsplat = 0, 0, Symbol[], false - - for arg in m.sig - if CoreTypes.isva(last(arg)) - maxargs = typemax(Int) - else - minargs += 1 - maxargs !== typemax(Int) && (maxargs += 1) - end - end - for kw in m.kws - if endswith(String(kw), "...") - kwsplat = true - else - push!(kws, kw) - end - end - return minargs, maxargs, kws, kwsplat -end - -function call_nargs(x::EXPR) - minargs, maxargs, kws = 0, 0, Symbol[] - if length(x.args) > 0 - for i = 2:length(x.args) - arg = x.args[i] - if isparameters(arg) - for j = 1:length(arg.args) - arg1 = arg.args[j] - if iskwarg(arg1) - push!(kws, Symbol(CSTParser.str_value(CSTParser.get_arg_name(arg1.args[1])))) - end - end - elseif iskwarg(arg) - push!(kws, Symbol(CSTParser.str_value(CSTParser.get_arg_name(arg.args[1])))) - elseif issplat(arg) - maxargs = typemax(Int) - else - minargs += 1 - maxargs !== typemax(Int) && (maxargs += 1) - end - end - else - @info string("call_nargs: ", to_codeobject(x)) - end - - return minargs, maxargs, kws -end - -# compare_f_call(m_counts, call_counts) = true # fallback method - -function compare_f_call( - (ref_minargs, ref_maxargs, ref_kws, kwsplat), - (act_minargs, act_maxargs, act_kws), - ) - # check matching on positional arguments - if act_maxargs == typemax(Int) - act_minargs <= act_maxargs < ref_minargs && return false - else - !(ref_minargs <= act_minargs <= act_maxargs <= ref_maxargs) && return false - end - - # check matching on keyword arguments - kwsplat && return true # splatted kw in method so accept any kw in call - - # no splatted kw in method sig - length(act_kws) > length(ref_kws) && return false # call has more kws than method accepts - !all(kw in ref_kws for kw in act_kws) && return false # call supplies a kw that isn't defined in the method - - return true -end - -function is_something_with_methods(x::Binding) - (CoreTypes.isfunction(x.type) && x.val isa EXPR) || - (CoreTypes.isdatatype(x.type) && x.val isa EXPR && CSTParser.defines_struct(x.val)) || - (x.val isa SymbolServer.FunctionStore || x.val isa SymbolServer.DataTypeStore) -end -is_something_with_methods(x::T) where T <: Union{SymbolServer.FunctionStore,SymbolServer.DataTypeStore} = true -is_something_with_methods(x) = false - -function check_call(x, env::ExternalEnv) - if iscall(x) - parentof(x) isa EXPR && headof(parentof(x)) === :do && return # TODO: add number of args specified in do block. - length(x.args) == 0 && return - # find the function we're dealing with - func_ref = refof_call_func(x) - func_ref === nothing && return - - if is_something_with_methods(func_ref) && !(func_ref isa Binding && func_ref.val isa EXPR && func_ref.val.head === :macro) - # intentionally empty - if func_ref isa Binding && func_ref.val isa EXPR && isassignment(func_ref.val) && isidentifier(func_ref.val.args[1]) && isidentifier(func_ref.val.args[2]) - # if func_ref is a shadow binding (for these purposes, an assignment that just changes the name of a mehtod), redirect to the rhs of the assignment. - func_ref = refof(func_ref.val.args[2]) - end - else - return - end - call_counts = call_nargs(x) - tls = retrieve_toplevel_scope(x) - tls === nothing && return @warn "Couldn't get top-level scope." # General check, this means something has gone wrong. - func_ref === nothing && return - if !sig_match_any(func_ref, x, call_counts, tls, env) - if func_ref.name isa SymbolServer.VarRef && - !isnothing(func_ref.name.parent) && - func_ref.name.parent.name == :Base && - !isnothing(func_ref.name.name) - - func_ref.name.name in [:copy] && return - end - function_name = func_ref.name isa SymbolServer.FakeTypeName ? - fetch_value(func_ref.name.name, :IDENTIFIER) : - fetch_value(func_ref.name, :IDENTIFIER) - function_name in ["delete!", "copy", "copy!", "write", "hash", "iterate"] && return - seterror!(x, "Possible method call error: $(function_name).") - end - end -end - -function sig_match_any(func_ref::Union{SymbolServer.FunctionStore,SymbolServer.DataTypeStore}, x, call_counts, tls::Scope, env::ExternalEnv) - iterate_over_ss_methods(func_ref, tls, env, m -> compare_f_call(func_nargs(m), call_counts)) -end - -function sig_match_any(func_ref::Binding, x, call_counts, tls::Scope, env::ExternalEnv) - if func_ref.val isa SymbolServer.FunctionStore || func_ref.val isa SymbolServer.DataTypeStore - match = sig_match_any(func_ref.val, x, call_counts, tls, env) - match && return true - end - - has_at_least_one_method = func_ref.val isa EXPR && defines_function(func_ref.val) - # handle case where func_ref is typed as Function and yet has no methods - - for r in func_ref.refs - method = get_method(r) - method === nothing && continue - has_at_least_one_method = true - sig_match_any(method, x, call_counts, tls, env) && return true - end - return !has_at_least_one_method -end - -function sig_match_any(func::EXPR, x, call_counts, tls::Scope, env::ExternalEnv) - if CSTParser.defines_function(func) - m_counts = func_nargs(func) - elseif CSTParser.defines_struct(func) - m_counts = struct_nargs(func) - else - return true # We shouldn't get here - end - if compare_f_call(m_counts, call_counts) || (CSTParser.rem_where_decl(CSTParser.get_sig(func)) == x) - return true - end - return false -end - -function get_method(name::EXPR) - f = maybe_get_parent_fexpr(name, x -> CSTParser.defines_function(x) || CSTParser.defines_struct(x) || CSTParser.defines_macro(x)) - if f !== nothing && CSTParser.get_name(f) == name - return f - end -end -function get_method(x::Union{SymbolServer.FunctionStore,SymbolServer.DataTypeStore}) - x -end -get_method(x) = nothing - -isdocumented(x::EXPR) = parentof(x) isa EXPR && CSTParser.ismacrocall(parentof(x)) && headof(parentof(x).args[1]) === :globalrefdoc - -function check_loop_iter(x::EXPR, env::ExternalEnv) - if headof(x) === :for - if length(x.args) > 1 - body = x.args[2] - if headof(x.args[1]) === :block && x.args[1].args !== nothing - for arg in x.args[1].args - check_incorrect_iter_spec(arg, body, env) - end - else - check_incorrect_iter_spec(x.args[1], body, env) - end - end - elseif headof(x) === :generator - body = x.args[1] - for i = 2:length(x.args) - check_incorrect_iter_spec(x.args[i], body, env) - end - end -end - -function check_incorrect_iter_spec(x, body, env) - if x.args !== nothing && CSTParser.is_range(x) - rng = rhs_of_iterator(x) - - if headof(rng) === :FLOAT || headof(rng) === :INTEGER || (iscall(rng) && refof(rng.args[1]) === getsymbols(env)[:Base][:length]) - seterror!(x, IncorrectIterSpec) - elseif iscall(rng) && valof(rng.args[1]) == ":" && - length(rng.args) === 3 && - headof(rng.args[2]) === :INTEGER && - iscall(rng.args[3]) && - length(rng.args[3].args) > 1 && ( - refof(rng.args[3].args[1]) === getsymbols(env)[:Base][:length] || - refof(rng.args[3].args[1]) === getsymbols(env)[:Base][:size] - ) - if length(x.args) >= 1 - lhs = x.args[1] - arr = rng.args[3].args[2] - b = refof(arr) - - # 1:length(arr) indexing is ok for Vector and Array specifically - if b isa Binding && (CoreTypes.isarray(b.type) || CoreTypes.isvector(b.type)) - return - end - if !all_underscore(valof(lhs)) - if check_is_used_in_getindex(body, lhs, arr) - seterror!(x, IndexFromLength) - end - end - end - end - end -end - -function check_is_used_in_getindex(expr, lhs, arr) - if headof(expr) === :ref && expr.args !== nothing && length(expr.args) > 1 - this_arr = expr.args[1] - if hasref(this_arr) && hasref(arr) && refof(this_arr) == refof(arr) - for index_arg in expr.args[2:end] - if hasref(index_arg) && hasref(lhs) && refof(index_arg) == refof(lhs) - seterror!(expr, IndexFromLength) - return true - end - end - end - end - if expr.args !== nothing - for arg in expr.args - check_is_used_in_getindex(arg, lhs, arr) && return true - end - end - return false -end - -function check_nothing_equality(x::EXPR, env::ExternalEnv) - if isbinarycall(x) && length(x.args) == 3 - if valof(x.args[1]) == "==" && ( - (valof(x.args[2]) == "nothing" && refof(x.args[2]) === getsymbols(env)[:Core][:nothing]) || - (valof(x.args[3]) == "nothing" && refof(x.args[3]) === getsymbols(env)[:Core][:nothing]) - ) - seterror!(x.args[1], NothingEquality) - elseif valof(x.args[1]) == "!=" && ( - (valof(x.args[2]) == "nothing" && refof(x.args[2]) === getsymbols(env)[:Core][:nothing]) || - (valof(x.args[3]) == "nothing" && refof(x.args[3]) === getsymbols(env)[:Core][:nothing]) - ) - seterror!(x.args[1], NothingNotEq) - end - end -end - -function _get_top_binding(x::EXPR, name::String) - if scopeof(x) isa Scope - return _get_top_binding(scopeof(x), name) - elseif parentof(x) isa EXPR - return _get_top_binding(parentof(x), name) - else - return nothing - end -end - -function _get_top_binding(s::Scope, name::String) - if scopehasbinding(s, name) - return s.names[name] - elseif parentof(s) isa Scope - return _get_top_binding(parentof(s), name) - else - return nothing - end -end - -function _get_global_scope(s::Scope) - if !CSTParser.defines_module(s.expr) && parentof(s) isa Scope && parentof(s) != s - return _get_global_scope(parentof(s)) - else - return s - end -end - -function check_if_conds(x::EXPR) - if headof(x) === :if - cond = x.args[1] - if headof(cond) === :TRUE || headof(cond) === :FALSE - seterror!(cond, ConstIfCondition) - elseif isassignment(cond) - seterror!(cond, EqInIfConditional) - end - end -end - -function check_lazy(x::EXPR) - if isbinarysyntax(x) - if valof(headof(x)) == "||" - if headof(x.args[1]) === :TRUE || headof(x.args[1]) === :FALSE - seterror!(x, PointlessOR) - end - elseif valof(headof(x)) == "&&" - if headof(x.args[1]) === :TRUE || headof(x.args[1]) === :FALSE || headof(x.args[2]) === :TRUE || headof(x.args[2]) === :FALSE - seterror!(x, PointlessAND) - end - end - end -end - -is_never_datatype(b, env::ExternalEnv) = false -is_never_datatype(b::SymbolServer.DataTypeStore, env::ExternalEnv) = false -function is_never_datatype(b::SymbolServer.FunctionStore, env::ExternalEnv) - !(SymbolServer._lookup(b.extends, getsymbols(env)) isa SymbolServer.DataTypeStore) -end -function is_never_datatype(b::Binding, env::ExternalEnv) - if b.val isa Binding - return is_never_datatype(b.val, env) - elseif b.val isa SymbolServer.FunctionStore - return is_never_datatype(b.val, env) - elseif CoreTypes.isdatatype(b.type) - return false - elseif b.type !== nothing - return true - end - return false -end - -function check_datatype_decl(x::EXPR, env::ExternalEnv) - # Only call in function signatures? - if isdeclaration(x) && parentof(x) isa EXPR && iscall(parentof(x)) - if (dt = refof_maybe_getfield(last(x.args))) !== nothing - if is_never_datatype(dt, env) - seterror!(x, InvalidTypeDeclaration) - end - elseif CSTParser.isliteral(last(x.args)) - seterror!(x, InvalidTypeDeclaration) - end - end -end - -function check_modulename(x::EXPR) - if CSTParser.defines_module(x) && # x is a module - scopeof(x) isa Scope && parentof(scopeof(x)) isa Scope && # it has a scope and a parent scope - CSTParser.defines_module(parentof(scopeof(x)).expr) && # the parent scope is a module - valof(CSTParser.get_name(x)) == valof(CSTParser.get_name(parentof(scopeof(x)).expr)) # their names match - seterror!(CSTParser.get_name(x), InvalidModuleName) - end -end - -# Check whether function arguments are unused -function check_farg_unused(x::EXPR) - if CSTParser.defines_function(x) - sig = CSTParser.rem_wheres_decls(CSTParser.get_sig(x)) - if (headof(x) === :function && length(x.args) == 2 && x.args[2] isa EXPR && length(x.args[2].args) == 1 && CSTParser.isliteral(x.args[2].args[1])) || - (length(x.args) > 1 && headof(x.args[2]) === :block && length(x.args[2].args) == 1 && CSTParser.isliteral(x.args[2].args[1])) - return # Allow functions that return constants - end - if iscall(sig) - arg_names = Set{String}() - for i = 2:length(sig.args) - arg = sig.args[i] - if arg.head === :parameters - for arg2 in arg.args - !check_farg_unused_(arg2, arg_names) && return - end - else - !check_farg_unused_(arg, arg_names) && return - end - end - end - end -end - -function check_farg_unused_(arg, arg_names) - if !hasbinding(arg) - if iskwarg(arg) - arg = arg.args[1] - end - if is_nospecialize_call(arg) - arg = unwrap_nospecialize(arg) - end - end - if !hasbinding(arg) - return false - end - b = bindingof(arg) - - # We don't care about these - valof(b.name) isa String && all_underscore(valof(b.name)) && return false - - if b === nothing || - # no refs: - isempty(b.refs) || - # only self ref: - (length(b.refs) == 1 && first(b.refs) == b.name) || - # first usage has binding: - (length(b.refs) > 1 && b.refs[2] isa EXPR && hasbinding(b.refs[2])) - seterror!(arg, UnusedFunctionArgument) - end - - if valof(b.name) === nothing - elseif valof(b.name) in arg_names - seterror!(arg, DuplicateFuncArgName) - else - push!(arg_names, valof(b.name)) - end - true -end - -function unwrap_nospecialize(x) - is_nospecialize_call(x) || return x - x.args[3] -end - -function is_nospecialize_call(x) - CSTParser.ismacrocall(x) && - CSTParser.ismacroname(x.args[1]) && - is_nospecialize(x.args[1]) -end - -""" -collect_hints(x::EXPR, env, missingrefs = :all, isquoted = false, errs = Tuple{Int,EXPR}[], pos = 0) - -Collect hints and errors from an expression. `missingrefs` = (:none, :id, :all) determines whether unresolved -identifiers are marked, the :all option will mark identifiers used in getfield calls." -""" -function collect_hints(x::EXPR, env, missingrefs=:all, isquoted=false, errs=Tuple{Int,EXPR}[], pos=0) - if quoted(x) - isquoted = true - elseif isquoted && unquoted(x) - isquoted = false - end - if headof(x) === :errortoken - # collect parse errors - push!(errs, (pos, x)) - elseif !isquoted - if missingrefs != :none && isidentifier(x) && !hasref(x) && - !(valof(x) == "var" && parentof(x) isa EXPR && isnonstdid(parentof(x))) && - !((valof(x) == "stdcall" || valof(x) == "cdecl" || valof(x) == "fastcall" || valof(x) == "thiscall" || valof(x) == "llvmcall") && is_in_fexpr(x, x -> iscall(x) && isidentifier(x.args[1]) && valof(x.args[1]) == "ccall")) - - push!(errs, (pos, x)) - elseif haserror(x) && errorof(x) isa StaticLint.LintCodes - # collect lint hints - push!(errs, (pos, x)) - elseif haserror(x) && errorof(x) isa String - # We now have an extended error - push!(errs, (pos, x)) - end - elseif isquoted && missingrefs == :all && should_mark_missing_getfield_ref(x, env) - push!(errs, (pos, x)) - end - - for i in 1:length(x) - collect_hints(x[i], env, missingrefs, isquoted, errs, pos) - pos += x[i].fullspan - end - - errs -end -function collect_lint_report(x::EXPR, isquoted=false, errs=Tuple{Int,EXPR}[], pos=0) - if haserror(x) - push!(errs, (pos, x)) - end - - for i in 1:length(x) - collect_lint_report(x[i], isquoted, errs, pos) - pos += x[i].fullspan - end - - errs -end - -function refof_maybe_getfield(x::EXPR) - if isidentifier(x) - return refof(x) - elseif is_getfield_w_quotenode(x) - return refof(x.args[2].args[1]) - end -end - -function should_mark_missing_getfield_ref(x, env) - if isidentifier(x) && !hasref(x) && # x has no ref - parentof(x) isa EXPR && headof(parentof(x)) === :quotenode && parentof(parentof(x)) isa EXPR && is_getfield(parentof(parentof(x))) # x is the rhs of a getproperty - lhsref = refof_maybe_getfield(parentof(parentof(x)).args[1]) - hasref(x) && return false # We've resolved - if lhsref isa SymbolServer.ModuleStore || (lhsref isa Binding && lhsref.val isa SymbolServer.ModuleStore) - # a module, we should know this. - return true - elseif lhsref isa Binding - # by-use type inference runs after we've resolved references so we may not have known lhsref's type first time round, lets try and find `x` again - resolve_getfield(x, lhsref, ResolveOnly(retrieve_scope(x), env, nothing)) # FIXME: Setting `server` to nothing might be sketchy? - hasref(x) && return false # We've resolved - if lhsref.val isa Binding - lhsref = lhsref.val - end - lhsref = get_root_method(lhsref, nothing) - if lhsref isa EXPR - # Not clear what is happening here. - return false - elseif lhsref.type isa SymbolServer.DataTypeStore && !(isempty(lhsref.type.fieldnames) || isunionfaketype(lhsref.type.name) || has_getproperty_method(lhsref.type, env)) - return true - elseif lhsref.type isa Binding && lhsref.type.val isa EXPR && CSTParser.defines_struct(lhsref.type.val) && !has_getproperty_method(lhsref.type) - # We may have infered the lhs type after the semantic pass that was resolving references. Copied from `resolve_getfield(x::EXPR, parent_type::EXPR, state::State)::Bool`. - if scopehasbinding(scopeof(lhsref.type.val), valof(x)) - setref!(x, scopeof(lhsref.type.val).names[valof(x)]) - return false - end - return true - end - end - end - return false -end - -unwrap_fakeunionall(x) = x isa SymbolServer.FakeUnionAll ? unwrap_fakeunionall(x.body) : x -function has_getproperty_method(b::SymbolServer.DataTypeStore, env) - getprop_vr = SymbolServer.VarRef(SymbolServer.VarRef(nothing, :Base), :getproperty) - if haskey(getsymbolextendeds(env), getprop_vr) - for ext in getsymbolextendeds(env)[getprop_vr] - for m in SymbolServer._lookup(ext, getsymbols(env))[:getproperty].methods - t = unwrap_fakeunionall(m.sig[1][2]) - !(t isa SymbolServer.FakeUnion) && t.name == b.name.name && return true - end - end - else - for m in getsymbols(env)[:Base][:getproperty].methods - t = unwrap_fakeunionall(m.sig[1][2]) - !(t isa SymbolServer.FakeUnion) && t.name == b.name.name && return true - end - end - return false -end - -function has_getproperty_method(b::Binding) - if b.val isa Binding || b.val isa SymbolServer.DataTypeStore - return has_getproperty_method(b.val) - elseif b isa Binding && CoreTypes.isdatatype(b.type) - for ref in b.refs - if ref isa EXPR && is_type_of_call_to_getproperty(ref) - return true - end - end - end - return false -end - -function is_type_of_call_to_getproperty(x::EXPR) - function is_call_to_getproperty(x::EXPR) - if iscall(x) - func_name = x.args[1] - return (isidentifier(func_name) && valof(func_name) == "getproperty") || # getproperty() - (is_getfield_w_quotenode(func_name) && isidentifier(func_name.args[2].args[1]) && valof(func_name.args[2].args[1]) == "getproperty") # Base.getproperty() - end - return false - end - - return parentof(x) isa EXPR && parentof(parentof(x)) isa EXPR && - ((isdeclaration(parentof(x)) && x === parentof(x).args[2] && is_call_to_getproperty(parentof(parentof(x)))) || - (iscurly(parentof(x)) && x === parentof(x).args[1] && isdeclaration(parentof(parentof(x))) && parentof(parentof(parentof(x))) isa EXPR && is_call_to_getproperty(parentof(parentof(parentof(x)))))) -end - -isunionfaketype(t::SymbolServer.FakeTypeName) = t.name.name === :Union && t.name.parent isa SymbolServer.VarRef && t.name.parent.name === :Core - -function check_typeparams(x::EXPR, markers::Dict{Symbol,String}) - haskey(markers, :macrocall) && markers[:macrocall] == "@match" && return - haskey(markers, :macrocall) && markers[:macrocall] == "@matchrule" && return - - if iswhere(x) - for i in 2:length(x.args) - a = x.args[i] - if hasbinding(a) && (bindingof(a).refs === nothing || length(bindingof(a).refs) < 2) - seterror!(a, UnusedTypeParameter) - end - end - end -end - -function check_for_pirates(x::EXPR) - if CSTParser.defines_function(x) - sig = CSTParser.rem_where_decl(CSTParser.get_sig(x)) - fname = CSTParser.get_name(sig) - if fname_is_noteq(fname) - seterror!(x, NotEqDef) - elseif iscall(sig) && hasbinding(x) && overwrites_imported_function(refof(fname)) - for i = 2:length(sig.args) - if hasbinding(sig.args[i]) && bindingof(sig.args[i]).type isa Binding - return - elseif refers_to_nonimported_type(sig.args[i]) - return - end - end - seterror!(x, TypePiracy) - end - end -end - -function fname_is_noteq(x) - if x isa EXPR - if isoperator(x) && valof(x) == "!=" - return true - elseif is_getfield_w_quotenode(x) - return fname_is_noteq(x.args[2].args[1]) - end - end - return false -end - -function refers_to_nonimported_type(arg::EXPR) - arg = CSTParser.rem_wheres(arg) - if hasref(arg) && refof(arg) isa Binding - return true - elseif isunarysyntax(arg) && (valof(headof(arg)) == "::" || valof(headof(arg)) == "<:") - return refers_to_nonimported_type(arg.args[1]) - elseif isdeclaration(arg) - return refers_to_nonimported_type(arg.args[2]) - elseif iscurly(arg) - for i = 1:length(arg.args) - if refers_to_nonimported_type(arg.args[i]) - return true - end - end - return false - end - return false -end - -overwrites_imported_function(b) = false -function overwrites_imported_function(b::Binding) - if ((b.val isa SymbolServer.FunctionStore || b.val isa SymbolServer.DataTypeStore) && - (is_in_fexpr(b.name, x -> headof(x) === :import)) || (b.refs isa Vector && length(b.refs) > 0 && (first(b.refs) isa SymbolServer.FunctionStore || first(b.refs) isa SymbolServer.DataTypeStore))) - return true - end - return false -end - -# Now called from add_binding -# Should return true/false indicating whether the binding should actually be added? -function check_const_decl(name::String, b::Binding, scope) - # assumes `scopehasbinding(scope, name)` - b.val isa Binding && return check_const_decl(name, b.val, scope) - if b.val isa EXPR && (CSTParser.defines_datatype(b.val) || is_const(bind)) - seterror!(b.val, CannotDeclareConst) - else - prev = scope.names[name] - if (CoreTypes.isdatatype(prev.type) && !is_mask_binding_of_datatype(prev)) || is_const(prev) - if b.val isa EXPR && prev.val isa EXPR && !in_same_if_branch(b.val, prev.val) - return - end - if b.val isa EXPR - seterror!(b.val, InvalidRedefofConst) - else - # TODO check what's going on here - seterror!(b.name, InvalidRedefofConst) - end - end - end -end - -function is_mask_binding_of_datatype(b::Binding) - b.val isa EXPR && CSTParser.isassignment(b.val) && (rhsref = refof(b.val.args[2])) !== nothing && (rhsref isa SymbolServer.DataTypeStore || (rhsref.val isa EXPR && rhsref.val isa SymbolServer.DataTypeStore) || (rhsref.val isa EXPR && CSTParser.defines_datatype(rhsref.val))) -end - -# check whether a and b are in all the same :if blocks and in the same branches -in_same_if_branch(a::EXPR, b::EXPR) = in_same_if_branch(find_if_parents(a), find_if_parents(b)) -in_same_if_branch(a::Dict, b::EXPR) = in_same_if_branch(a, find_if_parents(b)) -function in_same_if_branch(a::Dict, b::Dict) - return length(a) == length(b) && all(k in keys(b) for k in keys(a)) && all(a[k] == b[k] for k in keys(a)) -end - -# find any parent nodes that are :if blocks and a pseudo-index of which branch -# x is in -function find_if_parents(x::EXPR, current=Int[], list=Dict{EXPR,Vector{Int}}()) - if x.head in (:block, :elseif) && parentof(x) isa EXPR && headof(parentof(x)) in (:if, :elseif) - i = 1 - while i <= length(parentof(x).args) - if parentof(x).args[i] == x - pushfirst!(current, i) - break - end - i += 1 - end - if headof(parentof(x)) == :if - list[parentof(x)] = current - current = [] - end - end - return parentof(x) isa EXPR ? find_if_parents(parentof(x), current, list) : list -end - -is_const(x) = false -is_const(b::Binding) = is_const(b.val) -is_const(x::EXPR) = is_const_expr(parentof(x)) - -is_const_expr(x) = false -is_const_expr(x::EXPR) = headof(x) === :const - - -""" - check_kw_default(x::EXPR, server) - -Check that the default value matches the type for keyword arguments. Following types are -checked: `String, Symbol, Int, Char, Bool, Float32, Float64, UInt8, UInt16, UInt32, -UInt64, UInt128`. -""" -function check_kw_default(x::EXPR, env::ExternalEnv) - if headof(x) == :kw && isdeclaration(x.args[1]) && CSTParser.isliteral(x.args[2]) && hasref(x.args[1].args[2]) - decl_T = refof(x.args[1].args[2]) - rhs = x.args[2] - rhsval = valof(rhs) - if decl_T == getsymbols(env)[:Core][:String] && !CSTParser.isstringliteral(rhs) - seterror!(rhs, KwDefaultMismatch) - elseif decl_T == getsymbols(env)[:Core][:Symbol] && headof(rhs) !== :IDENTIFIER - seterror!(rhs, KwDefaultMismatch) - elseif decl_T == getsymbols(env)[:Core][:Int] && headof(rhs) !== :INTEGER - seterror!(rhs, KwDefaultMismatch) - elseif decl_T == getsymbols(env)[:Core][Sys.WORD_SIZE == 64 ? :Int64 : :Int32] && headof(rhs) !== :INTEGER - seterror!(rhs, KwDefaultMismatch) - elseif decl_T == getsymbols(env)[:Core][:Bool] && !(headof(rhs) === :TRUE || headof(rhs) === :FALSE) - seterror!(rhs, KwDefaultMismatch) - elseif decl_T == getsymbols(env)[:Core][:Char] && headof(rhs) !== :CHAR - seterror!(rhs, KwDefaultMismatch) - elseif decl_T == getsymbols(env)[:Core][:Float64] && headof(rhs) !== :FLOAT - seterror!(rhs, KwDefaultMismatch) - elseif decl_T == getsymbols(env)[:Core][:Float32] && !(headof(rhs) === :FLOAT && occursin("f", rhsval)) - seterror!(rhs, KwDefaultMismatch) - else - for T in (UInt8, UInt16, UInt32, UInt64, UInt128) - if decl_T == getsymbols(env)[:Core][Symbol(T)] - # count the digits without prefix (=0x, 0o, 0b) and make sure it fits - # between upper and lower literal boundaries for `T` where the boundaries - # depend on the type of literal (binary, octal, hex) - n = count(x -> x != '_', rhsval) - 2 - ub = sizeof(T) - lb = ub ÷ 2 - if headof(rhs) == :BININT - 8lb < n <= 8ub || seterror!(rhs, KwDefaultMismatch) - elseif headof(rhs) == :OCTINT - 3lb < n <= 3ub || seterror!(rhs, KwDefaultMismatch) - elseif headof(rhs) == :HEXINT - 2lb < n <= 2ub || seterror!(rhs, KwDefaultMismatch) - else - seterror!(rhs, KwDefaultMismatch) - end - end - end - # signed integers of non native size can't be declared as literal - for T in (Int8, Int16, Sys.WORD_SIZE == 64 ? Int32 : Int64, Int128) - if decl_T == getsymbols(env)[:Core][Symbol(T)] - seterror!(rhs, KwDefaultMismatch) - end - end - - end - end -end - -function check_use_of_literal(x::EXPR) - if CSTParser.defines_module(x) && length(x.args) > 1 && isbadliteral(x.args[2]) - seterror!(x.args[2], InappropriateUseOfLiteral) - elseif (CSTParser.defines_abstract(x) || CSTParser.defines_primitive(x)) && isbadliteral(x.args[1]) - seterror!(x.args[1], InappropriateUseOfLiteral) - elseif CSTParser.defines_struct(x) && isbadliteral(x.args[2]) - seterror!(x.args[2], InappropriateUseOfLiteral) - elseif (isassignment(x) || iskwarg(x)) && isbadliteral(x.args[1]) - seterror!(x.args[1], InappropriateUseOfLiteral) - elseif isdeclaration(x) && isbadliteral(x.args[2]) - seterror!(x.args[2], InappropriateUseOfLiteral) - elseif isbinarycall(x, "isa") && isbadliteral(x.args[3]) - seterror!(x.args[3], InappropriateUseOfLiteral) - end -end - -isbadliteral(x::EXPR) = CSTParser.isliteral(x) && (CSTParser.isstringliteral(x) || headof(x) === :INTEGER || headof(x) === :FLOAT || headof(x) === :CHAR || headof(x) === :TRUE || headof(x) === :FALSE) - -function check_break_continue(x::EXPR) - if iskeyword(x) && (headof(x) === :CONTINUE || headof(x) === :BREAK) && !is_in_fexpr(x, x -> headof(x) in (:for, :while)) - seterror!(x, ShouldBeInALoop) - end -end - -function check_const(x::EXPR) - if headof(x) === :const - if VERSION < v"1.8.0-DEV.1500" && CSTParser.isassignment(x.args[1]) && CSTParser.isdeclaration(x.args[1].args[1]) - seterror!(x, TypeDeclOnGlobalVariable) - elseif headof(x.args[1]) === :local - seterror!(x, UnsupportedConstLocalVariable) - end - end -end - -function check_unused_binding(b::Binding, scope::Scope) - cond = headof(scope.expr) !== :struct && - headof(scope.expr) !== :tuple && - !all_underscore(valof(b.name)) && - !does_begin_with_underscore(valof(b.name)) - if cond - refs = loose_refs(b) - if (isempty(refs) || length(refs) == 1 && refs[1] == b.name) && - !is_sig_arg(b.name) && !is_overwritten_in_loop(b.name) && - !is_overwritten_subsequently(b, scope) && !is_kw_of_macrocall(b) - # seterror!(b.name, UnusedBinding) - seterror!(b.name, LintRuleReport(UnaccountedRule(), "Variable has been assigned but not used, if you want to keep this variable unused then prefix it with `_`.")) - end - end -end - -does_begin_with_underscore(s::String) = length(s) >= 1 && first(s) == '_' - -all_underscore(s) = false -all_underscore(s::String) = all(==(0x5f), codeunits(s)) - -function is_sig_arg(x) - is_in_fexpr(x, CSTParser.iscall) -end - -function is_kw_of_macrocall(b::Binding) - b.val isa EXPR && isassignment(b.val) && parentof(b.val) isa EXPR && CSTParser.ismacrocall(parentof(b.val)) -end - -function is_overwritten_in_loop(x) - # Cuts out false positives for check_unused_binding - the linear nature of our - # semantic passes mean a variable declared at the end of a loop's block but used at - # the start won't appear to be referenced. - - # Cheap version: - # is_in_fexpr(x, x -> x.head === :while || x.head === :for) - - # We really want to check whether the enclosing scope(s) of the loop has a binding - # with matching name. - # Is this too expensive? - loop = maybe_get_parent_fexpr(x, x -> x.head === :while || x.head === :for) - if loop !== nothing - s = scopeof(loop) - if s isa Scope && parentof(s) isa Scope - s2 = check_parent_scopes_for(s, valof(x)) - if s2 isa Scope - prev_binding = parentof(s2).names[valof(x)] - if prev_binding isa Binding - return true - # s = ComesBefore(prev_binding.name, s2.expr, 0) - # traverse(parentof(s2).expr, s) - # return s.result == 1 - # for r in prev_binding.refs - # if r isa EXPR && is_in_fexpr(r, x -> x === loop) - # return true - # end - # end - else - return false - end - end - else - return false - end - else - false - end - false -end - -""" - ComesBefore - -Check whether x1 comes before x2 -""" -mutable struct ComesBefore - x1::EXPR - x2::EXPR - result::Int -end - -function (state::ComesBefore)(x::EXPR) - state.result > 0 && return - if x == state.x1 - state.result = 1 - return - elseif x == state.x2 - state.result = 2 - return - end - if !hasscope(x) - traverse(x, state) - state.result > 0 && return - end -end - -""" - check_parent_scopes_for(s::Scope, name) - -Checks whether the parent scope of `s` has the name `name`. -""" -function check_parent_scopes_for(s::Scope, name) - # This returns `s` rather than the parent so that s.expr can be used in the linear - # search (e.g. `bound_before`) - if s.expr.head !== :module && parentof(s) isa Scope && haskey(parentof(s).names, name) - s - elseif s.parent isa Scope - check_parent_scopes_for(parentof(s), name) - end -end - - - -function is_overwritten_subsequently(b::Binding, scope::Scope) - valof(b.name) === nothing && return false - s = BoundAfter(b.name, valof(b.name), 0) - traverse(scope.expr, s) - return s.result == 2 -end - -""" - ComesBefore - -Check whether x1 comes before x2 -""" -mutable struct BoundAfter - x1::EXPR - name::String - result::Int -end - -function (state::BoundAfter)(x::EXPR) - state.result > 1 && return - if x == state.x1 - state.result = 1 - return - end - if scopeof(x) isa Scope && haskey(scopeof(x).names, state.name) - state.result = 2 - return - end - traverse(x, state) -end diff --git a/src/linting/extended_checks.jl b/src/linting/extended_checks.jl index f15cd3f..203b3d2 100644 --- a/src/linting/extended_checks.jl +++ b/src/linting/extended_checks.jl @@ -67,7 +67,6 @@ function check_all(x::EXPR, markers::Dict{Symbol,String}=Dict{Symbol,String}()) end if headof(x) === :function - isdefined(Main, :Infiltrator) && Main.infiltrate(@__MODULE__, Base.@locals, @__FILE__, @__LINE__) markers[:function] = fetch_value(x, :IDENTIFIER) end diff --git a/src/macros.jl b/src/macros.jl deleted file mode 100644 index b340e5f..0000000 --- a/src/macros.jl +++ /dev/null @@ -1,268 +0,0 @@ -function handle_macro(@nospecialize(x), state) end -function handle_macro(x::EXPR, state) - !CSTParser.ismacrocall(x) && return - if headof(x.args[1]) === :globalrefdoc - if length(x.args) == 4 - if isidentifier(x.args[4]) && !resolve_ref(x.args[4], state) - if state isa Toplevel - push!(state.resolveonly, x) - end - elseif CSTParser.is_func_call(x.args[4]) - sig = (x.args[4]) - if sig isa EXPR - hasscope(sig) && return # We've already done this, don't repeat - setscope!(sig, Scope(sig)) - mark_sig_args!(sig) - end - if state isa Toplevel - push!(state.resolveonly, x) - end - end - end - elseif CSTParser.ismacroname(x.args[1]) - state(x.args[1]) - if _points_to_Base_macro(x.args[1], Symbol("@deprecate"), state) && length(x.args) == 4 - if bindingof(x.args[3]) !== nothing - return - elseif CSTParser.is_func_call(x.args[3]) - # add deprecated method - # add deprecated function binding and args in new scope - mark_binding!(x.args[3], x) - mark_sig_args!(x.args[3]) - s0 = state.scope # store previous scope - state.scope = Scope(s0, x, Dict(), nothing, nothing) - setscope!(x, state.scope) # tag new scope to generating expression - state(x.args[3]) - state(x.args[4]) - state.scope = s0 - elseif isidentifier(x.args[3]) - mark_binding!(x.args[3], x) - end - elseif _points_to_Base_macro(x.args[1], Symbol("@deprecate_binding"), state) && length(x.args) == 4 && isidentifier(x.args[3]) && isidentifier(x.args[4]) - setref!(x.args[3], refof(x.args[4])) - elseif _points_to_Base_macro(x.args[1], Symbol("@eval"), state) && length(x.args) == 3 && state isa Toplevel - # Create scope around eval'ed expression. This ensures anybindings are - # correctly hoisted to the top-level scope. - setscope!(x, Scope(x)) - setparent!(scopeof(x), state.scope) - s0 = state.scope - state.scope = scopeof(x) - interpret_eval(x.args[3], state) - state.scope = s0 - elseif _points_to_Base_macro(x.args[1], Symbol("@irrational"), state) && length(x.args) == 5 - mark_binding!(x.args[3], x) - elseif _points_to_Base_macro(x.args[1], Symbol("@enum"), state) - for i = 3:length(x.args) - if bindingof(x.args[i]) !== nothing - break - end - if i == 4 && headof(x.args[4]) === :block - for j in 1:length(x.args[4].args) - mark_binding!(x.args[4].args[j], x) - end - break - end - mark_binding!(x.args[i], x) - end - elseif _points_to_Base_macro(x.args[1], Symbol("@goto"), state) - if length(x.args) == 3 && isidentifier(x.args[3]) - setref!(x.args[3], Binding(noname, nothing, nothing, EXPR[])) - end - elseif _points_to_Base_macro(x.args[1], Symbol("@label"), state) - if length(x.args) == 3 && isidentifier(x.args[3]) - mark_binding!(x.args[3]) - end - elseif _points_to_Base_macro(x.args[1], Symbol("@NamedTuple"), state) && length(x.args) > 2 && headof(x.args[3]) == :braces - for a in x.args[3].args - if CSTParser.isdeclaration(a) && isidentifier(a.args[1]) && !hasref(a.args[1]) - setref!(a.args[1], Binding(noname, nothing, nothing, EXPR[])) - end - end - elseif is_nospecialize(x.args[1]) - for i = 2:length(x.args) - if bindingof(x.args[i]) !== nothing - break - end - mark_binding!(x.args[i], x) - end - # elseif _points_to_arbitrary_macro(x.args[1], :Turing, :model, state) && length(x) == 3 && - # isassignment(x.args[3]) && - # headof(x.args[3].args[2]) === CSTParser.Begin && length(x.args[3].args[2]) == 3 && headof(x.args[3].args[2].args[2]) === :block - # for i = 1:length(x.args[3].args[2].args[2]) - # ex = x.args[3].args[2].args[2].args[i] - # if isbinarycall(ex, "~") - # mark_binding!(ex) - # end - # end - # elseif _points_to_arbitrary_macro(x.args[1], :JuMP, :variable, state) - # if length(x.args) < 3 - # return - # elseif length(x) >= 5 && ispunctuation(x[2]) - # _mark_JuMP_binding(x[5]) - # else - # _mark_JuMP_binding(x[3]) - # end - # elseif (_points_to_arbitrary_macro(x[1], :JuMP, :expression, state) || - # _points_to_arbitrary_macro(x[1], :JuMP, :NLexpression, state) || - # _points_to_arbitrary_macro(x[1], :JuMP, :constraint, state) || _points_to_arbitrary_macro(x[1], :JuMP, :NLconstraint, state)) && length(x) > 1 - # if ispunctuation(x[2]) - # if length(x) == 8 - # _mark_JuMP_binding(x[5]) - # end - # else - # if length(x) == 4 - # _mark_JuMP_binding(x[3]) - # end - # end - end - end -end - -function _rem_ref(x::EXPR) - if headof(x) === :ref && length(x.args) > 0 - return x.args[1] - end - return x -end - -is_nospecialize(x) = isidentifier(x) && valofid(x) == "@nospecialize" - -function _mark_JuMP_binding(arg) - if isidentifier(arg) || headof(arg) === :ref - mark_binding!(_rem_ref(arg)) - elseif isbinarycall(arg, "==") || isbinarycall(arg, "<=") || isbinarycall(arg, ">=") - if isidentifier(arg.args[1]) || headof(arg.args[1]) === :ref - mark_binding!(_rem_ref(arg.args[1])) - else - mark_binding!(_rem_ref(arg.args[3])) - end - elseif headof(arg) === :comparision && length(arg.args) == 5 - mark_binding!(_rem_ref(arg.args[3])) - end -end - -function _points_to_Base_macro(x::EXPR, name, state) - CSTParser.is_getfield_w_quotenode(x) && return _points_to_Base_macro(x.args[2].args[1], name, state) - haskey(getsymbols(state)[:Base], name) || return false - targetmacro = maybe_lookup(getsymbols(state)[:Base][name], state) - isidentifier(x) && Symbol(valofid(x)) == name && (ref = refof(x)) !== nothing && - (ref == targetmacro || (ref isa Binding && ref.val == targetmacro)) -end - -function _points_to_arbitrary_macro(x::EXPR, module_name, name, state) - length(x.args) == 2 && isidentifier(x.args[2]) && valof(x.args[2]) == name && haskey(getsymbols(state), Symbol(module_name)) && haskey(getsymbols(state)[Symbol(module_name)], Symbol("@", name)) && (refof(x.args[2]) == maybe_lookup(getsymbols(state)[Symbol(module_name)][Symbol("@", name)], state) || - (refof(x.args[2]) isa Binding && refof(x.args[2]).val == maybe_lookup(getsymbols(state)[Symbol(module_name)][Symbol("@", name)], state))) -end - -maybe_lookup(x, env::ExternalEnv) = x isa SymbolServer.VarRef ? SymbolServer._lookup(x, getsymbols(env), true) : x -maybe_lookup(x, state::State) = maybe_lookup(x, state.env) - -function maybe_eventually_get_id(x::EXPR) - if isidentifier(x) - return x - elseif isbracketed(x) - return maybe_eventually_get_id(x.args[1]) - end - return nothing -end - -is_eventually_interpolated(x::EXPR) = isbracketed(x) ? is_eventually_interpolated(x.args[1]) : isunarysyntax(x) && valof(headof(x)) == "\$" -isquoted(x::EXPR) = headof(x) === :quotenode && hastrivia(x) && isoperator(x.trivia[1]) && valof(x.trivia[1]) == ":" -maybeget_quotedsymbol(x::EXPR) = isquoted(x) ? maybe_eventually_get_id(x.args[1]) : nothing - -function is_loop_iterator(x::EXPR) - CSTParser.is_range(x) && - ((parentof(x) isa EXPR && headof(parentof(x)) === :for) || - (parentof(x) isa EXPR && parentof(parentof(x)) isa EXPR && headof(parentof(parentof(x))) === :for)) -end - -""" - maybe_quoted_list(x::EXPR) - -Try and get a list of quoted symbols from x. Return nothing if not possible. -""" -function maybe_quoted_list(x::EXPR) - names = EXPR[] - if headof(x) === :vect || headof(x) === :tuple - for i = 1:length(x.args) - name = maybeget_quotedsymbol(x.args[i]) - if name !== nothing - push!(names, name) - else - return nothing - end - end - return names - end -end - -""" -interpret_eval(x::EXPR, state) - -Naive attempt to interpret `x` as though it has been eval'ed. Lifts -any bindings made within the scope of `x` to the toplevel and replaces -(some) interpolated binding names with the value where possible. -""" -function interpret_eval(x::EXPR, state) - # make sure we have bindings etc - state(x) - tls = retrieve_toplevel_scope(x) - for ex in collect_expr_with_bindings(x) - b = bindingof(ex) - if isidentifier(b.name) - # The name of the binding is fixed - add_binding(ex, state, tls) - elseif isunarysyntax(b.name) && valof(headof(b.name)) == "\$" - # The name of the binding is variable, we need to work out what the - # interpolated symbol points to. - variable_name = b.name.args[1] - resolve_ref(variable_name, state.scope, state) - if (ref = refof(variable_name)) isa Binding - if isassignment(ref.val) && (rhs = maybeget_quotedsymbol(ref.val.args[2])) !== nothing - # `name = :something` - toplevel_binding = Binding(rhs, b.val, nothing, []) - settype!(toplevel_binding, b.type) - infer_type(toplevel_binding, tls, state) - if scopehasbinding(tls, valofid(toplevel_binding.name)) - tls.names[valofid(toplevel_binding.name)] = toplevel_binding # TODO: do we need to check whether this adds a method? - else - tls.names[valofid(toplevel_binding.name)] = toplevel_binding - end - elseif is_loop_iterator(ref.val) && (names = maybe_quoted_list(rhs_of_iterator(ref.val))) !== nothing - # name is of a collection of quoted symbols - for name in names - toplevel_binding = Binding(name, b.val, nothing, []) - settype!(toplevel_binding, b.type) - infer_type(toplevel_binding, tls, state) - if scopehasbinding(tls, valofid(toplevel_binding.name)) - tls.names[valofid(toplevel_binding.name)] = toplevel_binding # TODO: do we need to check whether this adds a method? - else - tls.names[valofid(toplevel_binding.name)] = toplevel_binding - end - end - end - end - end - end -end - - -function rhs_of_iterator(x::EXPR) - if isassignment(x) - x.args[2] - else - x.args[3] - end -end - -function collect_expr_with_bindings(x, bound_exprs=EXPR[]) - if hasbinding(x) - push!(bound_exprs, x) - # Assuming here that if an expression has a binding we don't want anything bound to chlid nodes. - elseif x.args !== nothing && !((CSTParser.defines_function(x) && !is_eventually_interpolated(x.args[1])) || CSTParser.defines_macro(x) || headof(x) === :export) - for a in x.args - collect_expr_with_bindings(a, bound_exprs) - end - end - return bound_exprs -end diff --git a/src/methodmatching.jl b/src/methodmatching.jl deleted file mode 100644 index 341c73b..0000000 --- a/src/methodmatching.jl +++ /dev/null @@ -1,236 +0,0 @@ -function arg_type(arg, ismethod) - if ismethod - if hasbinding(arg) - if bindingof(arg) isa Binding && bindingof(arg).type !== nothing - type = bindingof(arg).type - if type isa Binding && type.val isa SymbolServer.DataTypeStore - type = type.val - end - return type - end - end - else - if hasref(arg) - if refof(arg) isa Binding && refof(arg).type !== nothing - type = refof(arg).type - if type isa Binding && type.val isa SymbolServer.DataTypeStore - type = type.val - end - return type - end - elseif headof(arg) === :STRING - return CoreTypes.String - elseif headof(arg) === :CHAR - return CoreTypes.Char - elseif headof(arg) === :FLOAT - return CoreTypes.Float64 - elseif headof(arg) === :INT - return CoreTypes.Int - elseif headof(arg) === :HEXINT - if length(arg.val) < 5 - return CoreTypes.UInt8 - elseif length(arg.val) < 7 - return CoreTypes.UInt16 - elseif length(arg.val) < 11 - return CoreTypes.UInt32 - else - return CoreTypes.UInt64 - end - elseif headof(arg) === :TRUE || headof(arg) === :FALSE - return CoreTypes.Bool - elseif isquotedsymbol(arg) - return SymbolServer.stdlibs[:Core][:Symbol] - end - end - # VarRef(VarRef(nothing, :Core), :Any) - CoreTypes.Any -end - -isquotedsymbol(x) = x isa EXPR && x.head === :quotenode && length(x.args) == 1 && x.args[1].head === :IDENTIFIER && hastrivia(x) - -function call_arg_types(call::EXPR, ismethod) - types, kws = [], [] - call.args === nothing && return types, kws - if length(call.args) > 1 && headof(call.args[2]) === :parameters - for i = 1:length(call.args[2].args) - push!(kws, call.args[2].args[i].args[1]) - end - for i = 3:length(call.args) - push!(types, arg_type(call.args[i], ismethod)) - end - else - for i = 2:length(call.args) - push!(types, arg_type(call.args[i], ismethod)) - end - end - types, kws -end - -function method_arg_types(call::EXPR) - types, opts, kws = [], [], [] - call.args === nothing && return types, opts, kws - if length(call.args) > 1 && headof(call.args[2]) === :parameters - for i = 1:length(call.args[2].args) - push!(kws, call.args[2].args[i].args[1]) - end - for i = 3:length(call.args) - if CSTParser.iskwarg(call.args[i]) - push!(opts, arg_type(call.args[i].args[1], true)) - else - push!(types, arg_type(call.args[i], true)) - end - end - else - for i = 2:length(call.args) - if CSTParser.iskwarg(call.args[i]) - push!(opts, arg_type(call.args[i].args[1], true)) - else - push!(types, arg_type(call.args[i], true)) - end - end - end - types, opts, kws -end - -function find_methods(x::EXPR, store) - possibles = [] - if iscall(x) - length(x.args) === 0 && return possibles - func_ref = refof_call_func(x) - func_ref === nothing && return possibles - args, kws = call_arg_types(x, false) - if func_ref isa Binding && func_ref.val isa SymbolServer.FunctionStore || - func_ref isa Binding && func_ref.val isa SymbolServer.DataTypeStore - func_ref = func_ref.val - end - if func_ref isa SymbolServer.FunctionStore || func_ref isa SymbolServer.DataTypeStore - for method in func_ref.methods - if match_method(args, kws, method, store) - push!(possibles, method) - end - end - elseif func_ref isa Binding - if (CoreTypes.isfunction(func_ref.type) || CoreTypes.isdatatype(func_ref.type)) && func_ref.val isa EXPR - for method in func_ref.refs - method = get_method(method) - if method !== nothing - if method isa SymbolServer.FunctionStore - for method1 in method.methods - if match_method(args, kws, method1, store) - push!(possibles, method1) - end - end - elseif match_method(args, kws, method, store) - push!(possibles, method) - end - end - end - elseif (method = method_of_callable_datatype(func_ref)) !== nothing - if match_method(args, kws, method, store) - push!(possibles, method) - end - end - end - end - possibles -end - -function match_method(args::Vector{Any}, kws::Vector{Any}, method::SymbolServer.MethodStore, store) - !isempty(kws) && isempty(method.kws) && return false - nmargs = length(method.sig) - varargval = nothing - if nmargs > 0 && last(method.sig)[2] isa SymbolServer.FakeTypeofVararg - if length(args) == nmargs - 1 - nmargs -= 1 - # vararg can be zero length - elseif length(args) >= nmargs - # set aside the type param of the Vararg for later use - varargval = last(method.sig)[2].T - end - end - if length(args) == nmargs - for i in 1:length(args) - if varargval !== nothing && i >= nmargs - !_issubtype(args[i], varargval, store) && !_issubtype(varargval, args[i], store) && return false - else - !_issubtype(args[i], method.sig[i][2], store) && !_issubtype(method.sig[i][2], args[i], store) && return false - end - - end - return true - end - return false -end - -function match_method(args::Vector{Any}, kws::Vector{Any}, method::EXPR, store) - margs, mkws = [], [] - vararg = false - if CSTParser.defines_struct(method) - for i in 1:length(method.args[3].args) - arg = method.args[3].args[i] - if defines_function(arg) - # Hit an inner constructor so forget about the default one. - for arg in method.args[3].args - if defines_function(arg) - !match_method(args, kws, arg, store) && return false - end - end - return true - end - push!(margs, arg_type(arg, true)) - end - else - sig = CSTParser.rem_decl(CSTParser.get_sig(method)) - margs, mopts, mkws = method_arg_types(sig) - # vararg - if length(sig.args) > 0 - if CSTParser.issplat(last(sig.args)) - vararg = true - end - end - end - !isempty(kws) && isempty(mkws) && return false - - if length(margs) < length(args) - for i in 1:min(length(mopts), length(args) - length(margs)) - push!(margs, mopts[i]) - end - if vararg - for _ in 1:length(args) - length(margs) - push!(margs, CoreTypes.Any) - end - end - end - - if length(args) == length(margs) || (vararg && length(args) == length(margs) - 1) - for i in 1:length(args) - !_issubtype(args[i], margs[i], store) && !_issubtype(margs[i], args[i], store) && return false - end - return true - end - return false -end - -function refof_call_func(x) - if isidentifier(first(x.args)) && hasref(first(x.args)) - return refof(first(x.args)) - elseif is_getfield_w_quotenode(x.args[1]) && (rhs = rhs_of_getfield(x.args[1])) !== nothing && hasref(rhs) - return refof(rhs) - else - return - end -end - -function is_sig_of_method(sig::EXPR, method = maybe_get_parent_fexpr(sig, defines_function)) - method !== nothing && sig == CSTParser.get_sig(method) -end - -function method_of_callable_datatype(b::Binding) - if b.type isa Binding && b.type.type === CoreTypes.DataType - for ref in b.type.refs - if ref isa EXPR && ref.parent isa EXPR && isdeclaration(ref.parent) && is_in_fexpr(ref.parent, x -> x.parent isa EXPR && x.parent.head === :call && x == x.parent.args[1] && is_in_funcdef(x.parent)) - return get_parent_fexpr(ref, defines_function) - end - end - end -end diff --git a/src/references.jl b/src/references.jl deleted file mode 100644 index 582242b..0000000 --- a/src/references.jl +++ /dev/null @@ -1,300 +0,0 @@ -function setref!(x::EXPR, binding::Binding) - if !hasmeta(x) - x.meta = Meta() - end - x.meta.ref = binding - push!(binding.refs, x) -end - -function setref!(x::EXPR, binding) - if !hasmeta(x) - x.meta = Meta() - end - x.meta.ref = binding -end - - -# Main function to be called. Given the `state` tries to determine what `x` -# refers to. If it remains unresolved and is in a delayed evaluation scope -# (i.e. a function) it gets pushed to list (.urefs) to be resolved after we've -# run over the entire top-level scope. -function resolve_ref(x, state) - if !(parentof(x) isa EXPR && headof(parentof(x)) === :quotenode) - resolve_ref(x, state.scope, state) - end -end - - -# The first method that is tried. Searches the current scope for local bindings -# that match `x`. Steps: -# 1. Check whether we've already checked this scope (inifinite loops are -# possible when traversing nested modules.) -# 2. Check what sort of EXPR we're dealing with, separate name from EXPR that -# binds. -# 3. Look in the scope's variable list for a binding matching the name. -# 4. If 3. is unsuccessful, check whether the scope imports any modules then check them. -# 5. If no match is found within this scope check the parent scope. -# The return value is a boolean that is false if x should point to something but -# can't be resolved. - -function resolve_ref(x::EXPR, scope::Scope, state::State)::Bool - # if the current scope is a soft scope we should check the parent scope first - # before trying to resolve the ref locally - # if is_soft_scope(scope) && parentof(scope) isa Scope - # resolve_ref(x, parentof(scope), state) && return true - # end - - hasref(x) && return true - resolved = false - - if is_getfield(x) - return resolve_getfield(x, scope, state) - elseif iskwarg(x) - # Note to self: this seems wronge - Binding should be attached to entire Kw EXPR. - if isidentifier(x.args[1]) && !hasbinding(x.args[1]) - setref!(x.args[1], Binding(x.args[1], nothing, nothing, [])) - elseif isdeclaration(x.args[1]) && isidentifier(x.args[1].args[1]) && !hasbinding(x.args[1].args[1]) - if hasbinding(x.args[1]) - setref!(x.args[1].args[1], bindingof(x.args[1])) - else - setref!(x.args[1].args[1], Binding(x.args[1], nothing, nothing, [])) - end - end - return true - elseif is_special_macro_term(x) || new_within_struct(x) - setref!(x, Binding(noname, nothing, nothing, [])) - return true - end - mn = nameof_expr_to_resolve(x) - mn === nothing && return true - - if scopehasbinding(scope, mn) - setref!(x, scope.names[mn]) - resolved = true - elseif scope.modules isa Dict && length(scope.modules) > 0 - for m in values(scope.modules) - resolved = resolve_ref_from_module(x, m, state) - resolved && return true - end - end - if !resolved && !CSTParser.defines_module(scope.expr) && parentof(scope) isa Scope - return resolve_ref(x, parentof(scope), state) - end - return resolved -end - -# Searches a module store for a binding/variable that matches the reference `x1`. -function resolve_ref_from_module(x1::EXPR, m::SymbolServer.ModuleStore, state::State)::Bool - hasref(x1) && return true - - if CSTParser.ismacroname(x1) - x = x1 - if valof(x) == "@." && m.name == VarRef(nothing, :Base) - # @. gets converted to @__dot__, probably during lowering. - setref!(x, m[:Broadcast][Symbol("@__dot__")]) - return true - end - - mn = Symbol(valof(x)) - if isexportedby(mn, m) - setref!(x, maybe_lookup(m[mn], state)) - return true - end - elseif isidentifier(x1) - x = x1 - if Symbol(valof(x)) == m.name.name - setref!(x, m) - return true - elseif isexportedby(x, m) - setref!(x, maybe_lookup(m[Symbol(valof(x))], state)) - return true - end - end - return false -end - -function resolve_ref_from_module(x::EXPR, scope::Scope, state::State)::Bool - hasref(x) && return true - resolved = false - - mn = nameof_expr_to_resolve(x) - mn === nothing && return true - - if scope_exports(scope, mn, state) - setref!(x, scope.names[mn]) - resolved = true - end - return resolved -end - -""" - scope_exports(scope::Scope, name::String) - -Does the scope export a variable called `name`? -""" -function scope_exports(scope::Scope, name::String, state) - if scopehasbinding(scope, name) && (b = scope.names[name]) isa Binding - initial_pass_on_exports(scope.expr, name, state) - for ref in b.refs - if ref isa EXPR && parentof(ref) isa EXPR && headof(parentof(ref)) === :export - return true - end - end - end - return false -end - -""" - initial_pass_on_exports(x::EXPR, server) - -Export statements need to be (pseudo) evaluated each time we consider -whether a variable is made available by an import statement. -""" - -function initial_pass_on_exports(x::EXPR, name, state) - for a in x.args[3] # module block expressions - if headof(a) === :export - for i = 1:length(a.args) - if isidentifier(a.args[i]) && valof(a.args[i]) == name && !hasref(a.args[i]) - Delayed(scopeof(x), state.env, state.server)(a.args[i]) - end - end - end - end -end - -# Fallback method -function resolve_ref(x::EXPR, m, state::State)::Bool - return hasref(x)::Bool -end - -rhs_of_getfield(x::EXPR) = CSTParser.is_getfield_w_quotenode(x) ? x.args[2].args[1] : x -lhs_of_getfield(x::EXPR) = rhs_of_getfield(x.args[1]) - -""" - resolve_getfield(x::EXPR, parent::Union{EXPR,Scope,ModuleStore,Binding}, state::State)::Bool - -Given an expression of the form `parent.x` try to resolve `x`. The method -called with `parent::EXPR` resolves the reference for `parent`, other methods -then check whether the Binding/Scope/ModuleStore to which `parent` points has -a field matching `x`. -""" -function resolve_getfield(x::EXPR, scope::Scope, state::State)::Bool - hasref(x) && return true - resolved = resolve_ref(x.args[1], scope, state) - if isidentifier(x.args[1]) - lhs = x.args[1] - elseif CSTParser.is_getfield_w_quotenode(x.args[1]) - lhs = lhs_of_getfield(x) - else - return resolved - end - if resolved && (rhs = rhs_of_getfield(x)) !== nothing - resolved = resolve_getfield(rhs, refof(lhs), state) - end - return resolved -end - - -function resolve_getfield(x::EXPR, parent_type::EXPR, state::State)::Bool - hasref(x) && return true - resolved = false - if isidentifier(x) - if CSTParser.defines_module(parent_type) && scopeof(parent_type) isa Scope - resolved = resolve_ref(x, scopeof(parent_type), state) - elseif CSTParser.defines_struct(parent_type) - if scopehasbinding(scopeof(parent_type), valofid(x)) - setref!(x, scopeof(parent_type).names[valofid(x)]) - resolved = true - end - end - end - return resolved -end - - -function resolve_getfield(x::EXPR, b::Binding, state::State)::Bool - hasref(x) && return true - resolved = false - if b.val isa Binding - resolved = resolve_getfield(x, b.val, state) - elseif b.val isa SymbolServer.ModuleStore || (b.val isa EXPR && CSTParser.defines_module(b.val)) - resolved = resolve_getfield(x, b.val, state) - elseif b.type isa Binding - resolved = resolve_getfield(x, b.type.val, state) - elseif b.type isa SymbolServer.DataTypeStore - resolved = resolve_getfield(x, b.type, state) - end - return resolved -end - -function resolve_getfield(x::EXPR, parent_type, state::State)::Bool - hasref(x) -end - -function is_overloaded(val::SymbolServer.SymStore, scope::Scope) - vr = val.name isa SymbolServer.FakeTypeName ? val.name.name : val.name - haskey(scope.overloaded, vr) -end - -function resolve_getfield(x::EXPR, m::SymbolServer.ModuleStore, state::State)::Bool - hasref(x) && return true - resolved = false - if CSTParser.ismacroname(x) && (val = maybe_lookup(SymbolServer.maybe_getfield(Symbol(valofid(x)), m, getsymbols(state)), state)) !== nothing - setref!(x, val) - resolved = true - elseif isidentifier(x) && (val = maybe_lookup(SymbolServer.maybe_getfield(Symbol(valofid(x)), m, getsymbols(state)), state)) !== nothing - # Check whether variable is overloaded in top-level scope - tls = retrieve_toplevel_scope(state.scope) - # if tls.overloaded !== nothing && (vr = val.name isa SymbolServer.FakeTypeName ? val.name.name : val.name; haskey(tls.overloaded, vr)) - # @info 1 - # setref!(x, tls.overloaded[vr]) - # return true - # end - vr = val.name isa SymbolServer.FakeTypeName ? val.name.name : val.name - if haskey(tls.names, valof(x)) && tls.names[valof(x)] isa Binding && tls.names[valof(x)].val isa SymbolServer.FunctionStore - setref!(x, tls.names[valof(x)]) - return true - elseif tls.overloaded !== nothing && haskey(tls.overloaded, vr) - setref!(x, tls.overloaded[vr]) - return true - end - setref!(x, val) - resolved = true - end - return resolved -end - -function resolve_getfield(x::EXPR, parent::SymbolServer.DataTypeStore, state::State)::Bool - hasref(x) && return true - resolved = false - if isidentifier(x) && Symbol(valof(x)) in parent.fieldnames - fi = findfirst(f -> Symbol(valof(x)) == f, parent.fieldnames) - ft = parent.types[fi] - val = SymbolServer._lookup(ft, getsymbols(state), true) - # TODO: Need to handle the case where we get back a FakeUnion, etc. - setref!(x, Binding(noname, nothing, val, [])) - resolved = true - end - return resolved -end - -resolvable_macroname(x::EXPR) = isidentifier(x) && CSTParser.ismacroname(x) && refof(x) === nothing - -nameof_expr_to_resolve(x) = isidentifier(x) ? valofid(x) : nothing - -""" - valofid(x) - -Returns the string value of an expression for which `isidentifier` is true, -i.e. handles NONSTDIDENTIFIERs. -""" -valofid(x::EXPR) = headof(x) === :IDENTIFIER ? valof(x) : valof(x.args[2]) - -""" -new_within_struct(x::EXPR) - -Checks whether x is a reference to `new` within a datatype constructor. -""" -new_within_struct(x::EXPR) = isidentifier(x) && valofid(x) == "new" && is_in_fexpr(x, CSTParser.defines_struct) -is_special_macro_term(x::EXPR) = isidentifier(x) && (valofid(x) == "__source__" || valofid(x) == "__module__") && is_in_fexpr(x, CSTParser.defines_macro) diff --git a/src/scope.jl b/src/scope.jl deleted file mode 100644 index b096b30..0000000 --- a/src/scope.jl +++ /dev/null @@ -1,157 +0,0 @@ -mutable struct Scope - parent::Union{Scope,Nothing} - expr::EXPR - names::Dict{String,Binding} - modules::Union{Nothing,Dict{Symbol,Any}} - overloaded::Union{Dict,Nothing} -end -Scope(expr) = Scope(nothing, expr, Dict{Symbol,Binding}(), nothing, nothing) -function Base.show(io::IO, s::Scope) - printstyled(io, headof(s.expr)) - printstyled(io, " ", join(keys(s.names), ","), color=:yellow) - s.modules isa Dict && printstyled(io, " ", join(keys(s.modules), ","), color=:blue) -end - -function overload_method(scope::Scope, b::Binding, vr::SymbolServer.VarRef) - if scope.overloaded === nothing - scope.overloaded = Dict() - end - if haskey(scope.overloaded, vr) - # TODO: need to check this hasn't already been done - push!(scope.overloaded[vr].refs, b.val) - else - scope.overloaded[vr] = b - end -end - -""" -scopehasmodule(s::Scope, mname::Symbol)::Bool - -Checks whether the module `mname` has been `using`ed in `s`. -""" -scopehasmodule(s::Scope, mname::Symbol) = s.modules !== nothing && haskey(s.modules, mname) - -""" - addmoduletoscope!(s, m, [mname::Symbol]) - -Adds module `m` to the list of used modules in scope `s`. -""" -function addmoduletoscope!(s::Scope, m, mname::Symbol) - if s.modules === nothing - s.modules = Dict{Symbol,Any}() - end - s.modules[mname] = m -end -addmoduletoscope!(s::Scope, m::SymbolServer.ModuleStore) = addmoduletoscope!(s, m, m.name.name) -addmoduletoscope!(s::Scope, m::EXPR) = CSTParser.defines_module(m) && addmoduletoscope!(s, scopeof(m), Symbol(valof(CSTParser.get_name(m)))) -addmoduletoscope!(s::Scope, s1::Scope) = CSTParser.defines_module(s1.expr) && addmoduletoscope!(s, s1, Symbol(valof(CSTParser.get_name(s1.expr)))) - - -getscopemodule(s::Scope, m::Symbol) = s.modules[m] - -""" - scopehasbinding(s::Scope, n::String) - -Checks whether s has a binding for variable named `n`. -""" -scopehasbinding(s::Scope, n::String) = haskey(s.names, n) - -is_soft_scope(scope::Scope) = scope.expr.head == :for || scope.expr.head == :while || scope.expr.head == :try - -""" - introduces_scope(x::EXPR, state) - -Does this expression introduce a new scope? -""" -function introduces_scope(x::EXPR, state) - # TODO: remove unused 2nd argument. - if CSTParser.isassignment(x) && (CSTParser.is_func_call(x.args[1]) || CSTParser.iscurly(x.args[1])) - return true - elseif CSTParser.defines_anon_function(x) - return true - elseif CSTParser.iswhere(x) - # unless in func def signature - return !_in_func_or_struct_def(x) - elseif CSTParser.istuple(x) && CSTParser.hastrivia(x) && ispunctuation(x.trivia[1]) && length(x.args) > 0 && isassignment(x.args[1]) - # named tuple - return true - elseif headof(x) === :function || - headof(x) === :macro || - headof(x) === :for || - headof(x) === :while || - headof(x) === :let || - headof(x) === :generator || # and Flatten? - headof(x) === :try || - headof(x) === :do || - headof(x) === :module || - headof(x) === :abstract || - headof(x) === :primitive || - headof(x) === :struct - return true - end - return false -end - - -hasscope(x::EXPR) = hasmeta(x) && hasscope(x.meta) -scopeof(x) = nothing -scopeof(x::EXPR) = scopeof(x.meta) -CSTParser.parentof(s::Scope) = s.parent - -function setscope!(x::EXPR, s) - if !hasmeta(x) - x.meta = Meta() - end - x.meta.scope = s -end - -""" - scopes(x::EXPR, state) - -Called when traversing the syntax tree and handles the association of -scopes with expressions. On the first pass this will add scopes as -necessary, on following passes it empties it. -""" -function scopes(x::EXPR, state) - clear_scope(x) - if scopeof(x) === nothing && introduces_scope(x, state) - setscope!(x, Scope(x)) - end - s0 = state.scope - if headof(x) === :file - setscope!(x, state.scope) - add_eval_method(x, state) - elseif scopeof(x) isa Scope - scopeof(x) != s0 && setparent!(scopeof(x), s0) - state.scope = scopeof(x) - if headof(x) === :module && headof(x.args[1]) === :TRUE # Add default modules to a new module - state.scope.modules = Dict{Symbol,Any}() # TODO: only create new Dict if not assigned? - state.scope.modules[:Base] = getsymbols(state)[:Base] - state.scope.modules[:Core] = getsymbols(state)[:Core] - add_eval_method(x, state) - elseif headof(x) === :module && headof(x.args[1]) === :FALSE - state.scope.modules = Dict{String,Any}() - state.scope.modules[:Core] = getsymbols(state)[:Core] - add_eval_method(x, state) - end - if headof(x) === :module && bindingof(x) !== nothing # Add reference to out of scope binding (i.e. itself) - # state.scope.names[bindingof(x).name] = bindingof(x) - # TODO: move this to the binding stage - add_binding(x, state) - # elseif headof(x) === :flatten && headof(x[1]) === CSTParser.Generator && length(x[1]) > 0 && headof(x[1][1]) === CSTParser.Generator - # setscope!(x[1][1], nothing) - end - end - return s0 -end - -# Add an `eval` method -function add_eval_method(x, state) - mod = if x.head === :module - CSTParser.isidentifier(x.args[3]) ? Symbol(valof(x.args[3])) : :unknown - else - Symbol("top-level") - end - meth = SymbolServer.MethodStore(:eval, mod, "", 0, [:expr => SymbolServer.FakeTypeName(SymbolServer.VarRef(SymbolServer.VarRef(nothing, :Core), :Any), [])], [], Any) - state.scope.names["eval"] = Binding(x, SymbolServer.FunctionStore(SymbolServer.VarRef(nothing, :nothing), SymbolServer.MethodStore[meth],"", SymbolServer.VarRef(nothing, :nothing), false), getsymbols(state)[:Core][:DataType], []) -end diff --git a/src/server.jl b/src/server.jl deleted file mode 100644 index 2293aa8..0000000 --- a/src/server.jl +++ /dev/null @@ -1,101 +0,0 @@ -#= -Project trees are usually made up of multiple files. An AbstractServer holds the AbstractFiles that represent this tree. FileServer is the basic implementation and assumes files are available and readable from disc. (LanguageServer illustrates another implementaiton). The accompanying functions summarised below are required for making an alternative implementation. - -Interface spec. -AbstractServer :-> (has/canload/load/set/get)file, getsymbols, getsymbolextends -AbstractFile :-> (get/set)path, (get/set)root, (get/set)cst, semantic_pass, (get/set)server -=# -abstract type AbstractServer end -abstract type AbstractFile end - -mutable struct File - path::String - source::String - cst::EXPR - root::Union{Nothing,File} - server -end - -mutable struct FileServer <: AbstractServer - files::Dict{String,File} - roots::Set{File} - workspacepackages::Dict{String,File} # list of files that may represent within-workspace packages - external_env::ExternalEnv -end -FileServer() = FileServer(Dict{String,File}(), Set{File}(), Dict{String,File}(), ExternalEnv(Dict{Symbol,SymbolServer.ModuleStore}(:Base => SymbolServer.stdlibs[:Base], :Core => SymbolServer.stdlibs[:Core]), SymbolServer.collect_extended_methods(SymbolServer.stdlibs), Symbol[])) - - -hasfile(server::FileServer, path::String) = haskey(server.files, path) -canloadfile(server, path) = isfile(path) -function setfile(server::FileServer, path::String, file::File) - server.files[path] = file -end -getfile(server::FileServer, path::String) = server.files[path] -function loadfile(server::FileServer, path::String) - try - source = read(path, String) - cst = CSTParser.parse(source, true) - f = File(path, source, cst, nothing, server) - setroot(f, f) - setfile(server, path, f) - return getfile(server, path) - catch - @info "Could not load $(path) from disk." - rethrow() - end -end - -getsymbols(env::ExternalEnv) = env.symbols -getsymbols(state::State) = getsymbols(state.env) - -getsymbolextendeds(env::ExternalEnv) = env.extended_methods -getsymbolextendeds(state::State) = getsymbolextendeds(state.env) - - -""" - getenv(file::File, server::FileServer) - -Get the relevant `ExternalEnv` for a given file. -""" -function getenv(file::File, server::FileServer) - # For FileServer this approach is equivalent to the previous behaviour. Other AbstractServers - # (e.g. LanguageServerInstance) can use this function to associate different files (or trees of - # files) with different environments. - server.external_env -end - - -getpath(file::File) = file.path - -getroot(file::File) = file.root -function setroot(file::File, root::File) - file.root = root - return file -end - -getcst(file::File) = file.cst -function setcst(file::File, cst::EXPR) - file.cst = cst - return file -end - -getserver(file::File) = file.server -function setserver(file::File, server::FileServer) - file.server = server - return file -end - -function Base.display(f::File) - println(f.path) -end - -function Base.display(s::FileServer) - n = length(s.files) - println(n, "-file Server") - cnt = 0 - for p in keys(s.files) - cnt += 1 - println(" ", p) - cnt > 10 && break - end -end diff --git a/src/subtypes.jl b/src/subtypes.jl deleted file mode 100644 index 1c2a478..0000000 --- a/src/subtypes.jl +++ /dev/null @@ -1,70 +0,0 @@ -function _issubtype(a, b, store) - _isany(b) && return true - _type_compare(a, b) && return true - sup_a = _super(a, store) - _type_compare(sup_a, b) && return true - !_isany(sup_a) && return _issubtype(sup_a, b, store) - return false -end - -_isany(x::SymbolServer.FakeTypeName) = x.name == VarRef(VarRef(nothing, :Core), :Any) -_isany(x::SymbolServer.DataTypeStore) = x.name.name == VarRef(VarRef(nothing, :Core), :Any) -_isany(x) = false - -_type_compare(a::SymbolServer.DataTypeStore, b::SymbolServer.DataTypeStore) = a.name == b.name -_type_compare(a::SymbolServer.FakeTypeName, b::SymbolServer.FakeTypeName) = a == b -_type_compare(a::SymbolServer.FakeTypeName, b::SymbolServer.DataTypeStore) = a == b.name -_type_compare(a::SymbolServer.DataTypeStore, b::SymbolServer.FakeTypeName) = a.name == b -_type_compare(a::SymbolServer.DataTypeStore, b::SymbolServer.FakeUnion) = _type_compare(a, b.a) || -_type_compare(a, b.b) - -function _type_compare(a::SymbolServer.DataTypeStore, b::SymbolServer.FakeTypeVar) - if b.ub isa SymbolServer.FakeUnion - return _type_compare(a, b.ub) - end - a == b -end - -_type_compare(a, b) = a == b - -_super(a::SymbolServer.DataTypeStore, store) = SymbolServer._lookup(a.super.name, store) -_super(a::SymbolServer.FakeTypeVar, store) = a.ub -_super(a::SymbolServer.FakeUnionAll, store) = a.body -_super(a::SymbolServer.FakeTypeName, store) = _super(SymbolServer._lookup(a.name, store), store) -@static if !(Vararg isa Type) - _super(a::SymbolServer.FakeTypeofVararg, store) = CoreTypes.Any -end - -function _super(b::Binding, store) - StaticLint.CoreTypes.isdatatype(b.type) || error() - b.val isa Binding && return _super(b.val, store) - sup = _super(b.val, store) - if sup isa EXPR && StaticLint.hasref(sup) - StaticLint.refof(sup) - else - store[:Core][:Any] - end -end - -function _super(x::EXPR, store)::Union{EXPR,Nothing} - if x.head === :struct - _super(x.args[2], store) - elseif x.head === :abstract || x.head === :primtive - _super(x.args[1], store) - elseif CSTParser.issubtypedecl(x) - x.args[2] - elseif CSTParser.isbracketed(x) - _super(x.args[1], store) - end -end - -function subtypes(T::Binding) - @assert CSTParser.defines_abstract(T.val) - subTs = [] - for r in T.refs - if r isa EXPR && r.parent isa EXPR && CSTParser.issubtypedecl(r.parent) && r.parent.parent isa EXPR && CSTParser.defines_datatype(r.parent.parent) - push!(subTs, r.parent.parent) - end - end - subTs -end diff --git a/src/type_inf.jl b/src/type_inf.jl deleted file mode 100644 index cd5c11e..0000000 --- a/src/type_inf.jl +++ /dev/null @@ -1,323 +0,0 @@ -function settype!(b::Binding, type::Binding) - push!(type.refs, b) - b.type = type -end - -function settype!(b::Binding, type) - b.type = type -end - -function infer_type(binding::Binding, scope, state) - if binding isa Binding - binding.type !== nothing && return - if binding.val isa EXPR && CSTParser.defines_module(binding.val) - settype!(binding, CoreTypes.Module) - elseif binding.val isa EXPR && CSTParser.defines_function(binding.val) - settype!(binding, CoreTypes.Function) - elseif binding.val isa EXPR && CSTParser.defines_datatype(binding.val) - settype!(binding, CoreTypes.DataType) - elseif binding.val isa EXPR - if isassignment(binding.val) - if CSTParser.is_func_call(binding.val.args[1]) - settype!(binding, CoreTypes.Function) - else - infer_type_assignment_rhs(binding, state, scope) - end - elseif binding.val.head isa EXPR && valof(binding.val.head) == "::" - infer_type_decl(binding, state, scope) - elseif iswhere(parentof(binding.val)) - settype!(binding, CoreTypes.DataType) - end - end - end -end - -function infer_type_assignment_rhs(binding, state, scope) - is_destructuring = false - lhs = binding.val.args[1] - rhs = binding.val.args[2] - if is_loop_iter_assignment(binding.val) - settype!(binding, infer_eltype(rhs)) - elseif headof(rhs) === :ref && length(rhs.args) > 1 - ref = refof_maybe_getfield(rhs.args[1]) - if ref isa Binding && ref.val isa EXPR - settype!(binding, infer_eltype(ref.val)) - end - else - if CSTParser.is_func_call(rhs) - if CSTParser.istuple(lhs) - if CSTParser.isparameters(lhs.args[1]) - is_destructuring = true - else - return - end - end - callname = CSTParser.get_name(rhs) - if isidentifier(callname) - resolve_ref(callname, scope, state) - if hasref(callname) - rb = get_root_method(refof(callname), state.server) - if (rb isa Binding && (CoreTypes.isdatatype(rb.type) || rb.val isa SymbolServer.DataTypeStore)) || rb isa SymbolServer.DataTypeStore - if is_destructuring - infer_destructuring_type(binding, rb) - else - settype!(binding, rb) - end - end - end - end - elseif headof(rhs) === :INTEGER - settype!(binding, CoreTypes.Int) - elseif headof(rhs) === :HEXINT - if length(rhs.val) < 5 - settype!(binding, CoreTypes.UInt8) - elseif length(rhs.val) < 7 - settype!(binding, CoreTypes.UInt16) - elseif length(rhs.val) < 11 - settype!(binding, CoreTypes.UInt32) - else - settype!(binding, CoreTypes.UInt64) - end - elseif headof(rhs) === :FLOAT - settype!(binding, CoreTypes.Float64) - elseif CSTParser.isstringliteral(rhs) - settype!(binding, CoreTypes.String) - elseif headof(rhs) === :TRUE || headof(rhs) === :FALSE - settype!(binding, CoreTypes.Bool) - elseif isidentifier(rhs) || is_getfield_w_quotenode(rhs) - refof_rhs = isidentifier(rhs) ? refof(rhs) : refof_maybe_getfield(rhs) - if refof_rhs isa Binding - if refof_rhs.val isa SymbolServer.GenericStore && refof_rhs.val.typ isa SymbolServer.FakeTypeName - settype!(binding, maybe_lookup(refof_rhs.val.typ.name, state)) - elseif refof_rhs.val isa SymbolServer.FunctionStore - settype!(binding, CoreTypes.Function) - elseif refof_rhs.val isa SymbolServer.DataTypeStore - settype!(binding, CoreTypes.DataType) - else - settype!(binding, refof_rhs.type) - end - elseif refof_rhs isa SymbolServer.GenericStore && refof_rhs.typ isa SymbolServer.FakeTypeName - settype!(binding, maybe_lookup(refof_rhs.typ.name, state)) - elseif refof_rhs isa SymbolServer.FunctionStore - settype!(binding, CoreTypes.Function) - elseif refof_rhs isa SymbolServer.DataTypeStore - settype!(binding, CoreTypes.DataType) - end - end - end -end - -function infer_destructuring_type(binding, rb::SymbolServer.DataTypeStore) - assigned_name = CSTParser.get_name(binding.val) - for (fieldname, fieldtype) in zip(rb.fieldnames, rb.types) - if fieldname == assigned_name - settype!(binding, fieldtype) - return - end - end -end -function infer_destructuring_type(binding::Binding, rb::EXPR) - assigned_name = string(to_codeobject(binding.name)) - scope = scopeof(rb) - names = scope.names - if haskey(names, assigned_name) - b = names[assigned_name] - settype!(binding, b.type) - end -end -infer_destructuring_type(binding, rb::Binding) = infer_destructuring_type(binding, rb.val) - -function infer_type_decl(binding, state, scope) - t = binding.val.args[2] - if isidentifier(t) - resolve_ref(t, scope, state) - end - if iscurly(t) - t = t.args[1] - resolve_ref(t, scope, state) - end - if CSTParser.is_getfield_w_quotenode(t) - resolve_getfield(t, scope, state) - t = t.args[2].args[1] - end - if refof(t) isa Binding - rb = get_root_method(refof(t), state.server) - if rb isa Binding && CoreTypes.isdatatype(rb.type) - settype!(binding, rb) - else - settype!(binding, refof(t)) - end - elseif refof(t) isa SymbolServer.DataTypeStore - settype!(binding, refof(t)) - end -end - -# Work out what type a bound variable has by functions that are called on it. -function infer_type_by_use(b::Binding, env::ExternalEnv) - b.type !== nothing && return # b already has a type - possibletypes = [] - visitedmethods = [] - ifbranch = nothing - for ref in b.refs - new_possibles = [] - ref isa EXPR || continue # skip non-EXPR (i.e. used for handling of globals) - # Some simple handling for :if blocks - if ifbranch === nothing - ifbranch = find_if_parents(ref) - else - newbranch = find_if_parents(ref) - if !in_same_if_branch(ifbranch, newbranch) - return - end - ifbranch = newbranch - end - check_ref_against_calls(ref, visitedmethods, new_possibles, env) - if !isempty(new_possibles) - if isempty(possibletypes) - possibletypes = new_possibles - else - possibletypes = intersect(possibletypes, new_possibles) - end - if isempty(possibletypes) - return - end - end - end - # Only do something if we're left with a singleton set at the end. - if length(possibletypes) == 1 - type = first(possibletypes) - if type isa Binding - settype!(b, type) - elseif type isa SymbolServer.DataTypeStore - settype!(b, type) - elseif type isa SymbolServer.VarRef - settype!(b, SymbolServer._lookup(type, getsymbols(env))) # could be nothing - elseif type isa SymbolServer.FakeTypeName && isempty(type.parameters) - settype!(b, SymbolServer._lookup(type.name, getsymbols(env))) # could be nothing - end - end -end - -function check_ref_against_calls(x, visitedmethods, new_possibles, env::ExternalEnv) - if is_arg_of_resolved_call(x) && !call_is_func_sig(x.parent) - sig = parentof(x) - # x is argument of function call (func) and we know what that function is - if CSTParser.isidentifier(sig.args[1]) - func = refof(sig.args[1]) - else - func = refof(sig.args[1].args[2].args[1]) - end - argi = get_arg_position_in_call(sig, x) # what slot does ref sit in? - tls = retrieve_toplevel_scope(x) - if func isa Binding - for method in func.refs - method = get_method(method) - method === nothing && continue - if method isa EXPR - if defines_function(method) - get_arg_type_at_position(method, argi, new_possibles) - # elseif CSTParser.defines_struct(method) - # Can we ignore this? Default constructor gives us no type info? - end - else # elseif what? - iterate_over_ss_methods(method, tls, env, m -> (get_arg_type_at_position(m, argi, new_possibles);false)) - end - end - else - iterate_over_ss_methods(func, tls, env, m -> (get_arg_type_at_position(m, argi, new_possibles);false)) - end - end -end - -function call_is_func_sig(call::EXPR) - # assume initially called on a :call - if call.parent isa EXPR - if call.parent.head === :function || CSTParser.is_eq(call.parent.head) - true - elseif isdeclaration(call.parent) || iswhere(call.parent) - call_is_func_sig(call.parent) - else - false - end - else - false - end -end - -function is_arg_of_resolved_call(x::EXPR) - parentof(x) isa EXPR && headof(parentof(x)) === :call && # check we're in a call signature - (caller = parentof(x).args[1]) !== x && # and that x is not the caller - ((CSTParser.isidentifier(caller) && hasref(caller)) || (is_getfield(caller) && headof(caller.args[2]) === :quotenode && hasref(caller.args[2].args[1]))) -end - -function get_arg_position_in_call(sig::EXPR, arg) - for i in 1:length(sig.args) - sig.args[i] == arg && return i - end -end - -function get_arg_type_at_position(method, argi, types) - if method isa EXPR - sig = CSTParser.get_sig(method) - if sig !== nothing && - sig.args !== nothing && argi <= length(sig.args) && - hasbinding(sig.args[argi]) && - (argb = bindingof(sig.args[argi]); argb isa Binding && argb.type !== nothing) && - !(argb.type in types) - push!(types, argb.type) - return - end - elseif method isa SymbolServer.DataTypeStore || method isa SymbolServer.FunctionStore - for m in method.methods - get_arg_type_at_position(m, argi, types) - end - end - return -end - -function get_arg_type_at_position(m::SymbolServer.MethodStore, argi, types) - if length(m.sig) >= argi && m.sig[argi][2] != SymbolServer.VarRef(SymbolServer.VarRef(nothing, :Core), :Any) && !(m.sig[argi][2] in types) - push!(types, m.sig[argi][2]) - end -end - -# Assumes x.head.val == "=" -is_loop_iter_assignment(x::EXPR) = x.parent isa EXPR && ((x.parent.head == :for || x.parent.head == :generator) || (x.parent.head == :block && x.parent.parent isa EXPR && (x.parent.parent.head == :for || x.parent.parent.head == :generator))) - -function infer_eltype(x::EXPR) - if isidentifier(x) && hasref(x) # assume is IDENT - r = refof(x) - if r isa Binding && r.val isa EXPR - if isassignment(r.val) && r.val.args[2] != x - return infer_eltype(r.val.args[2]) - end - end - elseif headof(x) === :ref && hasref(x.args[1]) - r = refof(x.args[1]) - if r isa SymbolServer.DataTypeStore || - r isa Binding && CoreTypes.isdatatype(r.type) - r - end - elseif headof(x) === :STRING - return CoreTypes.Char - elseif headof(x) === :call && length(x.args) > 2 && CSTParser.is_colon(x.args[1]) - if headof(x.args[2]) === :INTEGER && headof(x.args[3]) === :INTEGER - return CoreTypes.Int - elseif headof(x.args[2]) === :FLOAT && headof(x.args[3]) === :FLOAT - return CoreTypes.Float64 - elseif headof(x.args[2]) === :CHAR && headof(x.args[3]) === :CHAR - return CoreTypes.Char - end - elseif hasbinding(x) && isdeclaration(x) && length(x.args) == 2 - return maybe_get_vec_eltype(x.args[2]) - end -end - -function maybe_get_vec_eltype(t) - if iscurly(t) - lhs_ref = refof_maybe_getfield(t.args[1]) - if lhs_ref isa SymbolServer.DataTypeStore && CoreTypes.isarray(lhs_ref) && length(t.args) > 1 - refof(t.args[2]) - end - end -end diff --git a/src/utils.jl b/src/utils.jl deleted file mode 100644 index fb187cc..0000000 --- a/src/utils.jl +++ /dev/null @@ -1,337 +0,0 @@ -quoted(x) = headof(x) === :quote || headof(x) === :quotenode -unquoted(x) = isunarycall(x) && valof(x.args[1]) == "\$" - -function remove_ref(x::EXPR) - if hasref(x) && refof(x) isa Binding && refof(x).refs isa Vector - for ia in enumerate(refof(x).refs) - if ia[2] == x - deleteat!(refof(x).refs, ia[1]) - setref!(x, nothing) - return - end - end - error() - end -end - -function clear_binding(x::EXPR) - if bindingof(x) isa Binding - for r in bindingof(x).refs - if r isa EXPR - setref!(r, nothing) - elseif r isa Binding - if r.type == bindingof(x) - r.type = nothing - else - clear_binding(r) - end - end - end - x.meta.binding = nothing - end -end -function clear_scope(x::EXPR) - if hasmeta(x) && scopeof(x) isa Scope - setparent!(scopeof(x), nothing) - empty!(scopeof(x).names) - if headof(x) === :file && scopeof(x).modules isa Dict && scopehasmodule(scopeof(x), :Base) && scopehasmodule(scopeof(x), :Core) - m1, m2 = getscopemodule(scopeof(x), :Base), getscopemodule(scopeof(x), :Core) - empty!(scopeof(x).modules) - addmoduletoscope!(scopeof(x), m1) - addmoduletoscope!(scopeof(x), m2) - else - scopeof(x).modules = nothing - end - if scopeof(x).overloaded !== nothing - empty!(scopeof(x).overloaded) - end - end -end - -function clear_ref(x::EXPR) - if refof(x) isa Binding - if refof(x).refs isa Vector - for i in 1:length(refof(x).refs) - if refof(x).refs[i] == x - deleteat!(refof(x).refs, i) - break - end - end - end - setref!(x, nothing) - elseif refof(x) !== nothing - setref!(x, nothing) - end -end -function clear_error(x::EXPR) - if hasmeta(x) && x.meta.error !== nothing - x.meta.error = nothing - end -end -function clear_meta(x::EXPR) - clear_binding(x) - clear_ref(x) - clear_scope(x) - clear_error(x) - if x.args !== nothing - for a in x.args - clear_meta(a) - end - end - # if x.trivia !== nothing - # for a in x.trivia - # clear_meta(a) - # end - # end -end - -function get_root_method(b, server) - return b -end - -function get_root_method(b::Binding, server) - if CoreTypes.isfunction(b.type) && !isempty(b.refs) - first(b.refs) - else - b - end -end - -function retrieve_delayed_scope(x) - if (CSTParser.defines_function(x) || CSTParser.defines_macro(x)) && scopeof(x) !== nothing - if parentof(scopeof(x)) !== nothing - return parentof(scopeof(x)) - else - return scopeof(x) - end - else - return retrieve_scope(x) - end - return nothing -end - -function retrieve_scope(x) - if scopeof(x) !== nothing - return scopeof(x) - elseif parentof(x) isa EXPR - return retrieve_scope(parentof(x)) - end - return -end - - -# function find_return_statements(x::EXPR) -# rets = EXPR[] -# if CSTParser.defines_function(x) -# find_return_statements(x.args[2], true, rets) -# end -# return rets -# end - -# function find_return_statements(x::EXPR, last_stmt, rets) -# if last_stmt && !(headof(x) === :block || headof(x) === :if || iskw(x)) -# push!(rets, x) -# return rets, false -# end - -# if headof(x) === :return -# push!(rets, x) -# return rets, true -# end - - -# for i = 1:length(x) -# _, stop_iter = find_return_statements(x[i], last_stmt && (i == length(x) || (headof(x) === CSTParser.If && headof(x[i]) === CSTParser.Block)), rets) -# stop_iter && break -# end -# return rets, false -# end - -function find_exported_names(x::EXPR) - exported_vars = EXPR[] - for i in 1:length(x.args[3].args) - expr = x.args[3].args[i] - if headof(expr) === :export - for j = 2:length(expr.args) - if isidentifier(expr.args[j]) && hasref(expr.args[j]) - push!(exported_vars, expr.args[j]) - end - end - end - end - return exported_vars -end - -hasreadperm(p::String) = (uperm(p) & 0x04) == 0x04 - -# check whether a path is in (including subfolders) the julia base dir. Returns "" if not, and the path to the base dir if so. -function _is_in_basedir(path::String) - i = findfirst(r".*base", path) - i === nothing && return "" - path1 = path[i]::String - !hasreadperm(path1) && return "" - !isdir(path1) && return "" - files = readdir(path1) - if all(f -> f in files, ["Base.jl", "coreio.jl", "essentials.jl", "exports.jl"]) - return path1 - end - return "" -end - -_is_macrocall_to_BaseDIR(arg) = headof(arg) === :macrocall && length(arg.args) == 2 && valof(arg.args[1]) == "@__DIR__" - - -isexportedby(k::Symbol, m::SymbolServer.ModuleStore) = haskey(m, k) && k in m.exportednames -isexportedby(k::String, m::SymbolServer.ModuleStore) = isexportedby(Symbol(k), m) -isexportedby(x::EXPR, m::SymbolServer.ModuleStore) = isexportedby(valof(x), m) -isexportedby(k, m::SymbolServer.ModuleStore) = false - -function retrieve_toplevel_scope(x::EXPR) - if scopeof(x) !== nothing && is_toplevel_scope(x) - return scopeof(x) - elseif parentof(x) isa EXPR - return retrieve_toplevel_scope(parentof(x)) - else - @info "Tried to reach toplevel scope, no scope found. Final expression $(headof(x))" - return nothing - end -end -retrieve_toplevel_scope(s::Scope) = (is_toplevel_scope(s) || !(parentof(s) isa Scope)) ? s : retrieve_toplevel_scope(parentof(s)) -retrieve_toplevel_or_func_scope(s::Scope) = (is_toplevel_scope(s) || defines_function(s.expr) || !(parentof(s) isa Scope)) ? s : retrieve_toplevel_or_func_scope(parentof(s)) - -is_toplevel_scope(s::Scope) = is_toplevel_scope(s.expr) -is_toplevel_scope(x::EXPR) = CSTParser.defines_module(x) || headof(x) === :file - -# b::SymbolServer.FunctionStore or DataTypeStore -# tls is a top-level Scope (expected to contain loaded modules) -# for a FunctionStore b, checks whether additional methods are provided by other packages -# f is a function that returns `true` if we want to break early from the loop - -iterate_over_ss_methods(b, tls, env, f) = false -function iterate_over_ss_methods(b::SymbolServer.FunctionStore, tls::Scope, env::ExternalEnv, f) - for m in b.methods - ret = f(m) - ret && return true - end - if b.extends in keys(getsymbolextendeds(env)) && tls.modules !== nothing - # above should be modified, - rootmod = SymbolServer._lookup(b.extends.parent, getsymbols(env)) # points to the module containing the initial function declaration - if rootmod !== nothing && haskey(rootmod, b.extends.name) # check rootmod exists, and that it has the variable - # find extensoions - if haskey(getsymbolextendeds(env), b.extends) # method extensions listed - for vr in getsymbolextendeds(env)[b.extends] # iterate over packages with extensions - !(SymbolServer.get_top_module(vr) in keys(tls.modules)) && continue - rootmod = SymbolServer._lookup(vr, getsymbols(env)) - !(rootmod isa SymbolServer.ModuleStore) && continue - if haskey(rootmod.vals, b.extends.name) && (rootmod.vals[b.extends.name] isa SymbolServer.FunctionStore || rootmod.vals[b.extends.name] isa SymbolServer.DataTypeStore)# check package is available and has ref - for m in rootmod.vals[b.extends.name].methods # - ret = f(m) - ret && return true - end - end - end - end - end - end - return false -end - -function iterate_over_ss_methods(b::SymbolServer.DataTypeStore, tls::Scope, env::ExternalEnv, f) - if b.name isa SymbolServer.VarRef - bname = b.name - elseif b.name isa SymbolServer.FakeTypeName - bname = b.name.name - end - for m in b.methods - ret = f(m) - ret && return true - end - if (bname in keys(getsymbolextendeds(env))) && tls.modules !== nothing - # above should be modified, - rootmod = SymbolServer._lookup(bname.parent, getsymbols(env), true) # points to the module containing the initial function declaration - if rootmod !== nothing && haskey(rootmod, bname.name) # check rootmod exists, and that it has the variable - # find extensoions - if haskey(getsymbolextendeds(env), bname) # method extensions listed - for vr in getsymbolextendeds(env)[bname] # iterate over packages with extensions - !(SymbolServer.get_top_module(vr) in keys(tls.modules)) && continue - rootmod = SymbolServer._lookup(vr, getsymbols(env)) - !(rootmod isa SymbolServer.ModuleStore) && continue - if haskey(rootmod.vals, bname.name) && (rootmod.vals[bname.name] isa SymbolServer.FunctionStore || rootmod.vals[bname.name] isa SymbolServer.DataTypeStore)# check package is available and has ref - for m in rootmod.vals[bname.name].methods # - ret = f(m) - ret && return true - end - end - end - end - end - end - return false -end - - -""" - is_in_fexpr(x::EXPR, f) -Check whether `x` isa the child of an expression for which `f(parent) == true`. -""" -is_in_fexpr(x::EXPR, f) = f(x) || (parentof(x) isa EXPR && is_in_fexpr(parentof(x), f)) - -""" - get_in_fexpr(x::EXPR, f) -Get the `parent` of `x` for which `f(parent) == true`. (is_in_fexpr should be called first.) -""" -get_parent_fexpr(x::EXPR, f) = f(x) ? x : get_parent_fexpr(parentof(x), f) - -maybe_get_parent_fexpr(x::Nothing, f) = nothing -maybe_get_parent_fexpr(x::EXPR, f) = f(x) ? x : maybe_get_parent_fexpr(parentof(x), f) - -issigoffuncdecl(x::EXPR) = parentof(x) isa EXPR ? issigoffuncdecl(x, parentof(x)) : false -function issigoffuncdecl(x::EXPR, p::EXPR) - if CSTParser.iswhere(p) || CSTParser.isdeclaration(p) - return issigoffuncdecl(parentof(p)) - elseif CSTParser.defines_function(p) - return true - else - return false - end -end -issigoffuncdecl(x::EXPR, p) = false - -function is_nameof_func(name) - f = get_parent_fexpr(name, CSTParser.defines_function) - f !== nothing && CSTParser.get_name(f) == name -end - -function loose_refs(b::Binding) - b.val isa EXPR || return b.refs # to account for `#global` binding which doesn't have a val - scope = retrieve_scope(b.val) - scope isa Scope && isidentifier(b.name) || return b.refs - name_str = valofid(b.name) - name_str isa String || return b.refs - - if is_soft_scope(scope) && parentof(scope) isa Scope && scopehasbinding(parentof(scope), name_str) && !scopehasbinding(scope, name_str) - scope = parentof(scope) - end - state = LooseRefs(scope.expr, name_str, scope, []) - state(scope.expr) - vcat([r.refs for r in state.result]...) -end - -mutable struct LooseRefs - x::EXPR - name::String - scope::Scope - result::Vector{Binding} -end - -function (state::LooseRefs)(x::EXPR) - if hasbinding(x) - ex = bindingof(x).name - if isidentifier(ex) && valofid(ex) == state.name - push!(state.result, bindingof(x)) - end - end - if !hasscope(x) || (hasscope(x) && ((is_soft_scope(scopeof(x)) && !scopehasbinding(scopeof(x), state.name)) || scopeof(x) == state.scope)) - traverse(x, state) - end -end From d612d08120c1f5d0a9b4acba3f44b5bab6876133 Mon Sep 17 00:00:00 2001 From: Alexandre Bergel Date: Mon, 28 Oct 2024 10:34:14 +0100 Subject: [PATCH 05/12] Cleaning dependencies --- Manifest.toml | 98 +-------------------------------------------------- Project.toml | 2 -- 2 files changed, 1 insertion(+), 99 deletions(-) diff --git a/Manifest.toml b/Manifest.toml index a30fec1..4bcc93c 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -2,14 +2,7 @@ julia_version = "1.10.2" manifest_format = "2.0" -project_hash = "255338e3f9e4c058e9a5fae9b019df749db3086a" - -[[deps.ArgTools]] -uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" -version = "1.1.1" - -[[deps.Artifacts]] -uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +project_hash = "64d1be42d85e48d8d22a77d412761d83485cc341" [[deps.Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" @@ -24,14 +17,6 @@ version = "3.4.3" deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" -[[deps.Downloads]] -deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] -uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -version = "1.6.0" - -[[deps.FileWatching]] -uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" - [[deps.InteractiveUtils]] deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" @@ -48,67 +33,19 @@ version = "1.14.0" [deps.JSON3.weakdeps] ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" -[[deps.LibCURL]] -deps = ["LibCURL_jll", "MozillaCACerts_jll"] -uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" -version = "0.6.4" - -[[deps.LibCURL_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] -uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "8.4.0+0" - -[[deps.LibGit2]] -deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"] -uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" - -[[deps.LibGit2_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"] -uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" -version = "1.6.4+0" - -[[deps.LibSSH2_jll]] -deps = ["Artifacts", "Libdl", "MbedTLS_jll"] -uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" -version = "1.11.0+1" - -[[deps.Libdl]] -uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" - -[[deps.Logging]] -uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" - [[deps.Markdown]] deps = ["Base64"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" -[[deps.MbedTLS_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" -version = "2.28.2+1" - [[deps.Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" -[[deps.MozillaCACerts_jll]] -uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2023.1.10" - -[[deps.NetworkOptions]] -uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" -version = "1.2.0" - [[deps.Parsers]] deps = ["Dates", "PrecompileTools", "UUIDs"] git-tree-sha1 = "8489905bcdbcfac64d1daa51ca07c0d8f0283821" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" version = "2.8.1" -[[deps.Pkg]] -deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] -uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.10.0" - [[deps.PrecompileTools]] deps = ["Preferences"] git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f" @@ -125,10 +62,6 @@ version = "1.4.3" deps = ["Unicode"] uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" -[[deps.REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] -uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" - [[deps.Random]] deps = ["SHA"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -140,31 +73,17 @@ version = "0.7.0" [[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" -[[deps.Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" - [[deps.StructTypes]] deps = ["Dates", "UUIDs"] git-tree-sha1 = "ca4bccb03acf9faaf4137a9abc1881ed1841aa70" uuid = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" version = "1.10.0" -[[deps.SymbolServer]] -deps = ["InteractiveUtils", "LibGit2", "Markdown", "Pkg", "REPL", "SHA", "Serialization", "Sockets", "UUIDs"] -git-tree-sha1 = "adcc6a2335e5448adc05939f67d382fb8d17a367" -uuid = "cf896787-08d5-524d-9de7-132aaa0cb996" -version = "7.4.0" - [[deps.TOML]] deps = ["Dates"] uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" version = "1.0.3" -[[deps.Tar]] -deps = ["ArgTools", "SHA"] -uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" -version = "1.10.0" - [[deps.Tokenize]] git-tree-sha1 = "468b4685af4abe0e9fd4d7bf495a6554a6276e75" uuid = "0796e94c-ce3b-5d07-9a54-7f471281c624" @@ -176,18 +95,3 @@ uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [[deps.Unicode]] uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" - -[[deps.Zlib_jll]] -deps = ["Libdl"] -uuid = "83775a58-1f1d-513f-b197-d71354ab007a" -version = "1.2.13+1" - -[[deps.nghttp2_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" -version = "1.52.0+1" - -[[deps.p7zip_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" -version = "17.4.0+2" diff --git a/Project.toml b/Project.toml index 837b38e..c5f5df0 100644 --- a/Project.toml +++ b/Project.toml @@ -8,11 +8,9 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" -SymbolServer = "cf896787-08d5-524d-9de7-132aaa0cb996" [compat] CSTParser = "3.3" -SymbolServer = "5.1.1, 6.0, 7.0" julia = "1" [extras] From 050bb448ec4252fca859a7cfdba31a9ba77aad35 Mon Sep 17 00:00:00 2001 From: Alexandre Bergel Date: Mon, 28 Oct 2024 10:43:31 +0100 Subject: [PATCH 06/12] Removed unused SymbolServer --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index e65a1f7..34a1a82 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,4 @@ -using StaticLint, SymbolServer +using StaticLint using CSTParser, Test using StaticLint: convert_offset_to_line_from_lines, check_all From 7e90a9a618924e9bd7b3ebc5b43a15f3b3e8571b Mon Sep 17 00:00:00 2001 From: Alexandre Bergel Date: Mon, 28 Oct 2024 11:17:02 +0100 Subject: [PATCH 07/12] Removed update from the registry. --- scripts/run_lint_locally.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/run_lint_locally.sh b/scripts/run_lint_locally.sh index b87ea1d..7bd50c4 100755 --- a/scripts/run_lint_locally.sh +++ b/scripts/run_lint_locally.sh @@ -33,12 +33,12 @@ echo "STATICLINT PATH=" $STATICLINTPATH # ls echo "CURRENT PATH=" $PWD -echo "Julia Registry updating and instantiating..." -cd $STATICLINTPATH -julia --project=$STATICLINTPATH -e " - import Pkg ; Pkg.Registry.update() ; Pkg.instantiate() ; Pkg.build() -" -cd - +# echo "Julia Registry updating and instantiating..." +# cd $STATICLINTPATH +# julia --project=$STATICLINTPATH -e " +# import Pkg ; Pkg.Registry.update() ; Pkg.instantiate() ; Pkg.build() +# " +# cd - echo "About to run StaticLint.jl..." julia --project=$STATICLINTPATH -e " From a632cbb489a90f1d1f8bc998b659a57dc233c4fa Mon Sep 17 00:00:00 2001 From: Alexandre Bergel Date: Mon, 28 Oct 2024 11:50:03 +0100 Subject: [PATCH 08/12] Adding the registry update back --- scripts/run_lint_locally.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/run_lint_locally.sh b/scripts/run_lint_locally.sh index 7bd50c4..b87ea1d 100755 --- a/scripts/run_lint_locally.sh +++ b/scripts/run_lint_locally.sh @@ -33,12 +33,12 @@ echo "STATICLINT PATH=" $STATICLINTPATH # ls echo "CURRENT PATH=" $PWD -# echo "Julia Registry updating and instantiating..." -# cd $STATICLINTPATH -# julia --project=$STATICLINTPATH -e " -# import Pkg ; Pkg.Registry.update() ; Pkg.instantiate() ; Pkg.build() -# " -# cd - +echo "Julia Registry updating and instantiating..." +cd $STATICLINTPATH +julia --project=$STATICLINTPATH -e " + import Pkg ; Pkg.Registry.update() ; Pkg.instantiate() ; Pkg.build() +" +cd - echo "About to run StaticLint.jl..." julia --project=$STATICLINTPATH -e " From c90cd3d99ca159d596fb331ebd93b638090ac814 Mon Sep 17 00:00:00 2001 From: Alexandre Bergel Date: Mon, 28 Oct 2024 11:57:32 +0100 Subject: [PATCH 09/12] Trying to simplify the installation --- scripts/run_lint_locally.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/run_lint_locally.sh b/scripts/run_lint_locally.sh index b87ea1d..0b3f0d3 100755 --- a/scripts/run_lint_locally.sh +++ b/scripts/run_lint_locally.sh @@ -29,15 +29,16 @@ fi echo "FULLNAME SCRIPT" $0 STATICLINTPATH=$(dirname $0)/.. echo "STATICLINT PATH=" $STATICLINTPATH -# cd $STATICLINTPATH -# ls echo "CURRENT PATH=" $PWD echo "Julia Registry updating and instantiating..." cd $STATICLINTPATH julia --project=$STATICLINTPATH -e " - import Pkg ; Pkg.Registry.update() ; Pkg.instantiate() ; Pkg.build() + import Pkg ; Pkg.instantiate() " +# julia --project=$STATICLINTPATH -e " +# import Pkg ; Pkg.Registry.update() ; Pkg.instantiate() ; Pkg.build() +# " cd - echo "About to run StaticLint.jl..." From 769ecef76ed8147dc0246f9f98333208f27ff9e0 Mon Sep 17 00:00:00 2001 From: Alexandre Bergel Date: Mon, 28 Oct 2024 12:12:14 +0100 Subject: [PATCH 10/12] Optimizing... --- scripts/run_lint_locally.sh | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/scripts/run_lint_locally.sh b/scripts/run_lint_locally.sh index 0b3f0d3..0c858d0 100755 --- a/scripts/run_lint_locally.sh +++ b/scripts/run_lint_locally.sh @@ -29,20 +29,21 @@ fi echo "FULLNAME SCRIPT" $0 STATICLINTPATH=$(dirname $0)/.. echo "STATICLINT PATH=" $STATICLINTPATH -echo "CURRENT PATH=" $PWD +RAICODE_PATH=$PWD +echo "CURRENT PATH=" $RAICODE_PATH -echo "Julia Registry updating and instantiating..." -cd $STATICLINTPATH -julia --project=$STATICLINTPATH -e " - import Pkg ; Pkg.instantiate() -" +# echo "Julia Registry updating and instantiating..." +# cd $STATICLINTPATH # julia --project=$STATICLINTPATH -e " -# import Pkg ; Pkg.Registry.update() ; Pkg.instantiate() ; Pkg.build() +# import Pkg ; Pkg.instantiate() # " -cd - +# cd - echo "About to run StaticLint.jl..." julia --project=$STATICLINTPATH -e " + import Pkg + Pkg.instantiate() + using StaticLint result = StaticLint.LintResult() all_files_tmp=split(open(io->read(io, String), \"/tmp/files_to_run_lint.txt\", \"r\")) From 6f69ba0eaa3ce22297bcfaa6de7d8392aa1b661a Mon Sep 17 00:00:00 2001 From: Alexandre Bergel Date: Wed, 30 Oct 2024 18:26:24 +0100 Subject: [PATCH 11/12] Making run_lint_locally.sh run multiple times simultenously. --- scripts/run_lint_locally.sh | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/scripts/run_lint_locally.sh b/scripts/run_lint_locally.sh index 0c858d0..10a19b4 100755 --- a/scripts/run_lint_locally.sh +++ b/scripts/run_lint_locally.sh @@ -1,27 +1,20 @@ #!/bin/bash -# If file exist, typically from a previous run, then we remove it -if [ -f /tmp/files_to_run_lint.txt ]; then - rm /tmp/files_to_run_lint.txt -fi - -# If the file containing result of lint exist, then we remove it -if [ -f /tmp/result_lint.txt ]; then - rm /tmp/result_lint.txt -fi +# temporary file containing all the files on which lint has to run. +FILES_TO_RUN=$(mktemp) # If no argument is provided, then we simply use the files staged if [[ $# -eq 0 ]] ; then echo 'No argument provided, running on staged files' FILES_LOCALLY_ADDED=`git status --porcelain | awk 'match($1, "A"){print $2}'` FILES_LOCALLY_MODIFIED=`git status --porcelain | awk 'match($1, "M"){print $2}'` - echo ${FILES_LOCALLY_ADDED} > /tmp/files_to_run_lint.txt - echo ${FILES_LOCALLY_MODIFIED} >> /tmp/files_to_run_lint.txt + echo ${FILES_LOCALLY_ADDED} > $FILES_TO_RUN + echo ${FILES_LOCALLY_MODIFIED} >> $FILES_TO_RUN else # If some files are provided, then we use these - echo $@ >> /tmp/files_to_run_lint.txt + echo $@ >> $FILES_TO_RUN # echo "RUNNING LINT ON: " - # cat /tmp/files_to_run_lint.txt + # cat "$FILES_TO_RUN" # echo "---" fi @@ -31,13 +24,7 @@ STATICLINTPATH=$(dirname $0)/.. echo "STATICLINT PATH=" $STATICLINTPATH RAICODE_PATH=$PWD echo "CURRENT PATH=" $RAICODE_PATH - -# echo "Julia Registry updating and instantiating..." -# cd $STATICLINTPATH -# julia --project=$STATICLINTPATH -e " -# import Pkg ; Pkg.instantiate() -# " -# cd - +echo "FILES_TO_RUN=" $FILES_TO_RUN echo "About to run StaticLint.jl..." julia --project=$STATICLINTPATH -e " @@ -46,7 +33,7 @@ julia --project=$STATICLINTPATH -e " using StaticLint result = StaticLint.LintResult() - all_files_tmp=split(open(io->read(io, String), \"/tmp/files_to_run_lint.txt\", \"r\")) + all_files_tmp=split(open(io->read(io, String), \"$FILES_TO_RUN\", \"r\")) # convert substring into string all_files=map(string, all_files_tmp) # filter to existing Julia files only From 5e10210dd1425a2fc75ee626fbff3ca3e64c7c21 Mon Sep 17 00:00:00 2001 From: Alexandre Bergel Date: Tue, 5 Nov 2024 10:37:34 +0100 Subject: [PATCH 12/12] Fixing bug when msg are not printed. --- src/interface.jl | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 6345375..1d0306b 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -230,7 +230,8 @@ function print_summary(::PreCommitFormat, io::IO, result::LintResult) printstyled(io, "Note that the list above only show fatal violations\n", color=:red) end -function print_report(::PreCommitFormat, io::IO, lint_report::LintRuleReport) +function print_report(::PreCommitFormat, io::IO, lint_report::LintRuleReport, result::LintResult) + should_print_report(result) || return # Do not print anything if it is not a fatal violation lint_report.rule isa FatalLintRule || return printstyled(io, "Line $(lint_report.line), column $(lint_report.column):", color=:green) @@ -238,6 +239,7 @@ function print_report(::PreCommitFormat, io::IO, lint_report::LintRuleReport) print(io, lint_report.msg) print(io, " ") println(io, lint_report.file) + result.printout_count += 1 end should_print_report(result) = result.printout_count <= MAX_REPORTED_ERRORS @@ -274,12 +276,15 @@ function print_header(::PlainFormat, io::IO, rootpath::String) printstyled(io, "-" ^ 10 * " $(rootpath)\n", color=:blue) end -function print_report(::PlainFormat, io::IO, lint_report::LintRuleReport) +function print_report(::PlainFormat, io::IO, lint_report::LintRuleReport, result::LintResult) + should_print_report(result) || return printstyled(io, "Line $(lint_report.line), column $(lint_report.column):", color=:green) print(io, " ") print(io, lint_report.msg) print(io, " ") println(io, lint_report.file) + result.printout_count += 1 + end function print_summary( @@ -321,7 +326,9 @@ function remove_prefix_from_filename(file_name::String, format::MarkdownFormat) return remove_prefix_from_filename(file_name, format.file_prefix_to_remove) end -function print_report(format::MarkdownFormat, io::IO, lint_report::LintRuleReport) +function print_report(format::MarkdownFormat, io::IO, lint_report::LintRuleReport, result::LintResult) + should_print_report(result) || return + corrected_file_name = remove_prefix_from_filename(lint_report.file, format) coordinates = "Line $(lint_report.line), column $(lint_report.column):" @@ -335,6 +342,7 @@ function print_report(format::MarkdownFormat, io::IO, lint_report::LintRuleRepor # Produce workflow command to see results in the PR file changed tab: # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#example-setting-an-error-message println(format.stream_workflowcommand, "::error file=$(corrected_file_name),line=$(lint_report.line),col=$(lint_report.column)::$(lint_report.msg)") + result.printout_count += 1 end print_summary( @@ -391,26 +399,17 @@ function run_lint( # Fatal reports are printed in io_violations, but first io_tmp = isnothing(io_violations) ? io : io_violations for r in fatalviolation_reports - if should_print_report(result) - print_report(formatter, io_tmp, r) - result.printout_count += 1 - end + print_report(formatter, io_tmp, r, result) end io_tmp = isnothing(io_violations) ? io : io_violations for r in violation_reports - if should_print_report(result) - print_report(formatter, io_tmp, r) - result.printout_count += 1 - end + print_report(formatter, io_tmp, r, result) end io_tmp = isnothing(io_recommendations) ? io : io_recommendations for r in recommandation_reports - if should_print_report(result) - print_report(formatter, io_tmp, r) - result.printout_count += 1 - end + print_report(formatter, io_tmp, r, result) end # We run Lint on a single file.