Skip to content

Commit 74bd327

Browse files
committed
diagnostic: Add method error diagnostic (inference/method-error)
This commit is certainly quite useful as it does find new errors, but it's still difficult to identify where the errors are coming from, and it's still noisy in the current state. In particular, it even reports type instabilities in the code generated by `@testset`, so I'm not sure what to do about it.
1 parent 41785ee commit 74bd327

File tree

4 files changed

+127
-1
lines changed

4 files changed

+127
-1
lines changed

docs/src/diagnostic.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Here is a summary table of the diagnostics explained in this section:
110110
| [`inference/undef-global-var`](@ref diagnostic/reference/inference/undef-global-var) | `Warning` | `JETLS/save` | References to undefined global variables |
111111
| [`inference/field-error`](@ref diagnostic/reference/inference/field-error) | `Warning` | `JETLS/save` | Access to non-existent struct fields |
112112
| [`inference/bounds-error`](@ref diagnostic/reference/inference/bounds-error) | `Warning` | `JETLS/save` | Out-of-bounds field access by index |
113+
| [`inference/method-error`](@ref diagnostic/inference/method-error) | `Warning` | No matching method found for function calls |
113114
| [`testrunner/test-failure`](@ref diagnostic/reference/testrunner/test-failure) | `Error` | `JETLS/extra` | Test failures from TestRunner integration |
114115

