Skip to content

Add # typeprof:disable/enable directive for silencing diagnostics #306

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/typeprof/core/ast.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ def self.parse_rb(path, src)
cref = CRef::Toplevel
lenv = LocalEnv.new(path, cref, {}, [])

ProgramNode.new(raw_scope, lenv)
disable_ranges = TypeProf::Diagnostic::DisableDirective::Scanner.collect(result, src)
ProgramNode.new(raw_scope, lenv, disable_ranges: disable_ranges)
end

#: (untyped, TypeProf::Core::LocalEnv, ?bool) -> TypeProf::Core::AST::Node
Expand Down
17 changes: 15 additions & 2 deletions lib/typeprof/core/ast/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -192,20 +192,33 @@ def pretty_print_instance_variables
end

class ProgramNode < Node
def initialize(raw_node, lenv)
def initialize(raw_node, lenv, disable_ranges: [])
super(raw_node, lenv)

@tbl = raw_node.locals
@disable_ranges = disable_ranges
raw_body = raw_node.statements

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

attr_reader :tbl, :body
attr_reader :tbl, :disable_ranges, :body

def subnodes = { body: }
def attrs = { tbl: }

def diagnostics(genv, &blk)
if disable_ranges.empty?
super(genv, &blk)
else
filter = TypeProf::Diagnostic::DisableDirective::Filter.new(disable_ranges)
super(genv) do |diag|
next if filter.skip?(diag.code_range.first.lineno)
blk&.call(diag)
end
end
end

def install0(genv)
@tbl.each {|var| @lenv.locals[var] = Source.new(genv.nil_type) }
@lenv.locals[:"*self"] = lenv.cref.get_self(genv)
Expand Down
3 changes: 3 additions & 0 deletions lib/typeprof/diagnostic.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
require_relative "./diagnostic/disable_directive/scanner"
require_relative "./diagnostic/disable_directive/filter"

module TypeProf
class Diagnostic
def initialize(node, meth, msg, tags: nil)
Expand Down
16 changes: 16 additions & 0 deletions lib/typeprof/diagnostic/disable_directive/filter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module TypeProf
class Diagnostic
module DisableDirective
# Determine which diagnostic ranges should not be reported.
class Filter
def initialize(ranges)
@ranges = ranges
end

def skip?(line)
@ranges.any? { |r| r.cover?(line) }
end
end
end
end
end
66 changes: 66 additions & 0 deletions lib/typeprof/diagnostic/disable_directive/scanner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
module TypeProf
class Diagnostic
module DisableDirective
# Determine which diagnostic ranges should not be reported.
#
# This scanner processes comments in the source code to identify which lines should be excluded from diagnostics.
# It supports both block-level and inline disable/enable comments.
#
# Block-level comments start with `# typeprof:disable` and end with `# typeprof:enable`.
# Inline comments with `# typeprof:disable` exclude diagnostics only for the line containing the comment.
class Scanner
DISABLE_RE = /\s*#\stypeprof:disable$/
ENABLE_RE = /\s*#\stypeprof:enable$/

def self.collect(prism_result, src)
lines = src.lines
comments_by_line = Hash.new { |h, k| h[k] = [] }

prism_result.comments.each do |c|
comments_by_line[c.location.start_line] << c.location.slice
end

ranges = []
current_start = nil

1.upto(lines.size) do |ln|
comment_text = comments_by_line[ln].join(' ')
line_text = lines[ln - 1]

disable = (comment_text =~ DISABLE_RE) || (line_text =~ DISABLE_RE)
enable = (comment_text =~ ENABLE_RE) || (line_text =~ ENABLE_RE)

if current_start # Inside a disable comment block.
if enable # Enable comment found.
ranges << (current_start..ln - 1)
if line_text.strip.start_with?('#') # Block-level enable comment found.
current_start = nil # Close the disable comment block.
else
# Inline enable comment found.
# Exclude lines from the start of the disable comment block up to the current line.
current_start = ln + 1 # Start a new disable comment block on the next line.
end
end
else
# Outside a disable comment block.
next unless disable

if line_text.strip.start_with?('#') # Block-level disable comment found.
current_start = ln + 1 # Disable comment block starts on the next line.
else
# Inline disable comment found.
ranges << (ln..ln) # Exclude only the current line with inline disable.
end
end
end

# If a disable comment block was started but no matching enable comment was found,
# exclude all lines from the start of the disable comment block to the end of the file.
ranges << (current_start..Float::INFINITY) if current_start && current_start <= lines.size

ranges
end
end
end
end
end
11 changes: 11 additions & 0 deletions test/cli_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ def check: -> :ok
END
end

def test_e2e_disable_directive
assert_equal(<<~END, test_run("disable_directive", ["--show-error", "."]))
# TypeProf #{ TypeProf::VERSION }

# ./disable_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 }
Expand Down
41 changes: 41 additions & 0 deletions test/diagnostic/disable_directive/filter_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require_relative '../../helper'

