chutney 3.6.0 → 3.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +1 -2
  3. data/.rubocop.yml +4 -3
  4. data/Gemfile +9 -0
  5. data/README.md +49 -3
  6. data/chutney.gemspec +8 -17
  7. data/examples/emoji.feature +1 -2
  8. data/exe/chutney +28 -2
  9. data/exe/chutney-lsp +13 -0
  10. data/img/happy_chutney.png +0 -0
  11. data/lib/chutney/configuration.rb +31 -10
  12. data/lib/chutney/formatter/pie_formatter.rb +2 -2
  13. data/lib/chutney/linter/inconsistent_quoting.rb +1 -1
  14. data/lib/chutney/linter/invalid_file_name.rb +1 -1
  15. data/lib/chutney/linter/same_tag_different_case.rb +1 -1
  16. data/lib/chutney/linter/same_tag_for_all_scenarios.rb +2 -2
  17. data/lib/chutney/linter/unique_scenario_names.rb +1 -1
  18. data/lib/chutney/linter/unused_variable.rb +1 -1
  19. data/lib/chutney/linter/use_outline.rb +1 -1
  20. data/lib/chutney/linter.rb +1 -1
  21. data/lib/chutney/lsp/result.rb +16 -0
  22. data/lib/chutney/lsp/server.rb +177 -0
  23. data/lib/chutney/lsp.rb +7 -0
  24. data/lib/chutney/version.rb +1 -1
  25. data/lib/chutney.rb +8 -3
  26. data/usechutney.com/.gitignore +5 -0
  27. data/usechutney.com/Gemfile +37 -0
  28. data/usechutney.com/_config.yml +298 -0
  29. data/usechutney.com/_data/navigation.yml +112 -0
  30. data/usechutney.com/_data/ui-text.yml +2132 -0
  31. data/usechutney.com/_posts/2024-09-23-welcome-to-jekyll.markdown +28 -0
  32. data/usechutney.com/assets/images/mr_pickle.png +0 -0
  33. data/usechutney.com/assets/images/pug.png +0 -0
  34. data/usechutney.com/docs/configuration/index.md +104 -0
  35. data/usechutney.com/docs/disabling-rules/index.md +11 -0
  36. data/usechutney.com/docs/installing/index.md +30 -0
  37. data/usechutney.com/docs/language-server/index.md +24 -0
  38. data/usechutney.com/docs/rules/avoid-full-stops/index.md +23 -0
  39. data/usechutney.com/docs/rules/avoid-outline-for-single-example/index.md +28 -0
  40. data/usechutney.com/docs/rules/avoid-scripting/index.md +33 -0
  41. data/usechutney.com/docs/rules/avoid-splat-steps-in-background/index.md +25 -0
  42. data/usechutney.com/docs/rules/avoid-splat-steps-in-scenarios/index.md +29 -0
  43. data/usechutney.com/docs/rules/avoid-typographers-quotes/index.md +24 -0
  44. data/usechutney.com/docs/rules/background-does-more-than-setup/index.md +28 -0
  45. data/usechutney.com/docs/rules/background-requires-multiple-scenarios/index.md +26 -0
  46. data/usechutney.com/docs/rules/bad-scenario-name/index.md +28 -0
  47. data/usechutney.com/docs/rules/empty-feature-file/index.md +7 -0
  48. data/usechutney.com/docs/rules/file-name-differs-feature-name/index.md +37 -0
  49. data/usechutney.com/docs/rules/givens-after-background/index.md +36 -0
  50. data/usechutney.com/docs/rules/inconsistent-quoting/index.md +29 -0
  51. data/usechutney.com/docs/rules/invalid-step-flow/index.md +31 -0
  52. data/usechutney.com/docs/rules/invalid_file_name/index.md +19 -0
  53. data/usechutney.com/docs/rules/missing-example-name/index.md +49 -0
  54. data/usechutney.com/docs/rules/missing-example-table/index.md +34 -0
  55. data/usechutney.com/docs/rules/missing-feature-description/index.md +25 -0
  56. data/usechutney.com/docs/rules/missing-feature-name/index.md +19 -0
  57. data/usechutney.com/docs/rules/missing-scenario-name/index.md +19 -0
  58. data/usechutney.com/docs/rules/missing-scenario-outline/index.md +39 -0
  59. data/usechutney.com/docs/rules/missing-test-action/index.md +28 -0
  60. data/usechutney.com/docs/rules/missing-test-verification/index.md +28 -0
  61. data/usechutney.com/docs/rules/required-tag-starts-with/index.md +31 -0
  62. data/usechutney.com/docs/rules/same-tag-different-case/index.md +42 -0
  63. data/usechutney.com/docs/rules/same-tag-for-all-scenarios/index.md +42 -0
  64. data/usechutney.com/docs/rules/scenario-names-match/index.md +30 -0
  65. data/usechutney.com/docs/rules/tag-used-multiple-times/index.md +32 -0
  66. data/usechutney.com/docs/rules/too-clumsy/index.md +78 -0
  67. data/usechutney.com/docs/rules/too-long-step/index.md +30 -0
  68. data/usechutney.com/docs/rules/too-many-different-tags/index.md +32 -0
  69. data/usechutney.com/docs/rules/too-many-steps/index.md +79 -0
  70. data/usechutney.com/docs/rules/too-many-tags/index.md +32 -0
  71. data/usechutney.com/docs/rules/unique-scenario-names/index.md +39 -0
  72. data/usechutney.com/docs/rules/unknown-variable/index.md +38 -0
  73. data/usechutney.com/docs/rules/unused-variable/index.md +37 -0
  74. data/usechutney.com/docs/rules/use-background/index.md +40 -0
  75. data/usechutney.com/docs/rules/use-outline/index.md +39 -0
  76. data/usechutney.com/docs/running/index.md +33 -0
  77. data/usechutney.com/pages/404.html +25 -0
  78. data/usechutney.com/pages/about/index.md +14 -0
  79. data/usechutney.com/pages/index.markdown +40 -0
  80. metadata +76 -114
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0badc9f82efb1e59bc755597c4ef3f7a4f973e37fec2fc25d4b93192319a75eb
4
- data.tar.gz: 9459b41ac94999efc721d07f54e8e14b3c83c4f00786f003937a65dd2054fb9f
3
+ metadata.gz: 0e46a3bdaaed4652c5e7aa856c15b40995602a17f5b0c0d5ff1258e4a59c6842
4
+ data.tar.gz: f47caf4ee93c1e74499a6f1291de4e95643f7beab1f8ec9cee555d2bd7298f17
5
5
  SHA512:
