Skip to content

Commit

Permalink
Merge pull request #422 from soutaro/diagnostics-options
Browse files Browse the repository at this point in the history
Flexible diagnostics configuration
  • Loading branch information
soutaro committed Aug 29, 2021
2 parents c47a969 + df41719 commit b3c65f0
Show file tree
Hide file tree
Showing 60 changed files with 455 additions and 158 deletions.
6 changes: 6 additions & 0 deletions lib/steep/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ def process_check
opts.on("--save-expectations[=PATH]", "Save expectations with current type check result to PATH (or steep_expectations.yml)") do |path|
check.save_expectations_path = Pathname(path || "steep_expectations.yml")
end
opts.on("--severity-level=LEVEL", /^error|warning|information|hint$/, "Specify the minimum diagnostic severity to be recognized as an error (defaults: warning): error, warning, information, or hint") do |level|
check.severity_level = level.to_sym
end
handle_jobs_option check, opts
handle_logging_options opts
end.parse!(argv)
Expand Down Expand Up @@ -155,6 +158,9 @@ def process_watch
Drivers::Watch.new(stdout: stdout, stderr: stderr).tap do |command|
OptionParser.new do |opts|
opts.banner = "Usage: steep watch [options] [dirs]"
opts.on("--severity-level=LEVEL", /^error|warning|information|hint$/, "Specify the minimum diagnostic severity to be recognized as an error (defaults: warning): error, warning, information, or hint") do |level|
command.severity_level = level.to_sym
end
handle_jobs_option command, opts
handle_logging_options opts
end.parse!(argv)
Expand Down
65 changes: 59 additions & 6 deletions lib/steep/diagnostic/lsp_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,66 @@ module Diagnostic
class LSPFormatter
LSP = LanguageServer::Protocol

attr_reader :config
attr_reader :default_severity

ERROR = :error
WARNING = :warning
INFORMATION = :information
HINT = :hint

def initialize(config = {}, default_severity: ERROR)
@config = config
@default_severity = default_severity

config.each do |klass, severity|
validate_severity(klass, severity)
validate_class(klass)
end
validate_severity(:default, default_severity)
end

def validate_class(klass)
unless klass < Diagnostic::Ruby::Base
raise "Unexpected diagnostics class `#{klass}` given"
end
end

def validate_severity(klass, severity)
case severity
when ERROR, WARNING, INFORMATION, HINT, nil
# ok
else
raise "Unexpected severity `#{severity}` is specified for #{klass}"
end
end

def format(diagnostic)
LSP::Interface::Diagnostic.new(
message: diagnostic.full_message,
code: diagnostic.diagnostic_code,
severity: LSP::Constant::DiagnosticSeverity::ERROR,
range: diagnostic.location.as_lsp_range
).to_hash
severity = severity_for(diagnostic)

if severity
LSP::Interface::Diagnostic.new(
message: diagnostic.full_message,
code: diagnostic.diagnostic_code,
severity: severity,
range: diagnostic.location.as_lsp_range
).to_hash
end
end

def severity_for(diagnostic)
case config.fetch(diagnostic.class, default_severity)
when ERROR
LSP::Constant::DiagnosticSeverity::ERROR
when WARNING
LSP::Constant::DiagnosticSeverity::WARNING
when INFORMATION
LSP::Constant::DiagnosticSeverity::INFORMATION
when HINT
LSP::Constant::DiagnosticSeverity::HINT
when nil
nil
end
end
end
end
Expand Down
51 changes: 51 additions & 0 deletions lib/steep/diagnostic/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,57 @@ def header_line
"SyntaxError: #{message}"
end
end

ALL = ObjectSpace.each_object(Class).with_object([]) do |klass, array|
if klass < Base
array << klass
end
end

def self.all_error
@all_error ||= ALL.each.with_object({}) do |klass, hash|
hash[klass] = LSPFormatter::ERROR
end.freeze
end

def self.default
@default ||= all_error.merge(
{
ImplicitBreakValueMismatch => :warning,
FallbackAny => :information,
ElseOnExhaustiveCase => :warning,
UnknownConstantAssigned => :warning,
MethodDefinitionMissing => :information
}
).freeze
end

