Skip to content
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

Fix #180 by enhancing static type analysis #184

Merged
merged 7 commits into from
Mar 16, 2017
Merged
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: 1 addition & 1 deletion REQUIRE
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
julia 0.5
JSON 0.6.0
Compat 0.15.0
Compat 0.20.0
1 change: 0 additions & 1 deletion docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
`@lintpragma("Ignore unstable type variable [variable name]")` just before the warning.
* Incompatible type assertion and assignment e.g. `a::Int = 1.0`
* Incompatible tuple assignment sizes e.g. `(a,b) = (1,2,3)`
* Flatten behavior of nested vcat e.g. `[[1,2],[3,4]]`
* Loop over a single number. e.g. `for i=1 end`
* More indices than dimensions in an array lookup
* Look up a dictionary with the wrong key type, if the key's type can be inferred.
Expand Down
6 changes: 5 additions & 1 deletion docs/messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ Every error code starts with letter for the severity `E`:`ERROR`, `W`:`WARN` or
| E434 | value at position #i is the referenced x. Possible typo?
| E435 | new is provided with more arguments than fields
| E436 | more indices than dimensions
| E437 | @compat called with wrong number of arguments
| |
| **E5** | *Type Error*
| E511 | apparent non-Bool type
Expand All @@ -103,10 +104,11 @@ Every error code starts with letter for the severity `E`:`ERROR`, `W`:`WARN` or
| E532 | multiple value types detected. Use Dict{K,Any}() for mixed type dict
| E533 | type parameters are invariant, try f{T<:Number}(x::T)...
| E534 | introducing a new name for an implicit argument to the function, use {T<:X}
| E535 | introducing a new name for an algebric data type, use {T<:X}
| E535 | (no longer used)
| E536 | use {T<:...} instead of a known type
| E537 | (removed)
| E538 | known type in parametric data type, use {T<:...}
| E539 | assigning an error to a variable
| |
| **E6** | *Structure Error*
| E611 | constructor doesn't seem to return the constructed object
Expand Down Expand Up @@ -140,6 +142,7 @@ Every error code starts with letter for the severity `E`:`ERROR`, `W`:`WARN` or
| W543 | cannot determine if Type or not
| W544 | cannot determine if Type or not
| W545 | previously used variable has apparent type X, but now assigned Y
| W546 | implicitly discarding values, m of n used
| |
| **W6** | *Structure Warning*
| W641 | unreachable code after return
Expand All @@ -163,6 +166,7 @@ Every error code starts with letter for the severity `E`:`ERROR`, `W`:`WARN` or
| I382 | argument declared but not used
| I391 | also a global from src
| I392 | local variable might cause confusion with a synonymous export from Base
| I393 | using an existing type as type parameter name is probably a typo
| |
| **I4** | *Usage Info*
| I472 | assignment in the if-predicate clause
Expand Down
8 changes: 5 additions & 3 deletions src/Lint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ using Base.Meta
using Compat
using Compat.TypeUtils
using JSON
import Compat: readline

if isdefined(Base, :unwrap_unionall)
using Base: unwrap_unionall
Expand Down Expand Up @@ -140,7 +141,7 @@ function lintfile(file::AbstractString, code::AbstractString)
end

function lintstr{T<:AbstractString}(str::T, ctx::LintContext = LintContext(), lineoffset = 0)
linecharc = cumsum(map(x->endof(x)+1, @compat(split(str, "\n", keep=true))))
linecharc = cumsum(map(x->endof(x)+1, split(str, "\n", keep=true)))
numlines = length(linecharc)
i = start(str)
while !done(str,i)
Expand Down Expand Up @@ -242,6 +243,7 @@ function lintexpr(ex::Expr, ctx::LintContext)
elseif ex.head == :type
linttype(ex, ctx)
elseif ex.head == :typealias
# TODO: deal with X{T} = Y assignments, also const X = Y
linttypealias(ex, ctx)
elseif ex.head == :abstract
lintabstract(ex, ctx)
Expand Down Expand Up @@ -439,9 +441,9 @@ function readandwritethestream(conn,style)
if style == "original_behaviour"
# println("Connection accepted")
# Get file, code length and code
file = strip(readline(conn))
file = readline(conn)
# println("file: ", file)
code_len = parse(Int, strip(readline(conn)))
code_len = parse(Int, readline(conn))
# println("Code bytes: ", code_len)
code = Compat.UTF8String(read(conn, code_len))
# println("Code received")
Expand Down
4 changes: 2 additions & 2 deletions src/controls.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function lintifexpr(ex::Expr, ctx::LintContext)
(verconstraint1, verconstraint2) = versionconstraint(ex.args[1])
if verconstraint1 != nothing
tmpvtest = ctx.versionreachable
ctx.versionreachable = _->(tmpvtest(_) && verconstraint1(_))
ctx.versionreachable = x->(tmpvtest(x) && verconstraint1(x))
end
insideif(x -> lintexpr(ex.args[2], x), ctx)
if verconstraint1 != nothing
Expand All @@ -42,7 +42,7 @@ function lintifexpr(ex::Expr, ctx::LintContext)
if length(ex.args) > 2
if verconstraint2 != nothing
tmpvtest = ctx.versionreachable
ctx.versionreachable = _->(tmpvtest(_) && verconstraint2(_))
ctx.versionreachable = x->(tmpvtest(x) && verconstraint2(x))
end
insideif(x -> lintexpr(ex.args[3], x), ctx)
if verconstraint2 != nothing
Expand Down
21 changes: 20 additions & 1 deletion src/exprutils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ module ExpressionUtils
using Base.Meta

