-
Notifications
You must be signed in to change notification settings - Fork 28
/
Copy pathambiguities.jl
251 lines (225 loc) · 7.94 KB
/
ambiguities.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
"""
test_ambiguities(package::Union{Module, PkgId})
test_ambiguities(packages::Vector{Union{Module, PkgId}})
Test that there is no method ambiguities in given package(s). It
calls `Test.detect_ambiguities` in a separated clean process to avoid
false-positives.
# Keyword Arguments
- `broken::Bool = false`: If true, it uses `@test_broken` instead of
`@test` and shortens the error message.
- `color::Union{Bool, Nothing} = nothing`: Enable/disable colorful
output if a `Bool`. `nothing` (default) means to inherit the
setting in the current process.
- `exclude::AbstractVector = []`: A vector of functions or types to be
excluded from ambiguity testing. A function means to exclude _all_
its methods. A type means to exclude _all_ its methods of the
callable (sometimes also called "functor") and the constructor.
That is to say, `MyModule.MyType` means to ignore ambiguities between
`(::MyType)(x, y::Int)` and `(::MyType)(x::Int, y)`.
- `recursive::Bool = true`: Passed to `Test.detect_ambiguities`.
Note that the default here (`true`) is different from
`detect_ambiguities`. This is for testing ambiguities in methods
defined in all sub-modules.
- Other keyword arguments such as `imported` and `ambiguous_bottom`
are passed to `Test.detect_ambiguities` as-is.
"""
test_ambiguities(packages; kwargs...) = _test_ambiguities(aspkgids(packages); kwargs...)
const ExcludeSpec = Pair{Base.PkgId,String}
strnameof(x) = string(x)
strnameof(x::Type) = string(nameof(x))
rootmodule(x) = rootmodule(parentmodule(x))
rootmodule(m::Module) = Base.require(PkgId(m)) # this handles Base/Core well
normalize_exclude(x::Union{Type,Function}) =
Base.PkgId(rootmodule(x)) => join((fullname(parentmodule(x))..., strnameof(x)), ".")
normalize_exclude(::Any) = error("Only a function and type can be excluded.")
function getobj((pkgid, name)::ExcludeSpec)
nameparts = Symbol.(split(name, "."))
m = Base.require(pkgid)
for name in nameparts
m = getproperty(m, name)
end
return m
end
function normalize_and_check_exclude(exclude::AbstractVector)
exspecs = mapfoldl(normalize_exclude, push!, exclude, init = ExcludeSpec[])
for (spec, obj) in zip(exspecs, exclude)
if getobj(spec) !== obj
error("Name `$(spec[2])` is resolved to a different object.")
end
end
return exspecs::Vector{ExcludeSpec}
end
function reprexclude(exspecs::Vector{ExcludeSpec})
itemreprs = map(exspecs) do (pkgid, name)
string("(", reprpkgid(pkgid), " => ", repr(name), ")")
end
return string("Aqua.ExcludeSpec[", join(itemreprs, ", "), "]")
end
function _test_ambiguities(packages::Vector{PkgId}; broken::Bool = false, kwargs...)
num_ambiguities, strout, strerr =
_find_ambiguities(packages; skipdetails = broken, kwargs...)
print(stderr, strerr)
print(stdout, strout)
if broken
@test_broken iszero(num_ambiguities)
else
@test iszero(num_ambiguities)
end
end
function _find_ambiguities(
packages::Vector{PkgId};
skipdetails::Bool = false,
color::Union{Bool,Nothing} = nothing,
exclude::AbstractVector = [],
# Options to be passed to `Test.detect_ambiguities`:
detect_ambiguities_options...,
)
packages_repr = reprpkgids(collect(packages))
options_repr = checked_repr((; recursive = true, detect_ambiguities_options...))
exclude_repr = reprexclude(normalize_and_check_exclude(exclude))
# Ambiguity test is run inside a clean process.
# https://github.com/JuliaLang/julia/issues/28804
code = """
$(Base.load_path_setup_code())
using Aqua
Aqua.test_ambiguities_impl(
$packages_repr,
$options_repr,
$exclude_repr,
$skipdetails,
) || exit(1)
"""
cmd = Base.julia_cmd()
if something(color, Base.JLOptions().color == 1)
cmd = `$cmd --color=yes`
end
cmd = `$cmd --startup-file=no -e $code`
mktemp() do outfile, out
mktemp() do errfile, err
succ = success(pipeline(cmd; stdout = out, stderr = err))
strout = read(outfile, String)
strerr = read(errfile, String)
num_ambiguities = if succ
0
else
reg_match = match(r"(\d+) ambiguities found", strerr)
reg_match === nothing && error(
"Failed to parse output of `detect_ambiguities`.\nThe stdout was:\n" *
strout *
"\n\nThe stderr was:\n" *
strerr,
)
parse(Int, reg_match.captures[1]::AbstractString)
end
return num_ambiguities, strout, strerr
end
end
end
function reprpkgids(packages::Vector{PkgId})
packages_repr = sprint() do io
println(io, '[')
for pkg in packages
println(io, reprpkgid(pkg))
end
println(io, ']')
end
@assert Base.eval(Main, Meta.parse(packages_repr)) == packages
return packages_repr
end
function reprpkgid(pkg::PkgId)
name = pkg.name
uuid = pkg.uuid
if uuid === nothing
return "Base.PkgId($(repr(name)))"
end
return "Base.PkgId(Base.UUID($(repr(uuid.value))), $(repr(name)))"
end
struct _NoValue end
function getobj(m::Method)
signature = Base.unwrap_unionall(m.sig)
ty = if is_kwcall(signature)
signature.parameters[3]
else
signature.parameters[1]
end
ty = Base.unwrap_unionall(ty)
if ty <: Function
try
return ty.instance # this should work for functions
catch
end
end
try
if ty.name.wrapper === Type
return ty.parameters[1]
else
return ty.name.wrapper
end
catch err
@error(
"Failed to obtain a function from `Method`.",
exception = (err, catch_backtrace())
)
end
return _NoValue()
end
function test_ambiguities_impl(
packages::Vector{PkgId},
options::NamedTuple,
exspecs::Vector{ExcludeSpec},
skipdetails::Bool,
)
modules = map(Base.require, packages)
@debug "Testing method ambiguities" modules
ambiguities = detect_ambiguities(modules...; options...)
if !isempty(exspecs)
exclude_objs = getobj.(exspecs)
ambiguities = filter(ambiguities) do (m1, m2)
getobj(m1) ∉ exclude_objs && getobj(m2) ∉ exclude_objs
end
end
sort!(ambiguities, by = (ms -> (ms[1].name, ms[2].name)))
if !isempty(ambiguities)
printstyled(
stderr,
"$(length(ambiguities)) ambiguities found. To get a list, set `broken = false`.\n";
bold = true,
color = Base.error_color(),
)
end
if !skipdetails
for (i, (m1, m2)) in enumerate(ambiguities)
println(stderr, "Ambiguity #", i)
println(stderr, m1)
println(stderr, m2)
@static if isdefined(Base, :morespecific)
ambiguity_hint(stderr, m1, m2)
println(stderr)
end
println(stderr)
end
end
return isempty(ambiguities)
end
function ambiguity_hint(io::IO, m1::Method, m2::Method)
# based on base/errorshow.jl#showerror_ambiguous
# https://github.com/JuliaLang/julia/blob/v1.7.2/base/errorshow.jl#L327-L353
sigfix = Any
sigfix = typeintersect(m1.sig, sigfix)
sigfix = typeintersect(m2.sig, sigfix)
if isa(Base.unwrap_unionall(sigfix), DataType) && sigfix <: Tuple
let sigfix = sigfix
if all(m -> Base.morespecific(sigfix, m.sig), [m1, m2])
print(io, "\nPossible fix, define\n ")
Base.show_tuple_as_call(io, :function, sigfix)
else
println(io)
print(
io,
"""To resolve the ambiguity, try making one of the methods more specific, or
adding a new method more specific than any of the existing applicable methods.""",
)
end
end
end
end