dead_end 1.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7db29aed59a901a58a0b1ed50873b6f2c17692f7724ffad2736b99694b78ba0
4
- data.tar.gz: e1cf2a11fa38af85df30d89a559b0c6e5a74beec2c0add8b6f67dd142d793de1
3
+ metadata.gz: 72373898d38363c0ea0c3cec7fe2747399724ce55dae4eca1ce57b8f59dfc767
4
+ data.tar.gz: 9e99a5fddb8a054b839aef4fe1c2b87792bc9b4161bf5a6689156f2999dca935
5
5
  SHA512:
6
- metadata.gz: 444cfdfd7df93038d1714729a21a3b7a20f9000e9cf0541521d51a125d00be4968e83bbc8a51bf4c4689e2b99706cf5eb0032a318647ecb3fd4452a343798e7a
7
- data.tar.gz: c532b87160ae6231776b72ff68f3c8f940064051e14c27da28fa22a15c1be0060d9b2afd600bb66dd4571cd39b9372b1956782565040a5e159776f9dac9f9e62
6
+ metadata.gz: 7312a010846453f222bbb8717738831f9a2e23572eace345ed7738f00429c126d1a4760548ad9e7552237aee057b5c5bae90b7a0fe197144c3a9dc9d3db2ab0b
7
+ data.tar.gz: 0db2b356ab237f01eda6dc5ebeac0156c317d94535c2b59d099f766e1716d5d6f6b429edcc65bf439dea1bc15e1866f4d66a41cd28d927a29a666f54f54ee804
data/.circleci/config.yml CHANGED
@@ -13,6 +13,14 @@ references:
13
13
  command: bundle exec standardrb
14
14
 
15
15
  jobs:
16
+ "ruby-2-5":
17
+ docker:
18
+ - image: circleci/ruby:2.5
19
+ steps:
20
+ - checkout
21
+ - ruby/install-deps
22
+ - <<: *unit
23
+
16
24
  "ruby-2-6":
17
25
  docker:
18
26
  - image: circleci/ruby:2.6
@@ -49,6 +57,7 @@ workflows:
49
57
  version: 2
50
58
  build:
51
59
  jobs:
60
+ - "ruby-2-5"
52
61
  - "ruby-2-6"
53
62
  - "ruby-2-7"
54
63
  - "ruby-3-0"
@@ -1,13 +1,20 @@
1
1
  name: Check Changelog
2
2
 
3
3
  on:
4
- pull_request:
5
- types: [opened, reopened, edited, synchronize]
4
+ pull_request:
5
+ types: [opened, reopened, edited, labeled, unlabeled, synchronize]
6
+
6
7
  jobs:
7
- build:
8
+ check-changelog:
8
9
  runs-on: ubuntu-latest
10
+ if: |
11
+ !contains(github.event.pull_request.body, '[skip changelog]') &&
12
+ !contains(github.event.pull_request.body, '[changelog skip]') &&
13
+ !contains(github.event.pull_request.body, '[skip ci]') &&
14
+ !contains(github.event.pull_request.labels.*.name, 'skip changelog')
9
15
  steps:
10
- - uses: actions/checkout@v1
11
- - name: Check that CHANGELOG is touched
12
- run: |
13
- cat $GITHUB_EVENT_PATH | jq .pull_request.title | grep -i '\[\(\(changelog skip\)\|\(ci skip\)\)\]' || git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md
16
+ - uses: actions/checkout@v2.3.5
17
+ - name: Check that CHANGELOG is touched
18
+ run: |
19
+ git fetch origin ${{ github.base_ref }} --depth 1 && \
20
+ git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md
data/.standard.yml CHANGED
@@ -1 +1 @@
1
- ruby_version: 2.6.6
1
+ ruby_version: 2.5.9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  ## HEAD (unreleased)
2
2
 