def self.strict
@strict ||= all_error.merge(
{
NoMethod => nil,
ImplicitBreakValueMismatch => nil,
FallbackAny => nil,
ElseOnExhaustiveCase => nil,
UnknownConstantAssigned => nil,
MethodDefinitionMissing => nil
}
).freeze
end

def self.lenient
@lenient ||= all_error.merge(
{
NoMethod => nil,
ImplicitBreakValueMismatch => nil,
FallbackAny => nil,
ElseOnExhaustiveCase => nil,
UnknownConstantAssigned => nil,
MethodDefinitionMissing => nil,
UnexpectedJump => nil
}
).freeze
end
end
end
end
3 changes: 3 additions & 0 deletions lib/steep/drivers/check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Check
attr_reader :command_line_patterns
attr_accessor :with_expectations_path
attr_accessor :save_expectations_path
attr_accessor :severity_level

include Utils::DriverHelper
include Utils::JobsCount
Expand All @@ -16,6 +17,7 @@ def initialize(stdout:, stderr:)
@stdout = stdout
@stderr = stderr
@command_line_patterns = []
@severity_level = :warning
end

def run
Expand Down Expand Up @@ -70,6 +72,7 @@ def run
case
when response[:method] == "textDocument/publishDiagnostics"
ds = response[:params][:diagnostics]
ds.select! {|d| keep_diagnostic?(d) }
if ds.empty?
stdout.print "."
else
Expand Down
13 changes: 10 additions & 3 deletions lib/steep/drivers/init.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ class Init
include Utils::DriverHelper

TEMPLATE = <<~EOF
# D = Steep::Diagnostic
#
# target :lib do
# signature "sig"
#
Expand All @@ -18,15 +20,20 @@ class Init
#
# # library "pathname", "set" # Standard libraries
# # library "strong_json" # Gems
#
# # configure_code_diagnostics(D::Ruby.strict) # `strict` diagnostics setting
# # configure_code_diagnostics(D::Ruby.lenient) # `lenient` diagnostics setting
# # configure_code_diagnostics do |hash| # You can setup everything yourself
# # hash[D::Ruby::NoMethod] = :information
# # end
# end
# target :spec do
# target :test do
# signature "sig", "sig-private"
#
# check "spec"
# check "test"
#
# # library "pathname", "set" # Standard libraries
# # library "rspec"
# end
EOF

Expand Down
15 changes: 15 additions & 0 deletions lib/steep/drivers/utils/driver_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ def wait_for_message(reader:, unknown_messages: :ignore, &block)
end
end
end

def keep_diagnostic?(diagnostic)
severity = diagnostic[:severity]

case self.severity_level
when nil, :hint
true
when :error
severity <= LanguageServer::Protocol::Constant::DiagnosticSeverity::ERROR
when :warning
severity <= LanguageServer::Protocol::Constant::DiagnosticSeverity::WARNING
when :information
severity <= LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION
end
end
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/steep/drivers/validate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def run

any_error ||= !errors.empty?

formatter = Diagnostic::LSPFormatter.new
formatter = Diagnostic::LSPFormatter.new({})
diagnostics = errors.group_by {|e| e.location.buffer }.transform_values do |errors|
errors.map {|error| formatter.format(error) }
end
Expand Down
3 changes: 3 additions & 0 deletions lib/steep/drivers/watch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class Watch
attr_reader :stdout
attr_reader :stderr
attr_reader :queue
attr_accessor :severity_level

include Utils::DriverHelper
include Utils::JobsCount
Expand All @@ -16,6 +17,7 @@ def initialize(stdout:, stderr:)
@stdout = stdout
@stderr = stderr
@queue = Thread::Queue.new
@severity_level = :warning
end

def watching?(changed_path, files:, dirs:)
Expand Down Expand Up @@ -126,6 +128,7 @@ def run()
printer = DiagnosticPrinter.new(stdout: stdout, buffer: buffer)

diagnostics = response[:params][:diagnostics]
diagnostics.filter! {|d| keep_diagnostic?(d) }

