From 0fc7cfa43a0e8191ac50073179a4ae683fc1f03c Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Tue, 7 Apr 2026 15:50:09 +0900 Subject: [PATCH 1/2] 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 Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/typeprof/core/ast.rb | 16 ++++++- lib/typeprof/core/ast/base.rb | 14 +++++- test/cli_test.rb | 11 +++++ .../ignore_directive/ignore_directive.rb | 3 ++ .../ignore_directive/ignore_directive.rbs | 3 ++ test/lsp/lsp_test.rb | 45 +++++++++++++++++++ 6 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/ignore_directive/ignore_directive.rb create mode 100644 test/fixtures/ignore_directive/ignore_directive.rbs diff --git a/lib/typeprof/core/ast.rb b/lib/typeprof/core/ast.rb index 46cbf9275..b52d8891c 100644 --- a/lib/typeprof/core/ast.rb +++ b/lib/typeprof/core/ast.rb @@ -16,7 +16,21 @@ def self.parse_rb(path, src) cref = CRef::Toplevel lenv = LocalEnv.new(file_context, cref, {}, []) - ProgramNode.new(raw_scope, lenv) + ignore_ranges = collect_ignore_ranges(result) + ProgramNode.new(raw_scope, lenv, ignore_ranges: ignore_ranges) + end + + # Collect line ranges marked with `# typeprof:ignore` comments. + # Each range is suppressed in ProgramNode#each_diagnostic. + IGNORE_RE = /\A#\s*typeprof:ignore\s*\z/ + def self.collect_ignore_ranges(prism_result) + ranges = [] + prism_result.comments.each do |c| + next unless c.location.slice.match?(IGNORE_RE) + line = c.location.start_line + ranges << (line..line) + end + ranges end #: (untyped, TypeProf::Core::LocalEnv, ?bool, ?bool) -> TypeProf::Core::AST::Node diff --git a/lib/typeprof/core/ast/base.rb b/lib/typeprof/core/ast/base.rb index cd1fbf8dd..b53d80035 100644 --- a/lib/typeprof/core/ast/base.rb +++ b/lib/typeprof/core/ast/base.rb @@ -211,20 +211,30 @@ def pretty_print_instance_variables end class ProgramNode < Node - def initialize(raw_node, lenv) + def initialize(raw_node, lenv, ignore_ranges: []) super(raw_node, lenv) @tbl = raw_node.locals + @ignore_ranges = ignore_ranges raw_body = raw_node.statements @body = AST.create_node(raw_body, lenv, false) end - attr_reader :tbl, :body + attr_reader :tbl, :ignore_ranges, :body def subnodes = { body: } def attrs = { tbl: } + def each_diagnostic(genv, &blk) + return super if @ignore_ranges.empty? + super(genv) do |diag| + line = diag.code_range&.first&.lineno + next if line && @ignore_ranges.any? { |r| r.cover?(line) } + blk.call(diag) + end + end + def install0(genv) @tbl.each {|var| @lenv.locals[var] = Source.new(genv.nil_type) } @lenv.locals[:"*self"] = lenv.cref.get_self(genv) diff --git a/test/cli_test.rb b/test/cli_test.rb index 35b64ec5b..78632a969 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -47,6 +47,17 @@ def check: -> :ok END end + def test_e2e_ignore_directive + assert_equal(<<~END, test_run("ignore_directive", ["--show-error", "."])) + # TypeProf #{ TypeProf::VERSION } + + # ./ignore_directive.rb + class Object + def check: -> :ok + end + END + end + def test_e2e_syntax_error assert_equal(<<~END, test_run("syntax_error", ["."])) # TypeProf #{ TypeProf::VERSION } diff --git a/test/fixtures/ignore_directive/ignore_directive.rb b/test/fixtures/ignore_directive/ignore_directive.rb new file mode 100644 index 000000000..425b42527 --- /dev/null +++ b/test/fixtures/ignore_directive/ignore_directive.rb @@ -0,0 +1,3 @@ +def check + Foo.new.accept_int("str") # typeprof:ignore +end diff --git a/test/fixtures/ignore_directive/ignore_directive.rbs b/test/fixtures/ignore_directive/ignore_directive.rbs new file mode 100644 index 000000000..e21a5690e --- /dev/null +++ b/test/fixtures/ignore_directive/ignore_directive.rbs @@ -0,0 +1,3 @@ +class Foo + def accept_int: (Integer) -> :ok +end diff --git a/test/lsp/lsp_test.rb b/test/lsp/lsp_test.rb index 5ef58ec70..4897d74b3 100644 --- a/test/lsp/lsp_test.rb +++ b/test/lsp/lsp_test.rb @@ -187,6 +187,51 @@ def foo(nnn) end end + def test_diagnostics_ignore_directive + init("basic") + + notify( + "textDocument/didOpen", + textDocument: { uri: @folder + "basic.rb", version: 0, text: <<-END }, +def foo(nnn) + nnn +end + +foo(1, 2) +foo(1, 2) # typeprof:ignore +foo(1, 2) + END + ) + + expect_notification("typeprof.enableToggleButton") {|json| } + expect_request("workspace/codeLens/refresh") {|json| } + expect_notification("textDocument/publishDiagnostics") do |json| + assert_equal({ + uri: @folder + "basic.rb", + diagnostics: [ + { + message: "wrong number of arguments (2 for 1)", + range: { + start: { line: 4, character: 0 }, + end: { line: 4, character: 3 }, + }, + severity: 1, + source: "TypeProf", + }, + { + message: "wrong number of arguments (2 for 1)", + range: { + start: { line: 6, character: 0 }, + end: { line: 6, character: 3 }, + }, + severity: 1, + source: "TypeProf", + } + ], + }, json) + end + end + def test_diagnostics2 init("basic") From c4c569298918c2479b16a2d9b48832fb6a9e8192 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Tue, 7 Apr 2026 15:50:48 +0900 Subject: [PATCH 2/2] Support # typeprof:ignore:start / :end block form For suppressing diagnostics across multiple lines, support a block form bracketed by `# typeprof:ignore:start` and `# typeprof:ignore:end` comments: # typeprof:ignore:start foo(1, 2) bar(1, 2) # typeprof:ignore:end An unmatched `:start` extends to the end of the file. The keywords follow Steep's `# steep:ignore:start`/`:end` convention. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/typeprof/core/ast.rb | 29 ++++++++++++-- test/cli_test.rb | 11 ++++++ .../ignore_directive_block.rb | 6 +++ .../ignore_directive_block.rbs | 3 ++ test/lsp/lsp_test.rb | 38 +++++++++++++++++++ 5 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/ignore_directive_block/ignore_directive_block.rb create mode 100644 test/fixtures/ignore_directive_block/ignore_directive_block.rbs diff --git a/lib/typeprof/core/ast.rb b/lib/typeprof/core/ast.rb index b52d8891c..d1ec4ef8c 100644 --- a/lib/typeprof/core/ast.rb +++ b/lib/typeprof/core/ast.rb @@ -22,14 +22,37 @@ def self.parse_rb(path, src) # Collect line ranges marked with `# typeprof:ignore` comments. # Each range is suppressed in ProgramNode#each_diagnostic. - IGNORE_RE = /\A#\s*typeprof:ignore\s*\z/ + # + # Inline form (suppresses the line containing the comment): + # foo(1, 2) # typeprof:ignore + # + # Block form (suppresses lines between :start and :end): + # # typeprof:ignore:start + # foo(1, 2) + # # typeprof:ignore:end + # + # An unmatched `:start` extends to the end of the file. + IGNORE_RE = /\A#\s*typeprof:ignore\s*\z/ + IGNORE_START_RE = /\A#\s*typeprof:ignore:start\s*\z/ + IGNORE_END_RE = /\A#\s*typeprof:ignore:end\s*\z/ def self.collect_ignore_ranges(prism_result) ranges = [] + start_line = nil prism_result.comments.each do |c| - next unless c.location.slice.match?(IGNORE_RE) + text = c.location.slice line = c.location.start_line - ranges << (line..line) + if text.match?(IGNORE_START_RE) + start_line ||= line + elsif text.match?(IGNORE_END_RE) + if start_line + ranges << (start_line..line) + start_line = nil + end + elsif text.match?(IGNORE_RE) + ranges << (line..line) + end end + ranges << (start_line..Float::INFINITY) if start_line ranges end diff --git a/test/cli_test.rb b/test/cli_test.rb index 78632a969..a548a7aa4 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -58,6 +58,17 @@ def check: -> :ok END end + def test_e2e_ignore_directive_block + assert_equal(<<~END, test_run("ignore_directive_block", ["--show-error", "."])) + # TypeProf #{ TypeProf::VERSION } + + # ./ignore_directive_block.rb + class Object + def check: -> :ok + end + END + end + def test_e2e_syntax_error assert_equal(<<~END, test_run("syntax_error", ["."])) # TypeProf #{ TypeProf::VERSION } diff --git a/test/fixtures/ignore_directive_block/ignore_directive_block.rb b/test/fixtures/ignore_directive_block/ignore_directive_block.rb new file mode 100644 index 000000000..b80c8d8b8 --- /dev/null +++ b/test/fixtures/ignore_directive_block/ignore_directive_block.rb @@ -0,0 +1,6 @@ +def check + # typeprof:ignore:start + Foo.new.accept_int("str") + Foo.new.accept_int("str") + # typeprof:ignore:end +end diff --git a/test/fixtures/ignore_directive_block/ignore_directive_block.rbs b/test/fixtures/ignore_directive_block/ignore_directive_block.rbs new file mode 100644 index 000000000..e21a5690e --- /dev/null +++ b/test/fixtures/ignore_directive_block/ignore_directive_block.rbs @@ -0,0 +1,3 @@ +class Foo + def accept_int: (Integer) -> :ok +end diff --git a/test/lsp/lsp_test.rb b/test/lsp/lsp_test.rb index 4897d74b3..ad3fe2bf1 100644 --- a/test/lsp/lsp_test.rb +++ b/test/lsp/lsp_test.rb @@ -232,6 +232,44 @@ def foo(nnn) end end + def test_diagnostics_ignore_directive_block + init("basic") + + notify( + "textDocument/didOpen", + textDocument: { uri: @folder + "basic.rb", version: 0, text: <<-END }, +def foo(nnn) + nnn +end + +# typeprof:ignore:start +foo(1, 2) +foo(1, 2) +# typeprof:ignore:end +foo(1, 2) + END + ) + + expect_notification("typeprof.enableToggleButton") {|json| } + expect_request("workspace/codeLens/refresh") {|json| } + expect_notification("textDocument/publishDiagnostics") do |json| + assert_equal({ + uri: @folder + "basic.rb", + diagnostics: [ + { + message: "wrong number of arguments (2 for 1)", + range: { + start: { line: 8, character: 0 }, + end: { line: 8, character: 3 }, + }, + severity: 1, + source: "TypeProf", + } + ], + }, json) + end + end + def test_diagnostics2 init("basic")