Skip to content

Commit

Permalink
Initial implementation based on a combination of:
Browse files Browse the repository at this point in the history
  • Loading branch information
igray committed Aug 10, 2024
1 parent 52d881d commit 69f779a
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 54 deletions.
12 changes: 6 additions & 6 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License
The MIT License (MIT)

Copyright (c) 2024 Iain Gray
Copyright (c) 2024 iGray Consulting, LLC

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand All @@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
21 changes: 0 additions & 21 deletions LICENSE.txt

This file was deleted.

2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
{
devShells = forEachSupportedSystem ({ pkgs }: {
default = pkgs.mkShell {
packages = with pkgs; [ ruby_3_2 ];
packages = with pkgs; [ ruby_3_1 ];
};
});
};
Expand Down
75 changes: 75 additions & 0 deletions lib/ruby_lsp/reek/addon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# frozen_string_literal: true

require 'bundler/setup'
require 'sorbet-runtime'
require 'ruby_lsp/addon'
require 'ruby_lsp/base_server'
require 'ruby_lsp/server'
require 'uri'
require_relative 'runner'

module RubyLsp
module Reek
# Implements the RubyLsp::Addon interface to provide Reek linter support to Ruby LSP.
class Addon < ::RubyLsp::Addon
def initializer
@runner = nil
end

# @return [String] The name of the addon.
def name
'Reek: Code smell detector for Ruby'
end

# @param global_state [GlobalState] The global state of the Ruby LSP server.
# @param outgoing_queue [Thread::Queue] The outgoing message queue of the Ruby LSP server.
def activate(global_state, message_queue)
warn "Activating Reek Ruby LSP addon v#{::RubyLsp::Reek::VERSION}"
@runner = Runner.new
global_state.register_formatter('reek', @runner)
register_additional_file_watchers(global_state, message_queue)
warn "Initialized Reek Ruby LSP addon v#{::RubyLsp::Reek::VERSION}"
end

# @return [nil]
def deactivate
@runner = nil
end

# @param global_state [GlobalState] The global state of the Ruby LSP server.
# @param outgoing_queue [Thread::Queue] The outgoing message queue of the Ruby LSP server.
def register_additional_file_watchers(global_state, message_queue)
return unless global_state.supports_watching_files

message_queue << Request.new(
id: 'reek-file-watcher',
method: 'client/registerCapability',
params: Interface::RegistrationParams.new(
registrations: [
Interface::Registration.new(
id: 'workspace/didChangeWatchedFilesReek',
method: 'workspace/didChangeWatchedFiles',
register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
watchers: [
Interface::FileSystemWatcher.new(
glob_pattern: '**/.reek.yml',
kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE
)
]
)
)
]
)
)
end

# @param changes [Array<Hash>] The changes to the watched files.
def workspace_did_change_watched_files(changes)
return unless changes.any? { |change| change[:uri].end_with?('.reek.yml') }

@runner.init!
warn "Re-initialized Reek Ruby LSP addon v#{::RubyLsp::Reek::VERSION} due to .reek.yml file change"
end
end
end
end
60 changes: 60 additions & 0 deletions lib/ruby_lsp/reek/runner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

require 'reek'

module RubyLsp
module Reek
# Implements Ruby LSP Formatter interface: specifically run_diagnostic
class Runner
include RubyLsp::Requests::Support::Formatter

def initialize
@config = ::Reek::Configuration::AppConfiguration.from_default_path
end

# We are not implementing this method, but it is required by the interface
#
# @param uri [String] The URI of the document to format.
# @param document [RubyLsp::Interface::TextDocumentItem] The document to format.
# @return [String] The formatted document.
def run_formatting(_uri, document)
document.source
end

# @param uri [String] The URI of the document to run diagnostics on.
# @param document [RubyLsp::Interface::TextDocumentItem] The document to run diagnostics on.
def run_diagnostic(uri, document)
return [] if config.path_excluded?(Pathname.new(uri.path))

examiner = ::Reek::Examiner.new(document.source, configuration: config)
examiner.smells.map { |w| warning_to_diagnostic(w) }
end

private

attr_reader :config

# @param warning [Reek::SmellWarning] The warning to convert to a diagnostic.
# @return [RubyLsp::Interface::Diagnostic] The diagnostic.
def warning_to_diagnostic(warning)
::RubyLsp::Interface::Diagnostic.new(
range: ::RubyLsp::Interface::Range.new(
start: ::RubyLsp::Interface::Position.new(
line: warning.lines.first - 1,
character: 0
),
end: ::RubyLsp::Interface::Position.new(
line: warning.lines.last - 1,
character: 0
)
),
severity: Constant::DiagnosticSeverity::WARNING,
code: warning.smell_type,
code_description: ::RubyLsp::Interface::CodeDescription.new(href: warning.explanatory_link),
source: 'Reek',
message: warning.message
)
end
end
end
end
2 changes: 1 addition & 1 deletion lib/ruby_lsp/reek/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module RubyLsp
module Reek
VERSION = "0.1.0"
VERSION = '0.1.0'
end
end
40 changes: 21 additions & 19 deletions ruby-lsp-reek.gemspec
Original file line number Diff line number Diff line change
@@ -1,43 +1,45 @@
# frozen_string_literal: true

require_relative "lib/ruby_lsp/reek/version"
require_relative 'lib/ruby_lsp/reek/version'

Gem::Specification.new do |spec|
spec.name = "ruby-lsp-reek"
spec.name = 'ruby-lsp-reek'
spec.version = RubyLsp::Reek::VERSION
spec.authors = ["Iain Gray"]
spec.email = ["[email protected]"]
spec.authors = ['Iain Gray']
spec.email = ['[email protected]']

