Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ version = "4.5.1-DEV"
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
CSTParser = "00ebfdb7-1f24-5e51-bd34-a7502290713f"
SymbolServer = "cf896787-08d5-524d-9de7-132aaa0cb996"
CodeTools = "53a63b46-67e4-5edd-8c66-0af0544a99b9"
Copy link
Member

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.

Copy link
Contributor Author

@aminya aminya Jan 5, 2021

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.

Copy link
Member

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-vscode org, or @aminya you could also host it on your account, I'm indifferent on that.

Copy link
Contributor Author

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.

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?

Copy link
Member

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."

Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"

[extras]
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Expand Down
3 changes: 3 additions & 0 deletions src/StaticLint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,7 @@ include("macros.jl")
include("linting/checks.jl")
include("type_inf.jl")
include("utils.jl")
include("modulefiles.jl")
include("lint-api.jl")

end
136 changes: 136 additions & 0 deletions src/lint-api.jl
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!
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

@ZacLN ZacLN Jan 6, 2021

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this lint_file API can work. StaticLint.jl really kind of operates on folders, I think, so not sure how this would fit in.

Copy link
Contributor

Choose a reason for hiding this comment

The 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)
Copy link
Member

Choose a reason for hiding this comment

The 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 modulefiles also makes me nervous, and if we didn't have that API here we wouldn't need that code, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd just use the lint_file function on the Module entry file, so a thin wrapper function.

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
186 changes: 186 additions & 0 deletions src/modulefiles.jl
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