diff --git a/lib/typeprof/core/ast.rb b/lib/typeprof/core/ast.rb index 46cbf9275..d1ec4ef8c 100644 --- a/lib/typeprof/core/ast.rb +++ b/lib/typeprof/core/ast.rb @@ -16,7 +16,44 @@ 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. + # + # 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| + text = c.location.slice + line = c.location.start_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 #: (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..a548a7aa4 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -47,6 +47,28 @@ 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_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/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/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 5ef58ec70..ad3fe2bf1 100644 --- a/test/lsp/lsp_test.rb +++ b/test/lsp/lsp_test.rb @@ -187,6 +187,89 @@ 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_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")