Skip to content

Commit 0fc7cfa

Browse files
mamesmasatoclaude
committed
Add # typeprof:ignore comment to suppress diagnostics
Append `# typeprof:ignore` at the end of a line to suppress any diagnostic whose code range starts on that line. This is useful for code patterns that TypeProf cannot analyze precisely. def check Foo.new.accept_int("str") # typeprof:ignore end The keyword matches Steep's `# steep:ignore` to keep the Ruby type checker ecosystem consistent. The structure (collecting comment line ranges and filtering at ProgramNode#each_diagnostic) is borrowed from #306 which proposed a similar feature with `# typeprof:disable`/`enable`. Co-Authored-By: Masato Sugiyama <public@smasato.net> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d95ac88 commit 0fc7cfa

6 files changed

Lines changed: 89 additions & 3 deletions

File tree

lib/typeprof/core/ast.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,21 @@ def self.parse_rb(path, src)
1616
cref = CRef::Toplevel
1717
lenv = LocalEnv.new(file_context, cref, {}, [])
1818

19-
ProgramNode.new(raw_scope, lenv)
19+
ignore_ranges = collect_ignore_ranges(result)
20+
ProgramNode.new(raw_scope, lenv, ignore_ranges: ignore_ranges)
21+
end
22+
23+
# Collect line ranges marked with `# typeprof:ignore` comments.
24+
# Each range is suppressed in ProgramNode#each_diagnostic.
25+
IGNORE_RE = /\A#\s*typeprof:ignore\s*\z/
26+
def self.collect_ignore_ranges(prism_result)
27+
ranges = []
28+
prism_result.comments.each do |c|
29+
next unless c.location.slice.match?(IGNORE_RE)
30+
line = c.location.start_line
31+
ranges << (line..line)
32+
end
33+
ranges
2034
end
2135

2236
#: (untyped, TypeProf::Core::LocalEnv, ?bool, ?bool) -> TypeProf::Core::AST::Node

lib/typeprof/core/ast/base.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,20 +211,30 @@ def pretty_print_instance_variables
211211
end
212212

213213
class ProgramNode < Node
214-
def initialize(raw_node, lenv)
214+
def initialize(raw_node, lenv, ignore_ranges: [])
215215
super(raw_node, lenv)
216216

217217
@tbl = raw_node.locals
218+
@ignore_ranges = ignore_ranges
218219
raw_body = raw_node.statements
219220

220221
@body = AST.create_node(raw_body, lenv, false)
221222
end
222223

223-
attr_reader :tbl, :body
224+
attr_reader :tbl, :ignore_ranges, :body
224225

225226
def subnodes = { body: }
226227
def attrs = { tbl: }
227228

229+
def each_diagnostic(genv, &blk)
230+
return super if @ignore_ranges.empty?
231+
super(genv) do |diag|
232+
line = diag.code_range&.first&.lineno
233+
next if line && @ignore_ranges.any? { |r| r.cover?(line) }
234+
blk.call(diag)
235+
end
236+
end
237+
228238
def install0(genv)
229239
@tbl.each {|var| @lenv.locals[var] = Source.new(genv.nil_type) }
230240
@lenv.locals[:"*self"] = lenv.cref.get_self(genv)

test/cli_test.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ def check: -> :ok
4747
END
4848
end
4949

50+
def test_e2e_ignore_directive
51+
assert_equal(<<~END, test_run("ignore_directive", ["--show-error", "."]))
52+
# TypeProf #{ TypeProf::VERSION }
53+
54+
# ./ignore_directive.rb
55+
class Object
56+
def check: -> :ok
57+
end
58+
END
59+
end
60+
5061
def test_e2e_syntax_error
5162
assert_equal(<<~END, test_run("syntax_error", ["."]))
5263
# TypeProf #{ TypeProf::VERSION }
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def check
2+
Foo.new.accept_int("str") # typeprof:ignore
3+
end
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class Foo
2+
def accept_int: (Integer) -> :ok
3+
end

test/lsp/lsp_test.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,51 @@ def foo(nnn)
187187
end
188188
end
189189

190+
def test_diagnostics_ignore_directive
191+
init("basic")
192+
193+
notify(
194+
"textDocument/didOpen",
195+
textDocument: { uri: @folder + "basic.rb", version: 0, text: <<-END },
196+
def foo(nnn)
197+
nnn
198+
end
199+
200+
foo(1, 2)
201+
foo(1, 2) # typeprof:ignore
202+
foo(1, 2)
203+
END
204+
)
205+
206+
expect_notification("typeprof.enableToggleButton") {|json| }
207+
expect_request("workspace/codeLens/refresh") {|json| }
208+
expect_notification("textDocument/publishDiagnostics") do |json|
209+
assert_equal({
210+
uri: @folder + "basic.rb",
211+
diagnostics: [
212+
{
213+
message: "wrong number of arguments (2 for 1)",
214+
range: {
215+
start: { line: 4, character: 0 },
216+
end: { line: 4, character: 3 },
217+
},
218+
severity: 1,
219+
source: "TypeProf",
220+
},
221+
{
222+
message: "wrong number of arguments (2 for 1)",
223+
range: {
224+
start: { line: 6, character: 0 },
225+
end: { line: 6, character: 3 },
226+
},
227+
severity: 1,
228+
source: "TypeProf",
229+
}
230+
],
231+
}, json)
232+
end
233+
end
234+
190235
def test_diagnostics2
191236
init("basic")
192237

0 commit comments

Comments
 (0)