export split_comparison, simplify_literal, ispairexpr, isliteral,
lexicaltypeof, lexicalfirst, lexicallast, lexicalvalue
lexicaltypeof, lexicalfirst, lexicallast, lexicalvalue,
withincurly

# TODO: remove when 0.5 support dropped
function BROADCAST(f, x::Nullable)
Expand All @@ -14,6 +15,24 @@ function BROADCAST(f, x::Nullable)
end
end

"""
withincurly(ex)

Get just the function part of a function declaration, or just the type head of
a parameterized type name.

```jldoctest
julia> using Lint.ExpressionUtils

julia> withincurly(:(Vector{T}))
:Vector

julia> withincurly(:((::Type{T}){T}))
:(::Type{T})
```
"""
withincurly(ex) = isexpr(ex, :curly) ? ex.args[1] : ex

"""
split_comparison(::Expr)

Expand Down
20 changes: 6 additions & 14 deletions src/guesstype.jl
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,11 @@ function guesstype(ex, ctx::LintContext)
end

if isexpr(ex, :call)
# TODO: deal with vararg (...) calls properly
fn = ex.args[1]
if any(x -> isexpr(x, :kw) || isexpr(x, :(...)), ex.args[2:end])
# TODO: smarter way to deal with kw/vararg
return Any
end
argtypes = map(x -> guesstype(x, ctx), ex.args[2:end])

# check if it's a constructor for user-defined type, and figure
Expand Down Expand Up @@ -199,20 +202,9 @@ function guesstype(ex, ctx::LintContext)

# infer return types of Base functions
obj = stdlibobject(fn)
type_argtypes = [isa(t, Type) ? t : Any for t in argtypes]
if !isnull(obj)
inferred = try
typejoin(Base.return_types(
get(obj),
Tuple{(isa(t, Type) ? t : Any for t in argtypes)...})...)
catch # error might be thrown if generic function, try using inference
if all(typ -> isa(typ, Type) && isleaftype(typ), argtypes)
Core.Inference.return_type(
get(obj),
Tuple{argtypes...})
else
Any
end
end
inferred = StaticTypeAnalysis.infertype(get(obj), type_argtypes)
if inferred ≠ Any
return inferred
end
Expand Down
23 changes: 19 additions & 4 deletions src/macros.jl
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,22 @@ end
istopmacro(ex, mod, mac) = ex in (
mac,
GlobalRef(mod, mac),
Expr(:(.), Symbol(string(mod), mac)))
Expr(:(.), Symbol(string(mod)), mac))

function lintcompat(ex::Expr, ctx::LintContext)
if VERSION < v"0.6.0-dev.2746" &&
length(ex.args) == 2 && isexpr(ex.args[2], :abstract) &&
length(ex.args[2].args) == 1 && isexpr(ex.args[2].args[1], :type)
lintexpr(Compat._compat_abstract(ex.args[2].args[1]), ctx)
elseif VERSION < v"0.6.0-dev.2746" &&
length(ex.args) == 3 && ex.args[2] == :primitive
lintexpr(Compat._compat_primitive(ex.args[3]), ctx)
elseif length(ex.args) == 2
lintexpr(Compat._compat(ex.args[2]), ctx)
else
msg(ctx, :E437, ex, "@compat called with wrong number of arguments")
end
end

function lintmacrocall(ex::Expr, ctx::LintContext)
if istopmacro(ex.args[1], Base, Symbol("@deprecate"))
Expand Down Expand Up @@ -93,9 +108,9 @@ function lintmacrocall(ex::Expr, ctx::LintContext)
return
end

if ex.args[1] == Symbol("@compat")
# TODO: check number of arguments
lintexpr(ex.args[2], ctx)
if istopmacro(ex.args[1], Compat, Symbol("@compat"))
lintcompat(ex, ctx)
return
end

if ex.args[1] == Symbol("@gensym")
Expand Down
6 changes: 3 additions & 3 deletions src/modules.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function lintusing(ex::Expr, ctx::LintContext)
register_global(
ctx,
s,
@compat(Dict{Symbol,Any}(:file => ctx.file, :line => ctx.line))
Dict{Symbol,Any}(:file => ctx.file, :line => ctx.line)
)
end
end
Expand All @@ -53,7 +53,7 @@ function lintusing(ex::Expr, ctx::LintContext)
register_global(
ctx,
n,
@compat(Dict{Symbol,Any}(:file => ctx.file, :line => ctx.line))
Dict{Symbol,Any}(:file => ctx.file, :line => ctx.line)
)
end
end
Expand Down Expand Up @@ -103,7 +103,7 @@ function lintimport(ex::Expr, ctx::LintContext; all::Bool = false)
register_global(
ctx,
ex.args[1],
@compat(Dict{Symbol,Any}(:file => ctx.file, :line => ctx.line))
Dict{Symbol,Any}(:file => ctx.file, :line => ctx.line)
)
eval(Main, ex)
lastpart = ex.args[end]
Expand Down
2 changes: 1 addition & 1 deletion src/result.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function Base.show(io::IO, res::LintResult)
show(io, res.messages)
print(io, ")")
end
@compat function Base.show(io::IO, ::MIME"text/plain", res::LintResult)
function Base.show(io::IO, ::MIME"text/plain", res::LintResult)
for m in res.messages
Base.println_with_color(LINT_RESULT_COLORS[level(m)], io, string(m))
end
Expand Down
63 changes: 60 additions & 3 deletions src/statictype.jl
Original file line number Diff line number Diff line change
@@ -1,16 +1,53 @@
module StaticTypeAnalysis

macro lintpragma(ex); end

"""
StaticTypeAnalysis.infertype(f, argtypes)