unless diagnostics.empty?
diagnostics.each do |diagnostic|
Expand Down
67 changes: 44 additions & 23 deletions lib/steep/project/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,19 @@ class TargetDSL
attr_reader :ignored_sources
attr_reader :stdlib_root
attr_reader :core_root
attr_reader :strictness_level
attr_reader :typing_option_hash
attr_reader :repo_paths
attr_reader :code_diagnostics_config

def initialize(name, sources: [], libraries: [], signatures: [], ignored_sources: [], repo_paths: [])
def initialize(name, sources: [], libraries: [], signatures: [], ignored_sources: [], repo_paths: [], code_diagnostics_config: {})
@name = name
@sources = sources
@libraries = libraries
@signatures = signatures
@ignored_sources = ignored_sources
@strictness_level = :default
@typing_option_hash = {}
@core_root = nil
@stdlib_root = nil
@repo_paths = []
@code_diagnostics_config = code_diagnostics_config
end

def initialize_copy(other)
Expand All @@ -32,11 +30,10 @@ def initialize_copy(other)
@libraries = other.libraries.dup
@signatures = other.signatures.dup
@ignored_sources = other.ignored_sources.dup
@strictness_level = other.strictness_level
@typing_option_hash = other.typing_option_hash
@repo_paths = other.repo_paths.dup
@core_root = other.core_root
@stdlib_root = other.stdlib_root
@code_diagnostics_config = other.code_diagnostics_config.dup
end

def check(*args)
Expand All @@ -51,9 +48,8 @@ def library(*args)
libraries.push(*args)
end

def typing_options(level = @strictness_level, **hash)
@strictness_level = level
@typing_option_hash = hash
def typing_options(level = nil, **hash)
Steep.logger.error "#typing_options is deprecated and has no effect as of version 0.46.0"
end

def signature(*args)
Expand Down Expand Up @@ -86,6 +82,35 @@ def stdlib_path(core_root:, stdlib_root:)
def repo_path(*paths)
@repo_paths.push(*paths.map {|s| Pathname(s) })
end

# Configure the code diagnostics printing setup.
#
# Yields a hash, and the update the hash in the block.
#
# ```rb
# D = Steep::Diagnostic
#
# configure_code_diagnostics do |hash|
# # Assign one of :error, :warning, :information, :hint or :nil to error classes.
# hash[D::Ruby::UnexpectedPositionalArgument] = :error
# end
# ```
#
# Passing a hash is also allowed.
#
# ```rb
# D = Steep::Diagnostic
#
# configure_code_diagnostics(D::Ruby.lenient)
# ```
#
def configure_code_diagnostics(hash = nil)
if hash
code_diagnostics_config.merge!(hash)
end

yield code_diagnostics_config if block_given?
end
end

attr_reader :project
Expand All @@ -111,18 +136,22 @@ def self.register_template(name, target)
end

def self.parse(project, code, filename: "Steepfile")
self.new(project: project).instance_eval(code, filename)
Steep.logger.tagged filename do
self.new(project: project).instance_eval(code, filename)
end
end

def target(name, template: nil, &block)
target = if template
self.class.templates[template]&.dup&.update(name: name) or
raise "Unknown template: #{template}, available templates: #{@@templates.keys.join(", ")}"
else
TargetDSL.new(name)
TargetDSL.new(name, code_diagnostics_config: Diagnostic::Ruby.default.dup)
end

target.instance_eval(&block) if block_given?
Steep.logger.tagged "target=#{name}" do
target.instance_eval(&block) if block_given?
end

source_pattern = Pattern.new(patterns: target.sources, ignores: target.ignored_sources, ext: ".rb")
signature_pattern = Pattern.new(patterns: target.signatures, ext: ".rbs")
Expand All @@ -138,16 +167,8 @@ def target(name, template: nil, &block)
stdlib_root: target.stdlib_root,
repo_paths: target.repo_paths
)

case target.strictness_level
when :strict
options.apply_strict_typing_options!
when :lenient
options.apply_lenient_typing_options!
end

options.merge!(target.typing_option_hash)
end
end,
code_diagnostics_config: target.code_diagnostics_config
).tap do |target|
project.targets << target
end
Expand Down
Loading

0 comments on commit b3c65f0

Please sign in to comment.