115116
### [Syntax diagnostic (`syntax/*`)](@id diagnostic/reference/syntax)
@@ -607,6 +608,34 @@ function bounds_error(tpl::Tuple{Int})
607608
end
608609
```
609610
611+
#### [Method error (`inference/method-error`)](@id diagnostic/inference/method-error)
612+
613+
**Default severity:** `Warning`
614+
615+
Function calls where no matching method can be found for the inferred argument
616+
types. This diagnostic detects potential `MethodError`s that would occur at
617+
runtime.
618+
619+
Examples:
620+
621+
```julia
622+
function method_error_example()
623+
return sin(1, 2) # no matching method found `sin(::Int64, ::Int64)` (JETLS inference/method-error)
624+
end
625+
```
626+
627+
When multiple union-split signatures fail to find matches, the diagnostic will
628+
report all failed signatures:
629+
630+
```julia
631+
only_int(x::Int) = 2x
632+
633+
function union_split_method_error(x::Union{Int,String})
634+
return only_int(x) # no matching method found `only_int(::String)` (1/2 union split)
635+
# (JETLS inference/method-error)
636+
end
637+
```
638+
610639
### [TestRunner diagnostic (`testrunner/*`)](@id diagnostic/reference/testrunner)
611640
612641
TestRunner diagnostics are reported when you manually run tests via code lens

src/analysis/Analyzer.jl

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module Analyzer
22

33
export LSAnalyzer, inference_error_report_severity, inference_error_report_stack, reset_report_target_modules!
4-
export BoundsErrorReport, FieldErrorReport, UndefVarErrorReport
4+
export BoundsErrorReport, FieldErrorReport, MethodErrorReport, UndefVarErrorReport
55

66
using Core.IR
77
using JET.JETInterface
@@ -234,6 +234,29 @@ end # @static if VERSION ≥ v"1.12.2"
234234
# Analysis injections
235235
# ===================
236236

237+
function CC.abstract_call_gf_by_type(
238+
analyzer::LSAnalyzer, @nospecialize(func), arginfo::CC.ArgInfo, si::CC.StmtInfo,
239+
@nospecialize(atype), sv::CC.InferenceState, max_methods::Int
240+
)
241+
ret = @invoke CC.abstract_call_gf_by_type(analyzer::ToplevelAbstractAnalyzer,
242+
func::Any, arginfo::CC.ArgInfo, si::CC.StmtInfo, atype::Any, sv::CC.InferenceState, max_methods::Int)
243+
if !should_analyze(analyzer, sv)
244+
return ret
245+
end
246+
atype′ = Ref{Any}(atype)
247+
function after_abstract_call_gf_by_type(analyzer′::LSAnalyzer, sv′::CC.InferenceState)
248+
ret′ = ret[]
249+
report_method_error!(analyzer′, sv′, ret′, arginfo, atype′[])
250+
return true
251+
end
252+
if isready(ret)
253+
after_abstract_call_gf_by_type(analyzer, sv)
254+
else
255+
push!(sv.tasks, after_abstract_call_gf_by_type)
256+
end
257+
return ret
258+
end
259+
237260
# TODO Better to factor out and share it with `JET.JETAnalyzer`
238261
function CC.abstract_eval_globalref(
239262
analyzer::LSAnalyzer, g::GlobalRef, saw_latestworld::Bool, sv::CC.InferenceState;
@@ -469,6 +492,76 @@ function report_fieldaccess!(
469492
return true
470493
end
471494

495+
# MethodErrorReport
496+
# -----------------
497+
498+
@jetreport struct MethodErrorReport <: LSErrorReport
499+
@nospecialize t # ::Union{Type, Vector{Type}}
500+
union_split::Int
501+
end
502+
function JETInterface.print_report_message(io::IO, report::MethodErrorReport)
503+
print(io, "no matching method found ")
504+
if report.union_split == 0
505+
print_callsig(io, report.t)
506+
else
507+
ts = report.t::Vector{Any}
508+
nts = length(ts)
509+
for i = 1:nts
510+
print_callsig(io, ts[i])
511+
i == nts || print(io, ", ")
512+
end
513+
print(io, " (", nts, '/', report.union_split, " union split)")
514+
end
515+
end
516+
function print_callsig(io, @nospecialize(t))
517+
print(io, '`')
518+
Base.show_tuple_as_call(io, Symbol(""), t)
519+
print(io, '`')
520+
end
521+
inference_error_report_stack_impl(r::MethodErrorReport) = length(r.vst):-1:1
522+
inference_error_report_severity_impl(::MethodErrorReport) = DiagnosticSeverity.Warning
523+
524+
function report_method_error!(analyzer::LSAnalyzer,
525+
sv::CC.InferenceState, call::CC.CallMeta, arginfo::CC.ArgInfo, @nospecialize(atype))
526+
info = call.info
527+
if isa(info, CC.ConstCallInfo)
528+
info = info.call
529+
end
530+
if isa(info, CC.MethodMatchInfo)
531+
report_method_error!(analyzer, sv, info, atype)
532+
elseif isa(info, CC.UnionSplitInfo)
533+
report_method_error_for_union_split!(analyzer, sv, info, arginfo)
534+
end
535+
end
536+
537+
function report_method_error!(analyzer::LSAnalyzer, sv::CC.InferenceState, info::CC.MethodMatchInfo, @nospecialize(atype))
538+
if CC.isempty(info.results)
539+
report = MethodErrorReport(sv, atype, 0)
540+
add_new_report!(analyzer, sv.result, report)
541+
end
542+
end
543+
544+
function report_method_error_for_union_split!(analyzer::LSAnalyzer, sv::CC.InferenceState, info::CC.UnionSplitInfo, arginfo::CC.ArgInfo)
545+
# check each match for union-split signature
546+
split_argtypes = empty_matches = nothing
547+
for (i, matchinfo) in enumerate(info.split)
548+
if CC.isempty(matchinfo.results)
549+
if isnothing(split_argtypes)
550+
split_argtypes = CC.switchtupleunion(CC.typeinf_lattice(analyzer), arginfo.argtypes)
551+
end
552+
argtypes′ = split_argtypes[i]::Vector{Any}
553+
if empty_matches === nothing
554+
empty_matches = (Any[], length(info.split))
555+
end
556+
sig_n = CC.argtypes_to_type(argtypes′)
557+
push!(empty_matches[1], sig_n)
558+
end
559+
end
560+
if empty_matches !== nothing
561+
add_new_report!(analyzer, sv.result, MethodErrorReport(sv, empty_matches...))
562+
end
563+
end
564+
472565
# Constructor
473566
# ===========
474567

src/diagnostic.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,8 @@ function inference_error_report_code(@nospecialize report::JET.InferenceErrorRep
423423
return INFERENCE_FIELD_ERROR_CODE
424424
elseif report isa BoundsErrorReport
425425
return INFERENCE_BOUNDS_ERROR_CODE
426+
elseif report isa MethodErrorReport
427+
return INFERENCE_METHOD_ERROR_CODE
426428
end
427429
error(lazy"Diagnostic code is not defined for this report: $report")
428430
end

src/types.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ const INFERENCE_UNDEF_GLOBAL_VAR_CODE = "inference/undef-global-var"
398398
const INFERENCE_UNDEF_STATIC_PARAM_CODE = "inference/undef-static-param" # currently not reported
399399
const INFERENCE_FIELD_ERROR_CODE = "inference/field-error"
400400
const INFERENCE_BOUNDS_ERROR_CODE = "inference/bounds-error"
401+
const INFERENCE_METHOD_ERROR_CODE = "inference/method-error"
401402
const TESTRUNNER_TEST_FAILURE_CODE = "testrunner/test-failure"
402403

403404
const ALL_DIAGNOSTIC_CODES = Set{String}(String[
@@ -417,6 +418,7 @@ const ALL_DIAGNOSTIC_CODES = Set{String}(String[
417418
INFERENCE_UNDEF_STATIC_PARAM_CODE,
418419
INFERENCE_FIELD_ERROR_CODE,
419420
INFERENCE_BOUNDS_ERROR_CODE,
421+
INFERENCE_METHOD_ERROR_CODE,
420422
TESTRUNNER_TEST_FAILURE_CODE,
421423
])
422424

0 commit comments

Comments
 (0)