module TypeProf
class Diagnostic
module DisableDirective
class FilterTest < Test::Unit::TestCase
def test_skip_when_line_is_in_range
ranges = [1..3]
filter = Filter.new(ranges)

assert_equal(true, filter.skip?(1))
assert_equal(true, filter.skip?(2))
assert_equal(true, filter.skip?(3))
end

def test_not_ignore_when_line_is_not_in_range
ranges = [2..3]
filter = Filter.new(ranges)

assert_equal(false, filter.skip?(1))
assert_equal(false, filter.skip?(4))
end

def test_with_empty_ranges
filter = Filter.new([])

assert_equal(false, filter.skip?(1))
end

def test_with_infinite_range
ranges = [2..Float::INFINITY]
filter = Filter.new(ranges)

assert_equal(false, filter.skip?(1))
assert_equal(true, filter.skip?(2))
assert_equal(true, filter.skip?(100))
end
end
end
end
end
132 changes: 132 additions & 0 deletions test/diagnostic/disable_directive/scanner_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
require_relative '../../helper'

module TypeProf
class Diagnostic
module DisableDirective
class ScannerTest < Test::Unit::TestCase
def test_when_no_directives
src = <<~RUBY
def foo
x = 1
y = 2
end
RUBY
prism_result = Prism.parse(src)
ranges = Scanner.collect(prism_result, src)

assert_empty ranges
end

def test_when_only_inline_enable_comment
src = <<~RUBY
def foo
x = 1 # typeprof:enable
y = 2
end
RUBY
prism_result = Prism.parse(src)
ranges = Scanner.collect(prism_result, src)

assert_equal 0, ranges.size
end

def test_when_only_inline_disable_comment
src = <<~RUBY
def foo
x = 1 # typeprof:disable
y = 2
end
RUBY
prism_result = Prism.parse(src)
ranges = Scanner.collect(prism_result, src)

assert_equal 1, ranges.size
assert_equal (2..2), ranges[0]
end

def test_when_only_block_disable_comment
src = <<~RUBY
def foo
# typeprof:disable
x = 1
y = 2
end
RUBY
prism_result = Prism.parse(src)
ranges = Scanner.collect(prism_result, src)

assert_equal 1, ranges.size
assert_equal (3..Float::INFINITY), ranges[0]
end

def test_when_only_block_disable_and_enable_comment
src = <<~RUBY
def foo
# typeprof:disable
x = 1
y = 2
# typeprof:enable
z = 3
end
RUBY
prism_result = Prism.parse(src)
ranges = Scanner.collect(prism_result, src)

assert_equal 1, ranges.size
assert_equal (3..4), ranges.first
end

def test_when_inline_disable_comment
src = <<~RUBY
def foo
x = 1 # typeprof:disable
y = 2
end
RUBY
prism_result = Prism.parse(src)
ranges = Scanner.collect(prism_result, src)

assert_equal 1, ranges.size
assert_equal (2..2), ranges[0]
end

def test_when_only_block_disable_and_inline_enable_comment
src = <<~RUBY
def foo
# typeprof:disable
x = 1
y = 2
z = 3 # typeprof:enable
w = 4
end
RUBY
prism_result = Prism.parse(src)
ranges = Scanner.collect(prism_result, src)

assert_equal 2, ranges.size
assert_equal (3..4), ranges[0]
assert_equal (6..Float::INFINITY), ranges[1]
end

def test_when_multiple_comments
src = <<~RUBY
def foo
# typeprof:disable
x = 1
# typeprof:enable
y = 2
z = 3 # typeprof:disable
w = 4
end
RUBY
prism_result = Prism.parse(src)
ranges = Scanner.collect(prism_result, src)

assert_equal 2, ranges.size
assert_equal (3..3), ranges[0]
assert_equal (6..6), ranges[1]
end
end
end
end
end
3 changes: 3 additions & 0 deletions test/fixtures/disable_directive/disable_directive.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def check
Foo.new.accept_int("str") # typeprof:disable
end
3 changes: 3 additions & 0 deletions test/fixtures/disable_directive/disable_directive.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Foo
def accept_int: (Integer) -> :ok
end
28 changes: 28 additions & 0 deletions test/lsp/lsp_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,34 @@ def foo(nnn)
end
end

def test_disable_directive
init("basic")

notify(
"textDocument/didOpen",
textDocument: { uri: @folder + "basic.rb", version: 0, text: <<-END },
def foo(nnn)
nnn
end

foo(1, 2) # typeprof:disable
foo(1, 2)
END
)

expect_request("workspace/codeLens/refresh") {|json| }
expect_notification("textDocument/publishDiagnostics") do |json|
assert_equal([
{
message: "wrong number of arguments (2 for 1)",
range: { start: { line: 5, character: 0 }, end: { line: 5, character: 3 }},
severity: 1,
source: "TypeProf",
}
], json[:diagnostics])
end
end

def test_completion
init("basic")

Expand Down