chutney 3.6.0 → 3.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +1 -2
- data/.rubocop.yml +4 -3
- data/Gemfile +9 -0
- data/README.md +49 -3
- data/chutney.gemspec +8 -17
- data/examples/emoji.feature +1 -2
- data/exe/chutney +28 -2
- data/exe/chutney-lsp +13 -0
- data/img/happy_chutney.png +0 -0
- data/lib/chutney/configuration.rb +31 -10
- data/lib/chutney/formatter/pie_formatter.rb +2 -2
- data/lib/chutney/linter/inconsistent_quoting.rb +1 -1
- data/lib/chutney/linter/invalid_file_name.rb +1 -1
- data/lib/chutney/linter/same_tag_different_case.rb +1 -1
- data/lib/chutney/linter/same_tag_for_all_scenarios.rb +2 -2
- data/lib/chutney/linter/unique_scenario_names.rb +1 -1
- data/lib/chutney/linter/unused_variable.rb +1 -1
- data/lib/chutney/linter/use_outline.rb +1 -1
- data/lib/chutney/linter.rb +1 -1
- data/lib/chutney/lsp/result.rb +16 -0
- data/lib/chutney/lsp/server.rb +177 -0
- data/lib/chutney/lsp.rb +7 -0
- data/lib/chutney/version.rb +1 -1
- data/lib/chutney.rb +8 -3
- data/usechutney.com/.gitignore +5 -0
- data/usechutney.com/Gemfile +37 -0
- data/usechutney.com/_config.yml +298 -0
- data/usechutney.com/_data/navigation.yml +112 -0
- data/usechutney.com/_data/ui-text.yml +2132 -0
- data/usechutney.com/_posts/2024-09-23-welcome-to-jekyll.markdown +28 -0
- data/usechutney.com/assets/images/mr_pickle.png +0 -0
- data/usechutney.com/assets/images/pug.png +0 -0
- data/usechutney.com/docs/configuration/index.md +104 -0
- data/usechutney.com/docs/disabling-rules/index.md +11 -0
- data/usechutney.com/docs/installing/index.md +30 -0
- data/usechutney.com/docs/language-server/index.md +24 -0
- data/usechutney.com/docs/rules/avoid-full-stops/index.md +23 -0
- data/usechutney.com/docs/rules/avoid-outline-for-single-example/index.md +28 -0
- data/usechutney.com/docs/rules/avoid-scripting/index.md +33 -0
- data/usechutney.com/docs/rules/avoid-splat-steps-in-background/index.md +25 -0
- data/usechutney.com/docs/rules/avoid-splat-steps-in-scenarios/index.md +29 -0
- data/usechutney.com/docs/rules/avoid-typographers-quotes/index.md +24 -0
- data/usechutney.com/docs/rules/background-does-more-than-setup/index.md +28 -0
- data/usechutney.com/docs/rules/background-requires-multiple-scenarios/index.md +26 -0
- data/usechutney.com/docs/rules/bad-scenario-name/index.md +28 -0
- data/usechutney.com/docs/rules/empty-feature-file/index.md +7 -0
- data/usechutney.com/docs/rules/file-name-differs-feature-name/index.md +37 -0
- data/usechutney.com/docs/rules/givens-after-background/index.md +36 -0
- data/usechutney.com/docs/rules/inconsistent-quoting/index.md +29 -0
- data/usechutney.com/docs/rules/invalid-step-flow/index.md +31 -0
- data/usechutney.com/docs/rules/invalid_file_name/index.md +19 -0
- data/usechutney.com/docs/rules/missing-example-name/index.md +49 -0
- data/usechutney.com/docs/rules/missing-example-table/index.md +34 -0
- data/usechutney.com/docs/rules/missing-feature-description/index.md +25 -0
- data/usechutney.com/docs/rules/missing-feature-name/index.md +19 -0
- data/usechutney.com/docs/rules/missing-scenario-name/index.md +19 -0
- data/usechutney.com/docs/rules/missing-scenario-outline/index.md +39 -0
- data/usechutney.com/docs/rules/missing-test-action/index.md +28 -0
- data/usechutney.com/docs/rules/missing-test-verification/index.md +28 -0
- data/usechutney.com/docs/rules/required-tag-starts-with/index.md +31 -0
- data/usechutney.com/docs/rules/same-tag-different-case/index.md +42 -0
- data/usechutney.com/docs/rules/same-tag-for-all-scenarios/index.md +42 -0
- data/usechutney.com/docs/rules/scenario-names-match/index.md +30 -0
- data/usechutney.com/docs/rules/tag-used-multiple-times/index.md +32 -0
- data/usechutney.com/docs/rules/too-clumsy/index.md +78 -0
- data/usechutney.com/docs/rules/too-long-step/index.md +30 -0
- data/usechutney.com/docs/rules/too-many-different-tags/index.md +32 -0
- data/usechutney.com/docs/rules/too-many-steps/index.md +79 -0
- data/usechutney.com/docs/rules/too-many-tags/index.md +32 -0
- data/usechutney.com/docs/rules/unique-scenario-names/index.md +39 -0
- data/usechutney.com/docs/rules/unknown-variable/index.md +38 -0
- data/usechutney.com/docs/rules/unused-variable/index.md +37 -0
- data/usechutney.com/docs/rules/use-background/index.md +40 -0
- data/usechutney.com/docs/rules/use-outline/index.md +39 -0
- data/usechutney.com/docs/running/index.md +33 -0
- data/usechutney.com/pages/404.html +25 -0
- data/usechutney.com/pages/about/index.md +14 -0
- data/usechutney.com/pages/index.markdown +40 -0
- metadata +76 -114
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e46a3bdaaed4652c5e7aa856c15b40995602a17f5b0c0d5ff1258e4a59c6842
|
4
|
+
data.tar.gz: f47caf4ee93c1e74499a6f1291de4e95643f7beab1f8ec9cee555d2bd7298f17
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7a75ae98a45dc49284d3b4acd395bb887e6726aefe23c585aae3181e4d014ea23682f29427a7d87d00db6f964db26a89c8ce4554838899b6f721d6bbc90c5262
|
7
|
+
data.tar.gz: a97f98ccbb8254dd8940144ae24cea907e70df9c60b8582b0b76365217d620fcd78f23d908a59543d4ad1851f91cc1df07ec44bab8cc07b3452b17456b84d364
|
data/.circleci/config.yml
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
# Use the latest 2.1 version of CircleCI pipeline processing engine, see https://circleci.com/docs/2.0/configuration-reference/
|
2
2
|
version: 2.1
|
3
3
|
|
4
|
-
|
5
4
|
jobs:
|
6
5
|
build:
|
7
6
|
docker:
|
8
|
-
- image:
|
7
|
+
- image: cimg/ruby:3.2
|
9
8
|
steps:
|
10
9
|
- checkout
|
11
10
|
- run: bundle install
|
data/.rubocop.yml
CHANGED
@@ -27,14 +27,13 @@ Layout/LineLength:
|
|
27
27
|
Metrics/MethodLength:
|
28
28
|
Max: 20
|
29
29
|
|
30
|
-
|
31
30
|
# Offense count: 1
|
32
31
|
# Cop supports --auto-correct.
|
33
32
|
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
34
33
|
# SupportedStyles: predicate, comparison
|
35
34
|
Style/NumericPredicate:
|
36
35
|
Exclude:
|
37
|
-
-
|
36
|
+
- "lib/chutney/linter/file_name_differs_feature_name.rb"
|
38
37
|
|
39
38
|
Metrics/ModuleLength:
|
40
39
|
Exclude:
|
@@ -63,4 +62,6 @@ Style/StringConcatenation:
|
|
63
62
|
|
64
63
|
AllCops:
|
65
64
|
NewCops: enable
|
66
|
-
SuggestExtensions: false
|
65
|
+
SuggestExtensions: false
|
66
|
+
Exclude:
|
67
|
+
- "usechutney.com/**"
|
data/Gemfile
CHANGED
@@ -3,3 +3,12 @@
|
|
3
3
|
source 'https://rubygems.org'
|
4
4
|
|
5
5
|
gemspec
|
6
|
+
|
7
|
+
gem 'coveralls', '~> 0.8'
|
8
|
+
gem 'cucumber', '>= 9.0'
|
9
|
+
gem 'pry-byebug', '~> 3.0'
|
10
|
+
gem 'rake', '~> 13.0'
|
11
|
+
gem 'rerun', '~> 0.13'
|
12
|
+
gem 'rspec', '~> 3.13'
|
13
|
+
gem 'rspec-expectations', '~> 3.0'
|
14
|
+
gem 'rubocop', '~> 1.66'
|
data/README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
<h1 align="center">
|
2
|
-
<img src="https://raw.githubusercontent.com/BillyRuffian/chutney/master/img/
|
2
|
+
<img src="https://raw.githubusercontent.com/BillyRuffian/chutney/master/img/happy_chutney.png?sanitize=true" alt="Chutney" height="200">
|
3
3
|
<br>
|
4
4
|
Chutney
|
5
5
|
<br>
|
@@ -19,10 +19,56 @@
|
|
19
19
|
|
20
20
|
</div>
|
21
21
|
|
22
|
-
Read the documentation [here](https://
|
22
|
+
Read the documentation [here](https://www.usechutney.com/).
|
23
|
+
|
24
|
+
Your documentation is precious and should be treated as such. Chutney is a tool to help you keep your gherkin files in good shape. It will help you to write better gherkin and keep your feature files consistent through an opinionated, but optional, set of rules.
|
25
|
+
|
26
|
+
## Installation
|
27
|
+
|
28
|
+
### Ruby
|
29
|
+
|
30
|
+
Chutney is a ruby gem, so relies on you having ruby installed. It requires ruby 3.2 or later.
|
31
|
+
|
32
|
+
For macOS, Linux or other Unix-like systems, I recommend using a version manager like [rvm](https://rvm.io), [asdf](https://asdf-vm.com) or [rbenv](https://github.com/rbenv/rbenv).
|
33
|
+
|
34
|
+
For Windows, you can use [RubyInstaller](https://rubyinstaller.org/).
|
35
|
+
|
36
|
+
### Chutney
|
37
|
+
|
38
|
+
To install chutney system-wide, run:
|
39
|
+
|
40
|
+
```bash
|
41
|
+
gem install chutney
|
42
|
+
```
|
43
|
+
|
44
|
+
To install chutney for a specific project, add it to your Gemfile:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
gem 'chutney'
|
48
|
+
```
|
49
|
+
|
50
|
+
## Usage
|
51
|
+
|
52
|
+
To run chutney, simply run:
|
53
|
+
|
54
|
+
```bash
|
55
|
+
chutney
|
56
|
+
```
|
57
|
+
|
58
|
+
It will search for any `.feature` files beneath the current directory and give you an opinion. It comes with a default set of rules and will give you a little nudge if you haven't got your own chutney configuration file.
|
59
|
+
|
60
|
+
To create a configuration file, run:
|
61
|
+
|
62
|
+
```bash
|
63
|
+
chutney --init
|
64
|
+
```
|
65
|
+
|
66
|
+
(Configuration files can in either `.chutney.yml` or `.chutney.yml` and reside in the top-level of the project or in a `/config` directory.)
|
67
|
+
|
23
68
|
|
24
69
|
See [this page](https://billyruffian.github.io/chutney/usage/rules.html) for a full list of the rules chutney encourages.
|
25
70
|
|
71
|
+
|
26
72
|
## Notes
|
27
73
|
|
28
|
-
Chutney 3+ has replaced its direct dependency on Cucumber and instead uses the excellent [cuke_modeller](https://github.com/enkessler/cuke_modeler) to parse your feature files.
|
74
|
+
Chutney 3+ has replaced its direct dependency on Cucumber and instead uses the excellent [cuke_modeller](https://github.com/enkessler/cuke_modeler) to parse your feature files.
|
data/chutney.gemspec
CHANGED
@@ -43,26 +43,17 @@ Gem::Specification.new do |spec|
|
|
43
43
|
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
44
44
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|s|features)/}) }
|
45
45
|
end
|
46
|
-
|
46
|
+
|
47
47
|
spec.bindir = 'exe'
|
48
48
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
49
49
|
spec.require_paths = ['lib']
|
50
|
-
|
51
|
-
spec.add_runtime_dependency 'amatch', '~> 0.4.0'
|
52
|
-
spec.add_runtime_dependency 'cuke_modeler', '~> 3.3'
|
53
|
-
spec.add_runtime_dependency 'i18n', '>= 1.8.2', '< 1.15.0'
|
54
|
-
spec.add_runtime_dependency 'pastel', '~> 0.7'
|
55
|
-
spec.add_runtime_dependency 'tty-pie', '~> 0.3'
|
56
50
|
|
51
|
+
spec.add_dependency 'amatch', '~> 0.4.0'
|
52
|
+
spec.add_dependency 'cuke_modeler', '~> 3.21'
|
53
|
+
spec.add_dependency 'i18n', '>= 1.8.2', '< 1.15.0'
|
54
|
+
spec.add_dependency 'language_server-protocol', '~> 3.17'
|
55
|
+
spec.add_dependency 'pastel', '~> 0.7'
|
56
|
+
spec.add_dependency 'tty-pie', '~> 0.3'
|
57
57
|
|
58
|
-
spec.
|
59
|
-
spec.add_development_dependency 'cucumber', '>= 7.0'
|
60
|
-
spec.add_development_dependency 'pry-byebug', '~> 3.0'
|
61
|
-
spec.add_development_dependency 'rake', '~> 13.0'
|
62
|
-
spec.add_development_dependency 'rerun', '~> 0.13'
|
63
|
-
spec.add_development_dependency 'rspec-expectations', '~> 3.0'
|
64
|
-
spec.add_development_dependency 'rubocop', '~> 1.50.2'
|
65
|
-
spec.add_development_dependency 'rspec', '~> 3.8'
|
66
|
-
|
67
|
-
spec.required_ruby_version = '>= 2.6'
|
58
|
+
spec.required_ruby_version = '>= 3.2'
|
68
59
|
end
|
data/examples/emoji.feature
CHANGED
data/exe/chutney
CHANGED
@@ -9,6 +9,7 @@ require 'chutney/formatter/rainbow_formatter'
|
|
9
9
|
require 'optparse'
|
10
10
|
|
11
11
|
formatters = Set.new
|
12
|
+
quiet = false
|
12
13
|
|
13
14
|
# rubocop:disable Metrics/BlockLength
|
14
15
|
OptionParser.new do |opts|
|
@@ -21,6 +22,12 @@ OptionParser.new do |opts|
|
|
21
22
|
formatters << formatter
|
22
23
|
end
|
23
24
|
|
25
|
+
opts.on('-q',
|
26
|
+
'--quiet',
|
27
|
+
'Disable chutney usage warnings. Does not affect the output of the formatters.') do
|
28
|
+
quiet = true
|
29
|
+
end
|
30
|
+
|
24
31
|
opts.on('-v', '--version', 'Display the version.') do
|
25
32
|
puts Chutney::VERSION
|
26
33
|
exit
|
@@ -28,7 +35,7 @@ OptionParser.new do |opts|
|
|
28
35
|
|
29
36
|
opts.on('-l',
|
30
37
|
'--linters',
|
31
|
-
'List the linter status by this configuration and exit') do
|
38
|
+
'List the linter status by this configuration and exit.') do
|
32
39
|
pastel = Pastel.new
|
33
40
|
chutney_config = Chutney::ChutneyLint.new.configuration
|
34
41
|
max_name_length = chutney_config.keys.map(&:length).max + 1
|
@@ -43,6 +50,24 @@ OptionParser.new do |opts|
|
|
43
50
|
end
|
44
51
|
exit
|
45
52
|
end
|
53
|
+
|
54
|
+
opts.on('--init',
|
55
|
+
'Install a `chutney.yml` configuration file.') do
|
56
|
+
config_dest = if File.exist?('config') && File.directory?('config')
|
57
|
+
'config'
|
58
|
+
else
|
59
|
+
'.'
|
60
|
+
end
|
61
|
+
config_path = File.join(config_dest, 'chutney.yml')
|
62
|
+
default_path = Chutney::ChutneyLint.new.configuration.default_configuration_path
|
63
|
+
if File.exist?(config_path)
|
64
|
+
puts "#{config_path} already exists - remove it first if you want to overwrite."
|
65
|
+
else
|
66
|
+
FileUtils.cp(default_path, config_path)
|
67
|
+
puts "#{config_path} created."
|
68
|
+
end
|
69
|
+
exit
|
70
|
+
end
|
46
71
|
end.parse!
|
47
72
|
# rubocop:enable Metrics/BlockLength
|
48
73
|
|
@@ -52,6 +77,7 @@ files = ARGV.map { |pattern| Dir.glob(pattern) }.flatten
|
|
52
77
|
files = Dir.glob('features/**/*.feature') if ARGV.empty?
|
53
78
|
|
54
79
|
linter = Chutney::ChutneyLint.new(*files)
|
80
|
+
linter.configuration.quiet! if quiet
|
55
81
|
report = linter.analyse
|
56
82
|
|
57
83
|
formatters.each do |formatter|
|
@@ -64,7 +90,7 @@ basic_formatter = Chutney::Formatter.new
|
|
64
90
|
basic_formatter.results = report
|
65
91
|
|
66
92
|
if basic_formatter.files_with_issues.empty?
|
67
|
-
exit
|
93
|
+
exit
|
68
94
|
else
|
69
95
|
exit(false)
|
70
96
|
end
|
data/exe/chutney-lsp
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'language_server-protocol'
|
5
|
+
require 'logger'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
require 'chutney'
|
9
|
+
require 'chutney/lsp'
|
10
|
+
require 'chutney/lsp/result'
|
11
|
+
require 'chutney/lsp/server'
|
12
|
+
|
13
|
+
Chutney::LSP::Server.new.start
|
Binary file
|
@@ -5,28 +5,49 @@ require 'delegate'
|
|
5
5
|
module Chutney
|
6
6
|
# gherkin_lint configuration object
|
7
7
|
class Configuration < SimpleDelegator
|
8
|
+
attr_accessor :default_configuration_path, :user_configuration_path
|
9
|
+
|
8
10
|
def initialize(path)
|
9
|
-
@
|
11
|
+
@default_configuration_path = path
|
10
12
|
@config = load_configuration || {}
|
11
13
|
load_user_configuration
|
12
14
|
super(@config)
|
13
15
|
end
|
14
16
|
|
15
|
-
def configuration_path
|
16
|
-
@path
|
17
|
-
end
|
18
|
-
|
19
17
|
def load_configuration
|
20
|
-
YAML.load_file
|
18
|
+
YAML.load_file default_configuration_path || '' if default_configuration_path
|
21
19
|
end
|
22
20
|
|
23
21
|
def load_user_configuration
|
24
22
|
config_files = ['chutney.yml', '.chutney.yml'].map do |fname|
|
25
|
-
|
26
|
-
|
23
|
+
['.', 'config'].map do |dir|
|
24
|
+
Dir["#{dir}#{File::SEPARATOR}#{fname}"]
|
25
|
+
end
|
26
|
+
end.flatten.compact
|
27
|
+
|
28
|
+
self.user_configuration_path = config_files.first
|
29
|
+
return unless !user_configuration_path.nil? && File.exist?(user_configuration_path)
|
30
|
+
|
31
|
+
begin
|
32
|
+
merge_config(user_configuration_path)
|
33
|
+
rescue TypeError
|
34
|
+
unless quiet?
|
35
|
+
warn("Chutney: configuration file `#{user_configuration_path}` is not correctly formatted YAML, " \
|
36
|
+
'falling back to gem defaults.')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def using_user_configuration?
|
42
|
+
!user_configuration_path.nil?
|
43
|
+
end
|
44
|
+
|
45
|
+
def quiet?
|
46
|
+
@config.fetch('quiet', false)
|
47
|
+
end
|
27
48
|
|
28
|
-
|
29
|
-
|
49
|
+
def quiet!
|
50
|
+
@config['quiet'] = true
|
30
51
|
end
|
31
52
|
|
32
53
|
private
|
@@ -21,13 +21,13 @@ module Chutney
|
|
21
21
|
def print_report(data)
|
22
22
|
return if data.empty?
|
23
23
|
|
24
|
-
print TTY::Pie.new(data
|
24
|
+
print TTY::Pie.new(data:, radius: 8, legend: { format: '%<label>s %<name>s %<value>i' })
|
25
25
|
puts
|
26
26
|
end
|
27
27
|
|
28
28
|
def top_offences
|
29
29
|
offence = Hash.new(0)
|
30
|
-
files_with_issues.
|
30
|
+
files_with_issues.each_value do |linter|
|
31
31
|
linter.each do |lint|
|
32
32
|
offence[lint[:linter]] += lint[:issues].count
|
33
33
|
end
|
@@ -7,7 +7,7 @@ module Chutney
|
|
7
7
|
# matching group 1: opening quote; 2: quoted text; 3: closing quote
|
8
8
|
# opening and closing quote must match (via backrefs)
|
9
9
|
# apostrophes, both singular and plural posessives, are accounted for
|
10
|
-
QUOTED_STRING = /(?!\b\b)(['"])(.*(?:\b'\b[^\1]*)*(?!\b[\1]\b))(\1)
|
10
|
+
QUOTED_STRING = /(?!\b\b)(['"])(.*(?:\b'\b[^\1]*)*(?!\b[\1]\b))(\1)/
|
11
11
|
Parameter = Struct.new('Parameter', :quotation_mark, :name)
|
12
12
|
|
13
13
|
def lint
|
@@ -19,7 +19,7 @@ module Chutney
|
|
19
19
|
collision_with = case_collision(tag)
|
20
20
|
if collision_with
|
21
21
|
add_issue(I18n.t('linters.same_tag_different_case',
|
22
|
-
existing_tag: collision_with, tag:
|
22
|
+
existing_tag: collision_with, tag:),
|
23
23
|
feature, scenario)
|
24
24
|
else
|
25
25
|
@@all_known_tags << tag
|
@@ -20,7 +20,7 @@ module Chutney
|
|
20
20
|
|
21
21
|
add_issue(
|
22
22
|
I18n.t('linters.same_tag_for_all_scenarios.feature_level',
|
23
|
-
tag:
|
23
|
+
tag:),
|
24
24
|
feature
|
25
25
|
)
|
26
26
|
end
|
@@ -37,7 +37,7 @@ module Chutney
|
|
37
37
|
next if tag == 'skip'
|
38
38
|
|
39
39
|
add_issue(I18n.t('linters.same_tag_for_all_scenarios.example_level',
|
40
|
-
tag:
|
40
|
+
tag:), feature, scenario)
|
41
41
|
end
|
42
42
|
end
|
43
43
|
end
|
@@ -11,7 +11,7 @@ module Chutney
|
|
11
11
|
example.rows.first.cells.map(&:value).each do |variable|
|
12
12
|
next if used?(variable, scenario)
|
13
13
|
|
14
|
-
add_issue(I18n.t('linters.unused_variable', variable:
|
14
|
+
add_issue(I18n.t('linters.unused_variable', variable:), feature, scenario, example)
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
data/lib/chutney/linter.rb
CHANGED
@@ -82,7 +82,7 @@ module Chutney
|
|
82
82
|
|
83
83
|
def add_issue(message, feature = nil, scenario = nil, item = nil)
|
84
84
|
issues << Lint.new(
|
85
|
-
message
|
85
|
+
message:,
|
86
86
|
gherkin_type: type(feature, scenario, item),
|
87
87
|
location: location(feature, scenario, item),
|
88
88
|
feature: feature&.name,
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
module LSP
|
5
|
+
# A wrapper that holds a response to the query message
|
6
|
+
# from the LSP client
|
7
|
+
class Result
|
8
|
+
attr_reader :id, :response
|
9
|
+
|
10
|
+
def initialize(response:, id: nil)
|
11
|
+
@id = id
|
12
|
+
@response = response
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
module LSP
|
5
|
+
# A minimalistic language server which will lint gherkin
|
6
|
+
# files on open and save
|
7
|
+
class Server
|
8
|
+
LSP_CONST = LanguageServer::Protocol::Constant
|
9
|
+
LSP_IO = LanguageServer::Protocol::Transport::Stdio
|
10
|
+
LSP_IF = LanguageServer::Protocol::Interface
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@writer = LSP_IO::Writer.new
|
14
|
+
@reader = LSP_IO::Reader.new
|
15
|
+
@mutex = Mutex.new
|
16
|
+
@incoming_queue = Thread::Queue.new
|
17
|
+
@outgoing_queue = Thread::Queue.new
|
18
|
+
|
19
|
+
@dispatcher = Thread.new do
|
20
|
+
while (message = @outgoing_queue.pop)
|
21
|
+
if message.is_a? Result
|
22
|
+
@mutex.synchronize { @writer.write(id: message.id, result: message.response) }
|
23
|
+
else
|
24
|
+
@mutex.synchronize { @writer.write(message.to_hash) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
@worker = Thread.new do
|
30
|
+
while (message = @incoming_queue.pop)
|
31
|
+
process_message(message)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
Thread.main.priority = 1
|
36
|
+
end
|
37
|
+
|
38
|
+
def process_message(message)
|
39
|
+
case message[:method]
|
40
|
+
when 'initialize'
|
41
|
+
run_initialize(message)
|
42
|
+
when 'initialized'
|
43
|
+
run_initialized
|
44
|
+
when 'textDocument/didOpen', 'textDocument/didSave'
|
45
|
+
run_did_change(message)
|
46
|
+
when 'textDocument/didClose'
|
47
|
+
# no-op
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def send_message(message)
|
52
|
+
return if outgoing_queue.closed?
|
53
|
+
|
54
|
+
outgoing_queue << message
|
55
|
+
end
|
56
|
+
|
57
|
+
def send_log(message, method: 'window/logMessage', error: false)
|
58
|
+
type = error ? LSP_CONST::MessageType::ERROR : LSP_CONST::MessageType::INFO
|
59
|
+
notification = LSP_IF::NotificationMessage.new(
|
60
|
+
method:,
|
61
|
+
jsonrpc: '2.0',
|
62
|
+
params: LSP_IF::ShowMessageParams.new(
|
63
|
+
type:,
|
64
|
+
message: "Chutney LSP [#{VERSION}]: #{message}"
|
65
|
+
)
|
66
|
+
)
|
67
|
+
send_message(notification)
|
68
|
+
end
|
69
|
+
|
70
|
+
def send_notification(message, error: false)
|
71
|
+
send_log(message, method: 'window/showMessage', error:)
|
72
|
+
end
|
73
|
+
|
74
|
+
def run_initialize(message)
|
75
|
+
initialize_result = LSP_IF::InitializeResult.new(
|
76
|
+
capabilities: LSP_IF::ServerCapabilities.new(
|
77
|
+
document_formatting_provider: true,
|
78
|
+
text_document_sync: LSP_IF::TextDocumentSyncOptions.new(
|
79
|
+
change: LSP_CONST::TextDocumentSyncKind::FULL,
|
80
|
+
open_close: true,
|
81
|
+
save: true
|
82
|
+
)
|
83
|
+
),
|
84
|
+
server_info: {
|
85
|
+
name: 'chutney-lsp',
|
86
|
+
version: VERSION
|
87
|
+
}
|
88
|
+
)
|
89
|
+
send_message(Result.new(id: message[:id], response: initialize_result))
|
90
|
+
send_log('Initializing')
|
91
|
+
end
|
92
|
+
|
93
|
+
def run_initialized
|
94
|
+
RubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)
|
95
|
+
send_notification('Chutney LSP Server up and running')
|
96
|
+
send_log('Initialized')
|
97
|
+
end
|
98
|
+
|
99
|
+
def run_did_change(message)
|
100
|
+
document = message.dig(:params, :textDocument)
|
101
|
+
filename = document[:uri].delete_prefix('file://')
|
102
|
+
send_log("Evaluating #{filename}")
|
103
|
+
linter = Chutney::ChutneyLint.new(*filename)
|
104
|
+
linter.configuration.quiet!
|
105
|
+
begin
|
106
|
+
offenses = linter.analyse.values.first.filter { |r| r[:issues].any? }
|
107
|
+
rescue StandardError => e
|
108
|
+
send_log("Could not parse #{filename} as Gherkin. Received: #{e.full_message}", error: true)
|
109
|
+
send_notification("Could not parse #{filename} as Gherkin.", error: true)
|
110
|
+
return
|
111
|
+
end
|
112
|
+
send_log("Found #{offenses.count} offenses")
|
113
|
+
diagnostics = offenses
|
114
|
+
.flat_map { |group| group[:issues].each { |issue| issue[:linter] = group[:linter] } }
|
115
|
+
.map { |offense| to_diagnostic(offense) }
|
116
|
+
send_message(diagnostic_message(document[:uri], diagnostics))
|
117
|
+
end
|
118
|
+
|
119
|
+
def diagnostic_message(file_uri, diagnostics)
|
120
|
+
{
|
121
|
+
method: 'textDocument/publishDiagnostics',
|
122
|
+
params: {
|
123
|
+
uri: file_uri,
|
124
|
+
diagnostics:
|
125
|
+
}
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
def to_diagnostic(offense)
|
130
|
+
code = offense[:linter]
|
131
|
+
message = offense[:message]
|
132
|
+
source = 'chutney'
|
133
|
+
{ code:, message:, source:, severity: 1, range: to_range(offense[:location]) }
|
134
|
+
end
|
135
|
+
|
136
|
+
def to_range(location)
|
137
|
+
{
|
138
|
+
start: { character: location.fetch(:column, 1) - 1, line: location.fetch(:line, 1) - 1 },
|
139
|
+
end: { character: 0, line: location.fetch(:line, 1) }
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
def shutdown
|
144
|
+
incoming_queue.clear
|
145
|
+
outgoing_queue.clear
|
146
|
+
incoming_queue.close
|
147
|
+
outgoing_queue.close
|
148
|
+
worker.join
|
149
|
+
dispatcher.join
|
150
|
+
send_log('Shutdown complete')
|
151
|
+
end
|
152
|
+
|
153
|
+
def start
|
154
|
+
reader.read do |message|
|
155
|
+
method = message[:method]
|
156
|
+
send_log("Received #{method}")
|
157
|
+
|
158
|
+
case method
|
159
|
+
when 'initialize', 'initialized', 'textDocument/didOpen', 'textDocument/didClose', 'textDocument/didSave'
|
160
|
+
incoming_queue.push(message)
|
161
|
+
when 'shutdown'
|
162
|
+
shutdown
|
163
|
+
when 'exit'
|
164
|
+
mutex.synchronize do
|
165
|
+
status = incoming_queue.closed? ? 0 : 1
|
166
|
+
exit(status)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
attr_reader :reader, :writer, :mutex, :incoming_queue, :outgoing_queue, :worker, :dispatcher
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
data/lib/chutney/lsp.rb
ADDED
data/lib/chutney/version.rb
CHANGED
data/lib/chutney.rb
CHANGED
@@ -47,10 +47,7 @@ require 'chutney/version'
|
|
47
47
|
|
48
48
|
require 'cuke_modeler'
|
49
49
|
require 'forwardable'
|
50
|
-
# require 'gherkin/dialect'
|
51
|
-
# require 'gherkin/parser'
|
52
50
|
require 'i18n'
|
53
|
-
require 'set'
|
54
51
|
require 'yaml'
|
55
52
|
|
56
53
|
module Chutney
|
@@ -91,6 +88,14 @@ module Chutney
|
|
91
88
|
end
|
92
89
|
|
93
90
|
def analyse
|
91
|
+
if configuration.respond_to?(:using_user_configuration?) &&
|
92
|
+
!configuration.quiet? &&
|
93
|
+
!configuration.using_user_configuration?
|
94
|
+
warn('Chutney: no local configuration found, using gem defaults. Run `chutney -l` to list enabled ' \
|
95
|
+
'enabled linters, `chutney --init` to install a local configuration file or `chutney --quiet` ' \
|
96
|
+
'to disable this message.')
|
97
|
+
end
|
98
|
+
|
94
99
|
files.each do |f|
|
95
100
|
lint(f)
|
96
101
|
end
|