diff --git a/REQUIRE b/REQUIRE index 4bcf3b3..4447f78 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,2 +1,3 @@ -julia 0.4 +julia 0.5 +JSON 0.6.0 Compat 0.15.0 diff --git a/docs/features.md b/docs/features.md index 6bd81fc..6da5c37 100644 --- a/docs/features.md +++ b/docs/features.md @@ -209,10 +209,92 @@ Make Julia start listening on a given port and return lint messages to requests This feature is useful when you want to lint julia code in a non julia environment. Existing plugins: + * Sublime Text: [SublimeLinter-contrib-julialintserver](https://github.com/invenia/SublimeLinter-contrib-julialintserver) * linter-julia for Atom: [linter-julia](https://github.com/TeroFrondelius/linter-julia) -The protocol for the server is: +The new protocol for the server is JSON in both input and output: +```json +{ + "file":"path_to_the_file", + "code_str":"full_text_of_the_file", + "ignore_codes":["E381","W361","I171"], + "ignore_info":false, + "ignore_warnings":false, + "show_code":true +} +``` +Only the two first `"file"` and `"code_str"` are mandatory fields. For the output +there are four different protocols from which the `"lint-message"` is the direct +match of `LintMessage` and this way will be always up to date, but can also break. +Other three types are for convenience, they give you the opportunity to +directly pass the messages forward for example Atom linter. Here is one full example, +[to see more examples, see the tests.](https://github.com/tonyhffong/Lint.jl/blob/master/test/server.jl) +```julia +julia> using Lint + +julia> using JSON + +julia> if is_windows() + pipe_lm = "\\\\.\\pipe\\testsocket" + else # linux, osx + pipe_lm = tempname() + end +"/tmp/julial73DPo" + +julia> server_lm = @async lintserver(pipe_lm,"lint-message") +Server running on port/pipe /tmp/julial73DPo ... +Task (queued) @0x00007f1b20a38280 + +julia> input = Dict("file" => "none", "code_str" => "something") +Dict{String,String} with 2 entries: + "file" => "none" + "code_str" => "something" + +julia> conn = connect(pipe_lm) +Base.PipeEndpoint(open, 0 bytes waiting) + +julia> JSON.print(conn, input) + +julia> out = JSON.parse(conn) +1-element Array{Any,1}: + Dict{String,Any}(Pair{String,Any}("line",1),Pair{String,Any}("scope",""),Pair{String,Any}("message","use of undeclared symbol"),Pair{String,Any}("file","none"),Pair{String,Any}("code","E321"),Pair{String,Any}("variable","something")) + +julia> out[1] +Dict{String,Any} with 6 entries: + "line" => 1 + "scope" => "" + "message" => "use of undeclared symbol" + "file" => "none" + "code" => "E321" + "variable" => "something" + +julia> +``` +`lintserver(port,style)` style will accept four values: + +1. "lint-message", which is the preferred and shown in above example +2. ["standard-linter-v1"](https://github.com/steelbrain/linter/blob/v1/docs/examples/standard-linter-v1.md) +3. ["vscode"](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#diagnostic) +4. ["standard-linter-v2"](https://github.com/steelbrain/linter/blob/master/docs/examples/standard-linter-v2.md) + +If any of the above four JSON formats is not answering your needs, please make a +[new pull request](https://github.com/tonyhffong/Lint.jl/pulls). The file you want +to edit is [Lint.jl](https://github.com/tonyhffong/Lint.jl/blob/master/src/Lint.jl) +and the function is called `convertmsgtojson`. It is enough to add one `elseif` +block, here is one of them as an example: +```julia +elseif style == "standard-linter-v2" + push!(output, Dict("severity" => etype, + "location" => Dict("file" => file, + "position" => errorrange), + "excerpt" => code, + "description" => "$evar: $txt")) + +end +``` + +The old protocol for the server is: 1. The file path followed by a new line 2. The number of bytes of code being sent followed by a new line diff --git a/src/Lint.jl b/src/Lint.jl index 8b98f53..d633b8b 100644 --- a/src/Lint.jl +++ b/src/Lint.jl @@ -5,6 +5,7 @@ module Lint using Base.Meta using Compat using Compat.TypeUtils +using JSON if isdefined(Base, :unwrap_unionall) using Base: unwrap_unionall @@ -343,7 +344,7 @@ function lintinclude(ctx::LintContext, file::AbstractString) end """ -Lint all *.jl files at a given directory. +Lint all .jl ending files at a given directory. Will ignore LintContext file and already included files. """ function lintdir{T<:AbstractString}(dir::T, ctx::LintContext=LintContext()) @@ -358,34 +359,118 @@ function lintdir{T<:AbstractString}(dir::T, ctx::LintContext=LintContext()) ctx.messages end -function readandwritethestream(conn) - # println("Connection accepted") - # Get file, code length and code - file = strip(readline(conn)) - # println("file: ", file) - code_len = parse(Int, strip(readline(conn))) - # println("Code bytes: ", code_len) - code = Compat.UTF8String(read(conn, code_len)) - # println("Code received") - # Do the linting - msgs = lintfile(file, code) - # Write response to socket - for i in msgs - write(conn, string(i)) +function convertmsgtojson(msgs, style, dict_data) + if style == "lint-message" + return msgs + end + output = Any[] + for msg in msgs + evar = msg.variable + txt = msg.message + file = msg.file + linenumber = msg.line + # Atom index starts from zero thus minus one + errorrange = Array[[linenumber-1, 0], [linenumber-1, 80]] + code = string(msg.code) + if code[1] == 'I' + etype = "info" + etypenumber = 3 + elseif code[1] == 'W' + etype = "warning" + etypenumber = 2 + else + etype = "error" + etypenumber = 1 + end + + if style == "standard-linter-v1" + if haskey(dict_data,"show_code") + if dict_data["show_code"] + msgtext = "$code $evar: $txt" + else + msgtext = "$evar: $txt" + end + else + msgtext = "$code $evar: $txt" + end + push!(output, Dict("type" => etype, + "text" => msgtext, + "range" => errorrange, + "filePath" => file)) + elseif style == "vscode" + push!(output, Dict("severity" => etypenumber, + "message" => "$evar: $txt", + "range" => errorrange, + "filePath" => file, + "code" => code, + "source" => "Lint.jl")) + elseif style == "standard-linter-v2" + push!(output, Dict("severity" => etype, + "location" => Dict("file" => file, + "position" => errorrange), + "excerpt" => code, + "description" => "$evar: $txt")) + + end + end + return output +end + + +function filtermsgs(msgs,dict_data) + if haskey(dict_data,"ignore_warnings") + if dict_data["ignore_warnings"] + msgs = filter(i -> !iswarning(i), msgs) + end + end + if haskey(dict_data,"ignore_info") + if dict_data["ignore_info"] + msgs = filter(i -> !isinfo(i), msgs) + end + end + if haskey(dict_data,"ignore_codes") + msgs = filter(i -> !(string(i.code) in dict_data["ignore_codes"]), msgs) + end + return msgs +end + + +function readandwritethestream(conn,style) + if style == "original_behaviour" + # println("Connection accepted") + # Get file, code length and code + file = strip(readline(conn)) + # println("file: ", file) + code_len = parse(Int, strip(readline(conn))) + # println("Code bytes: ", code_len) + code = Compat.UTF8String(read(conn, code_len)) + # println("Code received") + # Do the linting + msgs = lintfile(file, code) + # Write response to socket + for i in msgs + write(conn, string(i)) + write(conn, "\n") + end + # Blank line to indicate end of messages write(conn, "\n") + else + dict_data = JSON.parse(conn) + msgs = lintfile(dict_data["file"], dict_data["code_str"]) + msgs = filtermsgs(msgs, dict_data) + out = convertmsgtojson(msgs, style, dict_data) + JSON.print(conn, out) end - # Blank line to indicate end of messages - write(conn, "\n") end -function lintserver(port) +function lintserver(port,style="original_behaviour") server = listen(port) try - println("Server running on port $port ...") + println("Server running on port/pipe $port ...") while true conn = accept(server) @async try - readandwritethestream(conn) + readandwritethestream(conn,style) catch err println(STDERR, "connection ended with error $err") finally @@ -399,6 +484,7 @@ function lintserver(port) end end + # precompile hints include("precompile.jl") diff --git a/test/server.jl b/test/server.jl index e903952..a532653 100644 --- a/test/server.jl +++ b/test/server.jl @@ -6,6 +6,33 @@ port = conn[1] server = @async lintserver(port) sleep(1) #let server start +function lintbyserver(socket,str) + println(socket, "none") + println(socket, sizeof(str)) # bytes of code + println(socket, str) # code +end + +function readfromserver_old(socket) + response = "" + line = "" + while line != "\n" + response *= line + line = readline(socket) + end + return response +end + +function readfromserver_new(socket) + response = "" + line = "" + while isopen(socket) + response *= line + line = readline(socket) + end + return response +end + + @testset "lintserver() tests" begin conn = connect(port) write(conn, "empty\n") @@ -24,17 +51,11 @@ sleep(1) #let server start end @testset "Testing the lintserver addition" begin - function lintbyserver(socket) - str = """ - test = "Hello" + "World" - """ - println(socket, "none") - println(socket, sizeof(str)) # bytes of code - println(socket, str) # code - end - + str = """ + test = "Hello" + "World" + """ socket = connect(port) - lintbyserver(socket) + lintbyserver(socket, str) response = "" line = "." while !isempty(line) @@ -45,7 +66,7 @@ end @test response == "none:1 E422 : string uses * to concatenate" socket = connect(port) - lintbyserver(socket) + lintbyserver(socket, str) res = "" line = "" while isopen(socket) @@ -56,9 +77,106 @@ end @test res == "none:1 E422 : string uses * to concatenate" end -# This isn't working on the nightly build. Ideally we explicitly stop the server process (as -# it loops forever). It seems to get stopped when the tests end, so it's not necessary. -# -#try # close the server -# Base.throwto(server, InterruptException()) -#end +@testset "Testing lintserver() with named pipe and JSON format" begin + if is_windows() + pipe_lm = "\\\\.\\pipe\\testsocket" + pipe_slv1 = "\\\\.\\pipe\\testsocket2" + pipe_vscode = "\\\\.\\pipe\\testsocket3" + pipe_slv2 = "\\\\.\\pipe\\testsocket4" + else + pipe_lm = tempname() + pipe_slv1 = tempname() + pipe_vscode = tempname() + pipe_slv2 = tempname() + end + + function writeandreadserver(pipe,json_input) + conn = connect(pipe) + JSON.print(conn, json_input) + JSON.parse(conn) + end + + server_lm = @async lintserver(pipe_lm,"lint-message") + server_slv1 = @async lintserver(pipe_slv1,"standard-linter-v1") + server_vscode = @async lintserver(pipe_vscode,"vscode") + server_slv2 = @async lintserver(pipe_slv2,"standard-linter-v2") + sleep(1) + + json_input1 = Dict("file" => "none", "code_str" => "something") + json_input2 = Dict("file" => "none", "code_str" => "pi=3") + json_input3 = Dict("file" => "none", "code_str" => "function a(b)\nend") + + results_array = writeandreadserver(pipe_lm, json_input1) + @test results_array[1]["line"] == 1 + @test results_array[1]["message"] == "use of undeclared symbol" + @test results_array[1]["file"] == "none" + @test results_array[1]["code"] == "E321" + + results_array = writeandreadserver(pipe_slv1, json_input1) + @test results_array[1]["text"] == "E321 something: use of undeclared symbol" + @test results_array[1]["filePath"] == "none" + @test results_array[1]["range"] == Array[[0, 0], [0, 80]] + @test results_array[1]["type"] == "error" + + results_array = writeandreadserver(pipe_slv1, json_input2) + @test results_array[1]["text"] == "W351 pi: redefining mathematical constant" + @test results_array[1]["filePath"] == "none" + @test results_array[1]["range"] == Array[[0, 0], [0, 80]] + @test results_array[1]["type"] == "warning" + + results_array = writeandreadserver(pipe_slv1, json_input3) + @test results_array[1]["text"] == "I382 b: argument declared but not used" + @test results_array[1]["filePath"] == "none" + @test results_array[1]["range"] == Array[[0, 0], [0, 80]] + @test results_array[1]["type"] == "info" + + results_array = writeandreadserver(pipe_vscode, json_input1) + @test results_array[1]["message"] == "something: use of undeclared symbol" + @test results_array[1]["filePath"] == "none" + @test results_array[1]["range"] == Array[[0, 0], [0, 80]] + @test results_array[1]["code"] == "E321" + @test results_array[1]["severity"] == 1 + @test results_array[1]["source"] == "Lint.jl" + + results_array = writeandreadserver(pipe_slv2, json_input1) + @test results_array[1]["description"] == "something: use of undeclared symbol" + @test results_array[1]["location"]["file"] == "none" + @test results_array[1]["location"]["position"] == Array[[0, 0], [0, 80]] + @test results_array[1]["severity"] == "error" + @test results_array[1]["excerpt"] == "E321" + + + json_input4 = Dict("file" => "none", "code_str" => "function a(b)\nend", + "ignore_info" => true) + results_array = writeandreadserver(pipe_lm, json_input4) + @test isempty(results_array) + + json_input5 = Dict("file" => "none", "code_str" => "pi = 1", + "ignore_warnings" => true) + results_array = writeandreadserver(pipe_lm, json_input5) + @test isempty(results_array) + + json_input6 = Dict("file" => "none", + "code_str" => "pi = 1\nfunction a(b)\nend", + "ignore_codes" => ["I382","W351"]) + results_array = writeandreadserver(pipe_lm, json_input6) + @test isempty(results_array) + + json_input7 = Dict("file" => "none", + "code_str" => "pi = 1\nfunction a(b)\nend", + "show_code" => false) + results_array = writeandreadserver(pipe_slv1, json_input7) + @test results_array[1]["text"] == "pi: redefining mathematical constant" + @test results_array[1]["filePath"] == "none" + @test results_array[1]["range"] == Array[[0, 0], [0, 80]] + @test results_array[1]["type"] == "warning" + + json_input8 = Dict("file" => "none", + "code_str" => "pi = 1\nfunction a(b)\nend", + "show_code" => true) + results_array = writeandreadserver(pipe_slv1, json_input8) + @test results_array[1]["text"] == "W351 pi: redefining mathematical constant" + @test results_array[1]["filePath"] == "none" + @test results_array[1]["range"] == Array[[0, 0], [0, 80]] + @test results_array[1]["type"] == "warning" +end