Given a function `f` and a `Tuple` of types `argtypes`, use inference to figure
out a type `S` such that the result of applying `f` to `argtypes` is always of
type `S`.
"""
function infertype(f, argtypes)
try
typejoin(Base.return_types(f, Tuple{argtypes...})...)
catch # error might be thrown if generic function, try using inference
if all(isleaftype, argtypes)
Core.Inference.return_type(f, Tuple{argtypes...})
else
Any
end
end
end

"""
StaticTypeAnalysis.getindexable(T::Type)

Return `true` if all objects of type `T` support `getindex`, and the `getindex`
operation on numbers is consistent with iteration order.

Note that, in particular, this is not true for `String` and `Dict`.
"""
StaticTypeAnalysis.length(T::Type)
getindexable{T<:Union{Tuple,Pair,Array,Number}}(::Type{T}) = true
getindexable(::Type) = false

"""
StaticTypeAnalysis.length(T::Type) :: Nullable{Int}

If it can be determined that all objects of type `T` have length `n`, then
return `Nullable(n)`. Otherwise, return `Nullable{Int}()`.
"""
length(::Type{Union{}}) = Nullable(0)
length(::Type) = Nullable{Int}()
length{T<:Pair}(::Type{T}) = 2
length{T<:Pair}(::Type{T}) = Nullable(2)

if VERSION < v"0.6.0-dev.2123" # where syntax introduced by julia PR #18457
length{T<:Tuple}(::Type{T}) = Nullable{Int}(Base.length(T.parameters))
length{T<:Tuple}(::Type{T}) = if Core.Inference.isvatuple(T)
Nullable{Int}()
else
Nullable{Int}(Base.length(T.types))
end
else
include_string("""
length(::Type{T}) where T <: NTuple{N, Any} where N = Nullable{Int}(N)
Expand All @@ -26,4 +63,24 @@ element type `S`.
eltype(::Type{Union{}}) = Union{}
eltype(T::Type) = Base.eltype(T)

_getindex_nth{n}(xs::Any, ::Type{Val{n}}) = xs[n]
_typeof_nth_getindex{T}(::Type{T}, n::Integer) =
infertype(_getindex_nth, Any[T, Type{Val{Int(n)}}])

"""
StaticTypeAnalysis.typeof_nth(T::Type)

Return `S` as specific as possible such that all objects of type `T`, when
iterated over, have `n`th element type `S`.
"""
typeof_nth(T::Type, n::Integer) =
if getindexable(T)
typeintersect(eltype(T), _typeof_nth_getindex(T, n))
else
eltype(T)
end
typeof_nth{K,V}(::Type{Pair{K,V}}, n::Integer) =
n == 1 ? K : n == 2 ? V : Union{}
typeof_nth(::Type{Union{}}, ::Integer) = Union{}

end
8 changes: 3 additions & 5 deletions src/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ function linttype(ex::Expr, ctx::LintContext)
end
end
if typefound && adt != :T
msg(ctx, :E535, adt, "introducing a new name for an algebric data " *
"type, use {T<:$(adt)}")
msg(ctx, :I393, adt, "using an existing type as type parameter name is probably a typo")
end
push!(ctx.callstack[end].types, adt)
push!(typeparams, adt)
Expand Down Expand Up @@ -138,10 +137,9 @@ function linttype(ex::Expr, ctx::LintContext)
end

function linttypealias(ex::Expr, ctx::LintContext)
# TODO: make this just part of lintassignment
if isa(ex.args[1], Symbol)
push!(ctx.callstack[end].types, ex.args[1])
elseif isexpr(ex.args[1], :curly)
push!(ctx.callstack[end].types, ex.args[1].args[1])
push!(ctx.callstack[end].types, withincurly(ex.args[1]))
end
end

Expand Down
Loading