3
+ ## 3.0.0
4
+
5
+ - [Breaking] Remove previously deprecated `require "dead_end/fyi"` interface (https://github.com/zombocom/dead_end/pull/94)
6
+ - Fix double output bug (https://github.com/zombocom/dead_end/pull/99)
7
+ - Fix bug causing poor results (fix #95, fix #88) (https://github.com/zombocom/dead_end/pull/96)
8
+ - DeadEnd is now fired on EVERY syntax error (https://github.com/zombocom/dead_end/pull/94)
9
+ - Output format changes:
10
+ - Parse errors emitted per-block rather than for the whole document (https://github.com/zombocom/dead_end/pull/94)
11
+ - The "banner" is now based on lexical analysis rather than parser regex (fix #68, fix #87) (https://github.com/zombocom/dead_end/pull/96)
12
+
13
+ ## 2.0.2
14
+
15
+ - Don't print terminal color codes when output is not tty (https://github.com/zombocom/dead_end/pull/91)
16
+
17
+ ## 2.0.1
18
+
19
+ - Reintroduce Ruby 2.5 support (https://github.com/zombocom/dead_end/pull/90)
20
+ - Support naked braces/brackets/parens, invert labels on banner (https://github.com/zombocom/dead_end/pull/89)
21
+ - Handle mismatched end when using rescue without begin (https://github.com/zombocom/dead_end/pull/83)
22
+ - CLI returns non-zero exit code when syntax error is found (https://github.com/zombocom/dead_end/pull/86)
23
+ - Let -v respond with gem version instead of 'unknown' (https://github.com/zombocom/dead_end/pull/82)
24
+
25
+ ## 2.0.0
26
+
27
+ - Support "endless" oneline method definitions for Ruby 3+ (https://github.com/zombocom/dead_end/pull/80)
28
+ - Reduce timeout to 1 second (https://github.com/zombocom/dead_end/pull/79)
29
+ - Logically consecutive lines (such as chained methods are now joined) (https://github.com/zombocom/dead_end/pull/78)
30
+ - Output improvement for cases where the only line is an single `end` (https://github.com/zombocom/dead_end/pull/78)
31
+
3
32
  ## 1.2.0
4
33
 
5
34
  - Output improvements via less greedy unmatched kw capture https://github.com/zombocom/dead_end/pull/73
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dead_end (1.2.0)
4
+ dead_end (3.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -60,4 +60,4 @@ DEPENDENCIES
60
60
  standard
61
61
 
62
62
  BUNDLED WITH
63
- 2.2.27
63
+ 2.2.29
data/README.md CHANGED
@@ -2,19 +2,14 @@
2
2
 
3
3
  An error in your code forces you to stop. DeadEnd helps you find those errors to get you back on your way faster.
4
4
 
5
- DeadEnd: Unmatched `end` detected
6
-
7
- This code has an unmatched `end`. Ensure that all `end` lines
8
- in your code have a matching syntax keyword (`def`, `do`, etc.)
9
- and that you don't have any extra `end` lines.
10
-
11
- file: path/to/dog.rb
12
- simplified:
5
+ ```
6
+ Unmatched `end', missing keyword (`do', `def`, `if`, etc.) ?
13
7
 
14
- 3 class Dog
15
- 5 defbark
16
- 7 end
17
- 12 end
8
+ 1 class Dog
9
+ 2 defbark
10
+ 4 end
11
+ 5 end
12
+ ```
18
13
 
19
14
  ## Installation in your codebase
20
15
 
@@ -52,34 +47,99 @@ This gives you the CLI command `$ dead_end` for more info run `$ dead_end --help
52
47
 
53
48
  ## What syntax errors does it handle?
54
49
 
50
+ Dead end will fire against all syntax errors and can isolate any syntax error. In addition, dead_end attempts to produce human readable descriptions of what needs to be done to resolve the issue. For example:
51
+
55
52
  - Missing `end`:
56
53
 
54
+ <!--
57
55
  ```ruby
58
56
  class Dog
59
57
  def bark
60
58
  puts "bark"
61
-
62
- def woof
63
- puts "woof"
64
- end
65
59
  end
66
- # => scratch.rb:8: syntax error, unexpected end-of-input, expecting `end'
67
60
  ```
61
+ -->
68
62
 
69
- - Unexpected `end`
63
+ ```
64
+ Unmatched keyword, missing `end' ?
70
65
 
66
+ ❯ 1 class Dog
67
+ ❯ 2 def bark
68
+ ❯ 4 end
69
+ ```
70
+
71
+ - Missing keyword
72
+ <!--
71
73
  ```ruby
72
74
  class Dog
73
75
  def speak
74
- @sounds.each |sound| # Note the missing `do` here
76
+ @sounds.each |sound|
75
77
  puts sound
76
78
  end
77
79
  end
78
80
  end
79
- # => scratch.rb:7: syntax error, unexpected `end', expecting end-of-input
80
81
  ```
82
+ -->
81
83
 
82
- As well as unmatched `|` and unmatched `}`. These errors can be time consuming to debug because Ruby often only tells you the last line in the file. The command `ruby -wc path/to/file.rb` can narrow it down a little bit, but this library does a better job.
84
+ ```
85
+ Unmatched `end', missing keyword (`do', `def`, `if`, etc.) ?
86
+
87
+ 1 class Dog
88
+ 2 def speak
89
+ ❯ 3 @sounds.each |sound|
90
+ ❯ 5 end
91
+ 6 end
92
+ 7 end
93
+ ```
94
+
95
+ - Missing pair characters (like `{}`, `[]`, `()` , or `|<var>|`)
96
+ <!--
97
+
98
+ ```ruby
99
+ class Dog
100
+ def speak(sound
101
+ puts sound
102
+ end
103
+ end
104
+ ```
105
+ -->
106
+
107
+ ```
108
+ Unmatched `(', missing `)' ?
109
+
110
+ 1 class Dog
111
+ ❯ 2 def speak(sound
112
+ ❯ 4 end
113
+ 5 end
114
+ ```
115
+
116
+ - Any ambiguous or unknown errors will be annotated by the original ripper error output:
117
+
118
+ <!--
119
+ class Dog
120
+ def meals_last_month
121
+ puts 3 *
122
+ end
123
+ end
124
+ -->
125
+
126
+ ```
127
+ syntax error, unexpected end-of-input
128
+
129
+ 1 class Dog
130
+ 2 def meals_last_month
131
+ ❯ 3 puts 3 *
132
+ 4 end
133
+ 5 end
134
+ ```
135
+
136
+ ## How is it better than `ruby -wc`?
137
+
138
+ Ruby allows you to syntax check a file with warnings using `ruby -wc`. This emits a parser error instead of a human focused error. Ruby's parse errors attempt to narrow down the location and can tell you if there is a glaring indentation error involving `end`.
139
+
140
+ The `dead_end` algorithm doesn't just guess at the location of syntax errors, it re-parses the document to prove that it captured them.
141
+
142
+ This library focuses on the human side of syntax errors. It cares less about why the document could not be parsed (computer problem) and more on what the programmer needs (human problem) to fix the problem.
83
143
 
84
144
  ## Sounds cool, but why isn't this baked into Ruby directly?
85
145
 
@@ -105,6 +165,14 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
105
165
 
106
166
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
107
167
 
168
+ ### How to debug changes to output display
169
+
170
+ You can see changes to output against a variety of invalid code by running specs and using the `DEBUG_DISPLAY=1` environment variable. For example:
171
+
172
+ ```
173
+ $ DEBUG_DISPLAY=1 be rspec spec/ --format=failures
174
+ ```
175
+
108
176
  ## Contributing
109
177
 
110
178
  Bug reports and pull requests are welcome on GitHub at https://github.com/zombocom/dead_end. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/zombocom/dead_end/blob/master/CODE_OF_CONDUCT.md).
data/exe/dead_end CHANGED
@@ -1,70 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "pathname"
4
- require "optparse"
5
3
  require_relative "../lib/dead_end"
6
4
 
7
- options = {}
8
- options[:terminal] = true
9
- options[:record_dir] = ENV["DEAD_END_RECORD_DIR"]
10
-
11
- parser = OptionParser.new do |opts|
12
- opts.banner = <<~EOM
13
- Usage: dead_end <file> [options]
14
-
15
- Parses a ruby source file and searches for syntax error(s) such as
16
- unexpected `end', expecting end-of-input.
17
-
18
- Example:
19
-
20
- $ dead_end dog.rb
21
-
22
- # ...
23
-
24
- ❯ 10 defdog
25
- ❯ 15 end
26
- ❯ 16
27
-
28
- Env options:
29
-
30
- DEAD_END_RECORD_DIR=<dir>
31
-
32
- When enabled, records the steps used to search for a syntax error to the
33
- given directory
34
-
35
- Options:
36
- EOM
37
-
38
- opts.on("--help", "Help - displays this message") do |v|
39
- puts opts
40
- exit
41
- end
42
-
43
- opts.on("--record <dir>", "When enabled, records the steps used to search for a syntax error to the given directory") do |v|
44
- options[:record_dir] = v
45
- end
46
-
47
- opts.on("--no-terminal", "Disable terminal highlighting") do |v|
48
- options[:terminal] = false
49
- end
50
- end
51
- parser.parse!
52
-
53
- file = ARGV[0]
54
-
55
- if file.nil? || file.empty?
56
- # Display help if raw command
57
- parser.parse! %w[--help]
58
- end
59
-
60
- file = Pathname(file)
61
- options[:record_dir] = "tmp" if ENV["DEBUG"]
62
-
63
- warn "Record dir: #{options[:record_dir]}" if options[:record_dir]
64
-
65
- DeadEnd.call(
66
- source: file.read,
67
- filename: file.expand_path,
68
- terminal: options[:terminal],
69
- record_dir: options[:record_dir]
70
- )
5
+ DeadEnd::Cli.new(
6
+ argv: ARGV
7
+ ).call
@@ -9,10 +9,10 @@ module DeadEnd
9
9
  #
10
10
  # Example:
11
11
  #
12
- # def dog
13
- # puts "bark"
14
- # puts "bark"
15
- # end
12
+ # def dog # 1
13
+ # puts "bark" # 2
14
+ # puts "bark" # 3
15
+ # end # 4
16
16
  #
17
17
  # scan = AroundBlockScan.new(
18
18
  # code_lines: code_lines
@@ -22,7 +22,7 @@ module DeadEnd
22
22
  # scan.scan_while { true }
23
23
  #
24
24
  # puts scan.before_index # => 0
25
- # puts scan.after_index # => 3
25
+ # puts scan.after_index # => 3
26
26
  #
27
27
  # Contents can also be filtered using AroundBlockScan#skip
28
28
  #
@@ -109,8 +109,6 @@ module DeadEnd
109
109
  kw_count = 0
110
110
  end_count = 0
111
111
  after_lines.each do |line|
112
- # puts "line: #{line.number} #{line.original_line}, indent: #{line.indent}, #{line.empty?} #{line.indent == @orig_indent}"
113
-
114
112
  next if line.empty?
115
113
  break if line.indent < @orig_indent
116
114
  next if line.indent != @orig_indent
@@ -124,7 +122,6 @@ module DeadEnd
124
122
 
125
123
  lines << line
126
124
  end
127
- lines.select! { |line| !line.is_comment? }
128
125
 
129
126
  lines
130
127
  end
@@ -197,7 +194,7 @@ module DeadEnd
197
194
  end
198
195
 
199
196
  private def after_lines
200
- @code_lines[after_index.next..] || []
197
+ @code_lines[after_index.next..-1] || []
201
198
  end
202
199
  end
203
200
  end
data/lib/dead_end/auto.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../dead_end/internals"
3
+ require_relative "../dead_end"
4
4
 
5
5
  # Monkey patch kernel to ensure that all `require` calls call the same
6
6
  # method
@@ -33,23 +33,3 @@ module Kernel
33
33
  DeadEnd.handle_error(e)
34
34
  end
35
35
  end
36
-
37
- # I honestly have no idea why this Object delegation is needed
38
- # I keep staring at bootsnap and it doesn't have to do this
39
- # is there a bug in their implementation they haven't caught or
40
- # am I doing something different?
41
- class Object
42
- private
43
-
44
- def load(path, wrap = false)
45
- Kernel.load(path, wrap)
46
- rescue SyntaxError => e
47
- DeadEnd.handle_error(e)
48
- end
49
-
50
- def require(path)
51
- Kernel.require(path)
52
- rescue SyntaxError => e
53
- DeadEnd.handle_error(e)
54
- end
55
- end
@@ -1,13 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeadEnd
4
- # Given a block, this method will capture surrounding
5
- # code to give the user more context for the location of
6
- # the problem.
4
+ # Turns a "invalid block(s)" into useful context
7
5
  #
8
- # Return is an array of CodeLines to be rendered.
6
+ # There are three main phases in the algorithm:
9
7
  #
10
- # Surrounding code is captured regardless of visible state
8
+ # 1. Sanitize/format input source
9
+ # 2. Search for invalid blocks
10
+ # 3. Format invalid blocks into something meaninful
11
+ #
12
+ # This class handles the third part.
13
+ #
14
+ # The algorithm is very good at capturing all of a syntax
15
+ # error in a single block in number 2, however the results
16
+ # can contain ambiguities. Humans are good at pattern matching
17
+ # and filtering and can mentally remove extraneous data, but
18
+ # they can't add extra data that's not present.
19
+ #
20
+ # In the case of known ambiguious cases, this class adds context
21
+ # back to the ambiguitiy so the programmer has full information.
22
+ #
23
+ # Beyond handling these ambiguities, it also captures surrounding
24
+ # code context information:
11
25
  #
12
26
  # puts block.to_s # => "def bark"
13
27
  #
@@ -16,7 +30,8 @@ module DeadEnd
16
30
  # code_lines: code_lines
17
31
  # )
18
32
  #
19
- # puts context.call.join
33
+ # lines = context.call.map(&:original)
34
+ # puts lines.join
20
35
  # # =>
21
36
  # class Dog
22
37
  # def bark
@@ -34,19 +49,34 @@ module DeadEnd
34
49
 
35
50
  def call
36
51
  @blocks.each do |block|
52
+ capture_first_kw_end_same_indent(block)
37
53
  capture_last_end_same_indent(block)
38
54
  capture_before_after_kws(block)
39
55
  capture_falling_indent(block)
40
56
  end
41
57
 
42
58
  @lines_to_output.select!(&:not_empty?)
43
- @lines_to_output.select!(&:not_comment?)
44
59
  @lines_to_output.uniq!
45
60
  @lines_to_output.sort!
46
61
 
47
62
  @lines_to_output
48
63
  end
49
64
 
65
+ # Shows the context around code provided by "falling" indentation
66
+ #
67
+ # Converts:
68
+ #
69
+ # it "foo" do
70
+ #
71
+ # into:
72
+ #
73
+ # class OH
74
+ # def hello
75
+ # it "foo" do
76
+ # end
77
+ # end
78
+ #
79
+ #
50
80
  def capture_falling_indent(block)
51
81
  AroundBlockScan.new(
52
82
  block: block,
@@ -56,7 +86,36 @@ module DeadEnd
56
86
  end
57
87
  end
58
88
 
89
+ # Shows surrounding kw/end pairs
90
+ #
91
+ # The purpose of showing these extra pairs is due to cases
92
+ # of ambiguity when only one visible line is matched.
93
+ #
94
+ # For example:
95
+ #
96
+ # 1 class Dog
97
+ # 2 def bark
98
+ # 4 def eat
99
+ # 5 end
100
+ # 6 end
101
+ #
102
+ # In this case either line 2 could be missing an `end` or
103
+ # line 4 was an extra line added by mistake (it happens).
104
+ #
105
+ # When we detect the above problem it shows the issue
106
+ # as only being on line 2
107
+ #
108
+ # 2 def bark
109
+ #
110
+ # Showing "neighbor" keyword pairs gives extra context:
111
+ #
112
+ # 2 def bark
113
+ # 4 def eat
114
+ # 5 end
115
+ #
59
116
  def capture_before_after_kws(block)
117
+ return unless block.visible_lines.count == 1
118
+
60
119
  around_lines = AroundBlockScan.new(code_lines: @code_lines, block: block)
61
120
  .start_at_next_line
62
121
  .capture_neighbor_context
@@ -66,9 +125,10 @@ module DeadEnd
66
125
  @lines_to_output.concat(around_lines)
67
126
  end
68
127
 
69
- # When there is an invalid with a keyword
70
- # right before an end, it's unclear where
71
- # the correct code should be.
128
+ # When there is an invalid block with a keyword
129
+ # missing an end right before another end,
130
+ # it is unclear where which keyword is missing the
131
+ # end
72
132
  #
73
133
  # Take this example:
74
134
  #
@@ -87,20 +147,21 @@ module DeadEnd
87
147
  # line 4. Also work backwards and if there's a mis-matched keyword, show it
88
148
  # too
89
149
  def capture_last_end_same_indent(block)
90
- start_index = block.visible_lines.first.index
91
- lines = @code_lines[start_index..block.lines.last.index]
150
+ return if block.visible_lines.length != 1
151
+ return unless block.visible_lines.first.is_kw?
152
+
153
+ visible_line = block.visible_lines.first
154
+ lines = @code_lines[visible_line.index..block.lines.last.index]
92
155
 
93
156
  # Find first end with same indent
94
157
  # (this would return line 4)
95
158
  #
96
159
  # end # 4
97
- matching_end = lines.find { |line| line.indent == block.current_indent && line.is_end? }
160
+ matching_end = lines.detect { |line| line.indent == block.current_indent && line.is_end? }
98
161
  return unless matching_end
99
162
 
100
163
  @lines_to_output << matching_end
101
164
 
102
- lines = @code_lines[start_index..matching_end.index]
103
-
104
165
  # Work backwards from the end to
105
166
  # see if there are mis-matched
106
167
  # keyword/end pairs
@@ -113,7 +174,7 @@ module DeadEnd
113
174
  # end # 4
114
175
  end_count = 0
115
176
  kw_count = 0
116
- kw_line = lines.reverse.detect do |line|
177
+ kw_line = @code_lines[visible_line.index..matching_end.index].reverse.detect do |line|
117
178
  end_count += 1 if line.is_end?
118
179
  kw_count += 1 if line.is_kw?
119
180
 
@@ -122,5 +183,51 @@ module DeadEnd
122
183
  return unless kw_line
123
184
  @lines_to_output << kw_line
124
185
  end
186
+
187
+ # The logical inverse of `capture_last_end_same_indent`
188
+ #
189
+ # When there is an invalid block with an `end`
190
+ # missing a keyword right after another `end`,
191
+ # it is unclear where which end is missing the
192
+ # keyword.
193
+ #
194
+ # Take this example:
195
+ #
196
+ # class Dog # 1
197
+ # puts "woof" # 2
198
+ # end # 3
199
+ # end # 4
200
+ #
201
+ # the problem line will be identified as:
202
+ #
203
+ # ❯ end # 4
204
+ #
205
+ # This happens because lines 1, 2, and 3 are technically valid code and are expanded
206
+ # first, deemed valid, and hidden. We need to un-hide the matching keyword on
207
+ # line 1. Also work backwards and if there's a mis-matched end, show it
208
+ # too
209
+ def capture_first_kw_end_same_indent(block)
210
+ return if block.visible_lines.length != 1
211
+ return unless block.visible_lines.first.is_end?
212
+
213
+ visible_line = block.visible_lines.first
214
+ lines = @code_lines[block.lines.first.index..visible_line.index]
215
+ matching_kw = lines.reverse.detect { |line| line.indent == block.current_indent && line.is_kw? }
216
+ return unless matching_kw
217
+
218
+ @lines_to_output << matching_kw
219
+
220
+ kw_count = 0
221
+ end_count = 0
222
+ orphan_end = @code_lines[matching_kw.index..visible_line.index].detect do |line|
223
+ kw_count += 1 if line.is_kw?
224
+ end_count += 1 if line.is_end?
225
+
226
+ end_count >= kw_count
227
+ end
228
+
229
+ return unless orphan_end
230
+ @lines_to_output << orphan_end
231
+ end
125
232
  end
126
233
  end