spec.summary = 'Ruby LSP Reek'
spec.description = 'An addon for Ruby LSP that enables linting with reek'
spec.homepage = 'https://github.com/igray/ruby-lsp-reek'
spec.license = "MIT"
spec.license = 'MIT'
spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')

spec.metadata["allowed_push_host"] = "https://rubygems.org"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = spec.homepage
spec.metadata["changelog_uri"] = spec.homepage
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
spec.metadata['homepage_uri'] = spec.homepage
spec.metadata['source_code_uri'] = spec.homepage
spec.metadata['changelog_uri'] = spec.homepage

spec.files = Dir.chdir(__dir__) do
`git ls-files -z`.split("\x0").reject do |f|
(File.expand_path(f) == __FILE__) ||
f.start_with?(*%w[bin/ test/ .git Gemfile])
end
end
spec.bindir = "exe"
spec.bindir = 'exe'
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
spec.require_paths = ['lib']

spec.add_dependency("ruby-lsp", ">= 0.12.0")
spec.add_dependency("reek", ">= 5.0")
spec.add_dependency('reek', '>= 5.0')
spec.add_dependency('ruby-lsp', '>= 0.12.0')
spec.add_dependency('sorbet-runtime', '>= 0.5.5685')

# For more information and examples about making a new gem, check out our
# guide at: https://bundler.io/guides/creating_gem.html
spec.add_development_dependency "bundler", "~> 2.4"
spec.add_development_dependency "minitest", "~> 5.20"
spec.add_development_dependency "rake", "~> 13.1"
spec.add_development_dependency "standard", "~> 1.31"
spec.add_development_dependency "rubocop-minitest", "~> 0.35"
spec.add_development_dependency "rubocop-rake", "~> 0.6"
spec.add_development_dependency 'bundler', '~> 2.4'
spec.add_development_dependency 'minitest', '~> 5.20'
spec.add_development_dependency 'pry', '~> 0.14'
spec.add_development_dependency 'rake', '~> 13.1'
spec.add_development_dependency 'rubocop-minitest', '~> 0.35'
spec.add_development_dependency 'rubocop-rake', '~> 0.6'
spec.add_development_dependency 'standard', '~> 1.31'
end
116 changes: 116 additions & 0 deletions test/ruby_lsp_addon_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))

require 'bundler/setup'
require 'minitest/autorun'
require 'sorbet-runtime'
require 'core_ext/uri'
require 'language_server-protocol'
require 'ruby_indexer/ruby_indexer'
require 'ruby_lsp/addon'
require 'ruby_lsp/base_server'
require 'ruby_lsp/server'
require 'ruby_lsp/requests'
require 'ruby_lsp/utils'
require 'ruby_lsp/store'
require 'ruby_lsp/document'
require 'ruby_lsp/global_state'
require 'ruby_lsp/ruby_document'
require 'ruby_lsp/type_inferrer'
require 'prism'
require 'pry'
require 'ruby_lsp/reek/addon'

class RubyLspAddonTest < Minitest::Test
def setup
@addon = RubyLsp::Reek::Addon.new
super
end

def test_name
assert_equal 'Reek: Code smell detector for Ruby', @addon.name
end

def test_diagnostic
source = <<~RUBY
def foo
s = 'hello'
puts s
end
RUBY
with_server(source, 'simple.rb') do |server, uri|
server.process_message(
id: 2,
method: 'textDocument/diagnostic',
params: {
textDocument: {
uri:
}
}
)

result = server.pop_response

assert_instance_of(RubyLsp::Result, result)
assert_equal 'full', result.response.kind
assert_equal 1, result.response.items.size
item = result.response.items.first
assert_equal({ line: 1, character: 0 }, item.range.start.to_hash)
assert_equal({ line: 1, character: 0 }, item.range.end.to_hash)
assert_equal RubyLsp::Constant::DiagnosticSeverity::WARNING, item.severity
assert_equal 'UncommunicativeVariableName', item.code
assert_equal(
'https://github.com/troessner/reek/blob/v6.3.0/docs/Uncommunicative-Variable-Name.md',
item.code_description.href
)
assert_equal 'Reek', item.source
assert_equal("has the variable name 's'", item.message)
end
end

private

# Lifted from here, because we need to override the formatter to "standard" in the test helper:
# https://github.com/Shopify/ruby-lsp/blob/4c1906172add4d5c39c35d3396aa29c768bfb898/lib/ruby_lsp/test_helper.rb#L20
def with_server(
source = nil,
path = 'fake.rb',
pwd: 'test/fixture/ruby_lsp',
stub_no_typechecker: false,
load_addons: true,
&block
)
Dir.chdir pwd do
server = RubyLsp::Server.new(test_mode: true)
uri = Kernel.URI(File.join(server.global_state.workspace_path, path))
server.global_state.instance_variable_set(:@linters, ['reek'])
server.global_state.stubs(:typechecker).returns(false) if stub_no_typechecker

if source
server.process_message(
{
method: 'textDocument/didOpen',
params: {
textDocument: {
uri:,
text: source,
version: 1
}
}
}
)
end

server.global_state.index.index_single(
RubyIndexer::IndexablePath.new(nil, uri.to_standardized_path),
source
)
server.load_addons if load_addons
block.call(server, uri)
end
ensure
if load_addons
RubyLsp::Addon.addons.each(&:deactivate)
RubyLsp::Addon.addons.clear
end
end
end
6 changes: 0 additions & 6 deletions test/test_helper.rb

This file was deleted.

0 comments on commit 69f779a

Please sign in to comment.