6
- metadata.gz: 404c7bd04be47d2c2fb6c71e9d44bc317b5d0a9abe1a3362945ce2841a76f9cc25eb7703cc39134c2065dfcc8c83d5f615f24e035160cd10aab09178e8f8d6fe
7
- data.tar.gz: 29150aae2c605d2ce489dd3a41b914ad7bc67dc53867122c7c11b67830d5fe82771a433d0b6250c926d0e7bd34c0fa8af53941c734e32d5d8db3f435153f8c0f
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: circleci/ruby:2.6.3-stretch-node
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
- - 'lib/chutney/linter/file_name_differs_feature_name.rb'
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/chutney.svg?sanitize=true" alt="Chutney" height="200">
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://billyruffian.github.io/chutney/).
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.add_development_dependency 'coveralls', '~> 0.8'
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  📚: 🤯
4
4
 
5
- As a 👹
5
+ As a 👹
6
6
  I want to 🏃‍♂️ 🥒
7
7
  So that 👁 don't 👀 a 🍆
8
8
 
@@ -11,4 +11,3 @@
11
11
  🎬 I am 😘 by a 👸
12
12
  🎬 I am 😘 by a 👸
13
13
  🙏 I will ⏎ into a 🤴
14
-
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(true)
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
- @path = path
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 configuration_path || '' if configuration_path
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
- Dir.glob(File.join(Dir.pwd, '**', fname))
26
- end.flatten
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
- config_file = config_files.first
29
- merge_config(config_file) if !config_file.nil? && File.exist?(config_file)
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: data, radius: 8, legend: { format: '%<label>s %<name>s %<value>i' })
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.each do |_file, linter|
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)/.freeze
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
@@ -15,7 +15,7 @@ module Chutney
15
15
  end
16
16
 
17
17
  def recommend(filename)
18
- File.basename(filename, '.*').gsub(/::/, '/')
18
+ File.basename(filename, '.*').gsub('::', '/')
19
19
  .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
20
20
  .gsub(/([a-z\d])([A-Z])/, '\1_\2')
21
21
  .tr('-', '_')
@@ -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: 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: 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: tag), feature, scenario)
40
+ tag:), feature, scenario)
41
41
  end
42
42
  end
43
43
  end
@@ -18,7 +18,7 @@ module Chutney
18
18
  def issue(name, first_location, scenario)
19
19
  add_issue(
20
20
  I18n.t('linters.unique_scenario_names',
21
- name: name,
21
+ name:,
22
22
  line: first_location[:line],
23
23
  column: first_location[:column]),
24
24
  feature, scenario
@@ -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: variable), feature, scenario, example)
14
+ add_issue(I18n.t('linters.unused_variable', variable:), feature, scenario, example)
15
15
  end
16
16
  end
17
17
  end
@@ -23,7 +23,7 @@ module Chutney
23
23
 
24
24
  def add_issue(lhs, rhs, pct)
25
25
  super(I18n.t('linters.use_outline',
26
- pct: pct,
26
+ pct:,
27
27
  lhs_name: lhs[:name],
28
28
  lhs_line: lhs[:reference][:line],
29
29
  rhs_name: rhs[:name],
@@ -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: 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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chutney
4
+ # The Chutney Language Server module wrapper
5
+ module LSP
6
+ end
7
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Chutney
4
- VERSION = '3.6.0'
4
+ VERSION = '3.8.0'
5
5
  end
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