-
Notifications
You must be signed in to change notification settings - Fork 31
feat: add Lint API #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add Lint API #220
Changes from all commits
7c14aab
2f370df
d9448dc
77e2d53
dbbd137
23f6b2b
71b0a47
4d5794d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| export lint_string | ||
| """ | ||
| lint_string(str::AbstractString, root = nothing, server = get_symol_server()[1]) | ||
| Lint the given string and get the result as a CST. | ||
| """ | ||
| function lint_string(str::AbstractString, root = nothing, server = SymbolServer.get_symol_server()[1]) | ||
| # parse | ||
| cst = CSTParser.parse_code(str) | ||
|
|
||
| # TODO: we should not need to write to a file! | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that is definitely key :) LS doesn't write any files, so it can be done, but I'm not familiar enough with the API to help out here.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From https://github.com/julia-vscode/StaticLint.jl/blob/master/test/runtests.jl: using SymbolServer, StaticLint
function parse_and_pass(s)
server = StaticLint.FileServer()
ssi = SymbolServerInstance("/path/to/julia/depot/")
_, server.symbolserver = SymbolServer.getstore(ssi, "/path/to/env")
server.symbol_extends = SymbolServer.collect_extended_methods(server.symbolserver)
f = StaticLint.File("", s, CSTParser.parse(s, true), nothing, server)
StaticLint.setroot(f, f)
StaticLint.setfile(server, "", f)
StaticLint.scopepass(f)
StaticLint.check_all(f.cst, StaticLint.LintOptions(), server)
return f.cst
end |
||
|
|
||
| # Write to file | ||
| filepath = mktemp()[1] | ||
| Base.write(filepath, str) | ||
|
|
||
| # File | ||
| file = StaticLint.File(filepath, str, cst, root, server) | ||
|
|
||
| # Get bindings and scopes | ||
| StaticLint.scopepass(file) | ||
|
|
||
| return file.cst | ||
| end | ||
|
|
||
|
|
||
| """ | ||
| isjuliafile(filepath::AbstractString) | ||
| return true if it is a julia file. | ||
| """ | ||
| function isjuliafile(filepath::AbstractString) | ||
| ext = splitext(filepath)[2] | ||
| return ext == ".jl" | ||
| end | ||
|
|
||
| export lint_file | ||
| """ | ||
| lint_file(filepath::AbstractString, root = nothing, server = SymbolServer.get_symbol_server()[1]) | ||
| Lint the given file and get the result as a StaticLint.File | ||
| """ | ||
| function lint_file(filepath::AbstractString, root = nothing, server = SymbolServer.get_symbol_server()[1]) | ||
|
|
||
| # Check extention | ||
| if !isjuliafile(filepath) | ||
| error("$filepath is not a julia file.") | ||
| end | ||
|
|
||
| # read | ||
| str = Base.read(filepath, String) | ||
|
|
||
| # parse_code | ||
| cst = CSTParser.parse_code(str) | ||
|
|
||
| # File | ||
| file = StaticLint.File(filepath, str, cst, root, server) | ||
|
|
||
| # Get bindings and scopes | ||
| StaticLint.scopepass(file) | ||
|
|
||
| return file | ||
| end | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. using SymbolServer, StaticLint
function parse_and_pass(path)
server = StaticLint.FileServer()
ssi = SymbolServerInstance("/path/to/julia/depot/")
_, server.symbolserver = SymbolServer.getstore(ssi, "/path/to/env")
server.symbol_extends = SymbolServer.collect_extended_methods(server.symbolserver)
f = StaticLint.loadfile(server, path)
StaticLint.scopepass(f)
StaticLint.check_all(f.cst, StaticLint.LintOptions(), server)
return f.cst
end |
||
|
|
||
|
|
||
| using Pkg: develop | ||
| using CodeTools: getmodule | ||
|
|
||
| export lint_module | ||
| """ | ||
| lint_module(modul::Module) | ||
| lint_module(module_name::String) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am also not sure whether that API makes sense? I think we should probably limit the API to linting files and folders in some way. The whole code around
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd just use the |
||
| lint a module and get the result as a StaticLint.File | ||
| # Examples | ||
| ```julia | ||
| using Test1 | ||
| fileserver = lint_module(Test1) | ||
| ``` | ||
| ```julia | ||
| fileserver = lint_module("./test") | ||
| ``` | ||
| """ | ||
| function lint_module(path::AbstractString) | ||
| module_name, modul_path = module_name_root(path) | ||
| Pkg.develop(path=modul_path) | ||
| modul = CodeTools.getmodule(module_name) | ||
| if isnothing(modul) | ||
| module_sym = Symbol(module_name) | ||
| @eval using $module_sym # will throw error for Pkg if not available | ||
| modul = CodeTools.getmodule(module_name) | ||
| end | ||
| return lint_module(modul) | ||
| end | ||
|
|
||
| # Since the module is loaded it will give the modulefiles easily | ||
| function lint_module(modul::Module, env_path = dirname(pathof(modul))) | ||
| symbolserver, symbol_extends = SymbolServer.get_symbol_server() | ||
| parentfile_path, included_files_path = modulefiles(modul) | ||
|
|
||
| root = lint_module_file(parentfile_path, nothing, symbolserver) | ||
|
|
||
| included_files_path_len = length(included_files_path) | ||
| files = Vector{File}(undef, included_files_path_len) | ||
| for ifile = 1:included_files_path_len | ||
| files[i] = lint_module_file(included_files_path[i], root, symbolserver) | ||
| end | ||
|
|
||
| allfiles_path = prepend!(included_files_path, [parentfile_path]) | ||
| allfiles = prepend!(files, [root]) | ||
|
|
||
| files = Dict( file_path => file for (file_path, file) in zip(allfiles_path, allfiles)) | ||
| roots = Set([root]) | ||
|
|
||
| return FileServer(files, roots, symbolserver, symbol_extends) | ||
| end | ||
|
|
||
| export lint_module_file | ||
| function lint_module_file(filepath::AbstractString, root = nothing, server = nothing) | ||
|
|
||
| # Check extention | ||
| if !isjuliafile(filepath) | ||
| error("$filepath is not a julia file.") | ||
| end | ||
|
|
||
| # read | ||
| str = Base.read(filepath, String) | ||
|
|
||
| # parse_code | ||
| cst = CSTParser.parse_code(str) | ||
|
|
||
| # File | ||
| return StaticLint.File(filepath, str, cst, root, server) | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| # code licensed under MIT from https://github.com/JunoLab/Atom.jl/blob/b42ce6519e96b59fd3c7172a6599dcc50a051c90/src/modules.jl#L26 | ||
|
|
||
| export modulefiles | ||
|
|
||
| using Base: PkgId, UUID | ||
|
|
||
| """ | ||
| parentfile, included_files = modulefiles(mod::Module) | ||
| Return the `parentfile` in which `mod` was defined, as well as a list of any | ||
| other files that were `include`d to define `mod`. If this operation is unsuccessful, | ||
| `(nothing, nothing)` is returned. | ||
| All files are returned as absolute paths. | ||
| """ | ||
| function modulefiles(mod::Module) | ||
| # NOTE: src_file_key stuff was removed when adapted | ||
| parentfile = String(first(methods(getfield(mod, :eval))).file) | ||
| id = Base.PkgId(mod) | ||
| if id.name == "Base" || Symbol(id.name) ∈ stdlib_names | ||
| parentfile = normpath(Base.find_source_file(parentfile)) | ||
| filedata = Base._included_files | ||
| else | ||
| use_compiled_modules() || return nothing, nothing # FIXME: support non-precompiled packages | ||
| _, filedata = pkg_fileinfo(id) | ||
| end | ||
| filedata === nothing && return nothing, nothing | ||
| included_files = filter(mf -> mf[1] == mod, filedata) | ||
| return fixpath(parentfile), String[fixpath(mf[2]) for mf in included_files] | ||
| end | ||
|
|
||
| # Fix paths to files that define Julia (base and stdlibs) | ||
| function fixpath( | ||
| filename::AbstractString; | ||
| badpath = basebuilddir, | ||
| goodpath = basepath("..") | ||
| ) | ||
| startswith(filename, badpath) || return fullpath(normpath(filename)) # NOTE: `fullpath` added when adapted | ||
| filec = filename | ||
| relfilename = relpath(filename, badpath) | ||
| relfilename0 = relfilename | ||
| for strippath in (joinpath("usr", "share", "julia"),) | ||
| if startswith(relfilename, strippath) | ||
| relfilename = relpath(relfilename, strippath) | ||
| if occursin("stdlib", relfilename0) && !occursin("stdlib", relfilename) | ||
| relfilename = joinpath("stdlib", relfilename) | ||
| end | ||
| end | ||
| end | ||
| ffilename = normpath(joinpath(goodpath, relfilename)) | ||
| if (isfile(filename) & !isfile(ffilename)) | ||
| ffilename = normpath(filename) | ||
| end | ||
| fullpath(ffilename) # NOTE: `fullpath` added when adapted | ||
| end | ||
|
|
||
| """ | ||
| basebuilddir | ||
| Julia's top-level directory when Julia was built, as recorded by the entries in | ||
| `Base._included_files`. | ||
| """ | ||
| const basebuilddir = let # NOTE: changed from `begin` to `let` when adapted | ||
| sysimg = filter(x -> endswith(x[2], "sysimg.jl"), Base._included_files)[1][2] | ||
| dirname(dirname(sysimg)) | ||
| end | ||
|
|
||
| use_compiled_modules() = Base.JLOptions().use_compiled_modules != 0 | ||
|
|
||
| # For tracking Julia's own stdlibs | ||
| const stdlib_names = Set([ | ||
| :Base64, | ||
| :CRC32c, | ||
| :Dates, | ||
| :DelimitedFiles, | ||
| :Distributed, | ||
| :FileWatching, | ||
| :Future, | ||
| :InteractiveUtils, | ||
| :Libdl, | ||
| :LibGit2, | ||
| :LinearAlgebra, | ||
| :Logging, | ||
| :Markdown, | ||
| :Mmap, | ||
| :OldPkg, | ||
| :Pkg, | ||
| :Printf, | ||
| :Profile, | ||
| :Random, | ||
| :REPL, | ||
| :Serialization, | ||
| :SHA, | ||
| :SharedArrays, | ||
| :Sockets, | ||
| :SparseArrays, | ||
| :Statistics, | ||
| :SuiteSparse, | ||
| :Test, | ||
| :Unicode, | ||
| :UUIDs, | ||
| ]) | ||
| @static VERSION ≥ v"1.6.0-DEV.734" && push!(stdlib_names, :TOML) | ||
|
|
||
| function pkg_fileinfo(id::PkgId) | ||
| uuid, name = id.uuid, id.name | ||
| # Try to find the matching cache file | ||
| paths = Base.find_all_in_cache_path(id) | ||
| sourcepath = Base.locate_package(id) | ||
| if length(paths) > 1 | ||
| fpaths = filter(path->Base.stale_cachefile(sourcepath, path) !== true, paths) | ||
| if isempty(fpaths) | ||
| # Work-around for #371 (broken dependency prevents tracking): | ||
| # find the most recent cache file. Presumably this is the one built | ||
| # to load the package. | ||
| sort!(paths; by=path->mtime(path), rev=true) | ||
| deleteat!(paths, 2:length(paths)) | ||
| else | ||
| paths = fpaths | ||
| end | ||
| end | ||
| isempty(paths) && return nothing, nothing | ||
| @assert length(paths) == 1 | ||
| path = first(paths) | ||
| provides, includes_requires = try | ||
| parse_cache_header(path) | ||
| catch | ||
| return nothing, nothing | ||
| end | ||
| mods_files_mtimes, _ = includes_requires | ||
| for (pkgid, buildid) in provides | ||
| if pkgid.uuid === uuid && pkgid.name == name | ||
| return path, mods_files_mtimes | ||
| end | ||
| end | ||
| end | ||
|
|
||
| # A near-copy of the same method in `base/loading.jl`. However, this retains the full module path to the file. | ||
| function parse_cache_header(f::IO) | ||
| modules = Vector{Pair{PkgId,UInt64}}() | ||
| while true | ||
| n = read(f, Int32) | ||
| n == 0 && break | ||
| sym = String(read(f, n)) # module name | ||
| uuid = UUID((read(f, UInt64), read(f, UInt64))) # pkg UUID | ||
| build_id = read(f, UInt64) # build UUID (mostly just a timestamp) | ||
| push!(modules, PkgId(uuid, sym) => build_id) | ||
| end | ||
| totbytes = read(f, Int64) # total bytes for file dependencies | ||
| # read the list of requirements | ||
| # and split the list into include and requires statements | ||
| includes = Tuple{Module,String,Float64}[] | ||
| requires = Pair{Module,PkgId}[] | ||
| while true | ||
| n2 = read(f, Int32) | ||
| n2 == 0 && break | ||
| depname = String(read(f, n2)) | ||
| mtime = read(f, Float64) | ||
| n1 = read(f, Int32) | ||
| mod = (n1 == 0) ? Main : Base.root_module(modules[n1].first) | ||
| if n1 != 0 | ||
| # determine the complete module path | ||
| while true | ||
| n1 = read(f, Int32) | ||
| totbytes -= 4 | ||
| n1 == 0 && break | ||
| submodname = String(read(f, n1)) | ||
| mod = getfield(mod, Symbol(submodname)) | ||
| totbytes -= n1 | ||
| end | ||
| end | ||
| if depname[1] != '\0' | ||
| push!(includes, (mod, depname, mtime)) | ||
| end | ||
| totbytes -= 4 + 4 + n2 + 8 | ||
| end | ||
| @assert totbytes == 12 "header of cache file appears to be corrupt" | ||
| return modules, (includes, requires) | ||
| end | ||
|
|
||
| function parse_cache_header(cachefile::String) | ||
| io = open(cachefile, "r") | ||
| try | ||
| !Base.isvalid_cache_header(io) && throw(ArgumentError("Invalid header in cache file $cachefile.")) | ||
| return parse_cache_header(io) | ||
| finally | ||
| close(io) | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't take a dependency on that, it pulls in too many other packages.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can move my PRs to this organization to a different package if you think you can't add a dependency, or the codes make you nervous.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment was really narrowly talking about CodeTools.jl: I think that would pull at least five new packages in, and because we ship all packages manually for the VS Code extension that would add just too much extra work to keep all of that updated and maintained.
BUT, having said that: I think maybe having a new package that is user facing and exposes a simple API for LanguageServer, SymbolServer, StaticLint etc. might actually be the best design. Such a package could really primarily target REPL users and make things most convenient for them. It could also easily take on any additional dependencies that are not needed for the VS Code extension. Plus, you wouldn't have to deal with the bottleneck of our snail paced review times ;)
The general idea then would be that packages like StaticLint, SymbolServer, LanguageServer etc. are really targeting IDE integration authors, are low level and not user friendly, with a minimal API surface. And (your) new package would expose things for REPL/end users.
@ZacLN and @aminya does that make sense to you? We could actually host such a package here in the
julia-vscodeorg, or @aminya you could also host it on your account, I'm indifferent on that.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's OK. I can host a Lint.jl package on my account.
The old Lint.jl is not registered anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds like a good idea! @aminya could you ping me when you create it or if you need any help on it? I'd be lively interested in it.
@davidanthoff when there's a separate package for standalone linter, maybe this could be mentioned in Readme here as well so it would be easier for people looking for such functionality to find it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we could definitely change the README here to essentially say "This is a low-level package, go to Lint.jl for something that most users would want to use."