dead_end 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.circleci/config.yml +41 -0
- data/.github/workflows/check_changelog.yml +13 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +39 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +36 -0
- data/LICENSE.txt +21 -0
- data/README.md +122 -0
- data/Rakefile +8 -0
- data/assets/syntax_search.gif +0 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/dead_end.gemspec +28 -0
- data/exe/dead_end +70 -0
- data/lib/dead_end.rb +4 -0
- data/lib/dead_end/around_block_scan.rb +193 -0
- data/lib/dead_end/auto.rb +51 -0
- data/lib/dead_end/block_expand.rb +74 -0
- data/lib/dead_end/capture_code_context.rb +62 -0
- data/lib/dead_end/code_block.rb +78 -0
- data/lib/dead_end/code_frontier.rb +151 -0
- data/lib/dead_end/code_line.rb +139 -0
- data/lib/dead_end/code_search.rb +157 -0
- data/lib/dead_end/display_code_with_line_numbers.rb +71 -0
- data/lib/dead_end/display_invalid_blocks.rb +122 -0
- data/lib/dead_end/fyi.rb +7 -0
- data/lib/dead_end/heredoc_block_parse.rb +30 -0
- data/lib/dead_end/internals.rb +156 -0
- data/lib/dead_end/lex_all.rb +58 -0
- data/lib/dead_end/parse_blocks_from_indent_line.rb +56 -0
- data/lib/dead_end/trailing_slash_join.rb +53 -0
- data/lib/dead_end/version.rb +5 -0
- data/lib/dead_end/who_dis_syntax_error.rb +66 -0
- metadata +83 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeadEnd
|
4
|
+
# Outputs code with highlighted lines
|
5
|
+
#
|
6
|
+
# Whatever is passed to this class will be rendered
|
7
|
+
# even if it is "marked invisible" any filtering of
|
8
|
+
# output should be done before calling this class.
|
9
|
+
#
|
10
|
+
#
|
11
|
+
# DisplayCodeWithLineNumbers.new(
|
12
|
+
# lines: lines,
|
13
|
+
# highlight_lines: [lines[2], lines[3]]
|
14
|
+
# ).call
|
15
|
+
# # =>
|
16
|
+
# 1
|
17
|
+
# 2 def cat
|
18
|
+
# ❯ 3 Dir.chdir
|
19
|
+
# ❯ 4 end
|
20
|
+
# 5 end
|
21
|
+
# 6
|
22
|
+
class DisplayCodeWithLineNumbers
|
23
|
+
TERMINAL_HIGHLIGHT = "\e[1;3m" # Bold, italics
|
24
|
+
TERMINAL_END = "\e[0m"
|
25
|
+
|
26
|
+
def initialize(lines: , highlight_lines: [], terminal: false)
|
27
|
+
@lines = Array(lines).sort
|
28
|
+
@terminal = terminal
|
29
|
+
@highlight_line_hash = Array(highlight_lines).each_with_object({}) {|line, h| h[line] = true }
|
30
|
+
@digit_count = @lines.last&.line_number.to_s.length
|
31
|
+
end
|
32
|
+
|
33
|
+
def call
|
34
|
+
@lines.map do |line|
|
35
|
+
format_line(line)
|
36
|
+
end.join
|
37
|
+
end
|
38
|
+
|
39
|
+
private def format_line(code_line)
|
40
|
+
# Handle trailing slash lines
|
41
|
+
code_line.original.lines.map.with_index do |contents, i|
|
42
|
+
format(
|
43
|
+
empty: code_line.empty?,
|
44
|
+
number: (code_line.number + i).to_s,
|
45
|
+
contents: contents,
|
46
|
+
highlight: @highlight_line_hash[code_line]
|
47
|
+
)
|
48
|
+
end.join
|
49
|
+
end
|
50
|
+
|
51
|
+
private def format(contents: , number: , highlight: false, empty:)
|
52
|
+
string = String.new("")
|
53
|
+
if highlight
|
54
|
+
string << "❯ "
|
55
|
+
else
|
56
|
+
string << " "
|
57
|
+
end
|
58
|
+
|
59
|
+
string << number.rjust(@digit_count).to_s
|
60
|
+
if empty
|
61
|
+
string << contents
|
62
|
+
else
|
63
|
+
string << " "
|
64
|
+
string << TERMINAL_HIGHLIGHT if @terminal && highlight
|
65
|
+
string << contents
|
66
|
+
string << TERMINAL_END if @terminal
|
67
|
+
end
|
68
|
+
string
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "capture_code_context"
|
4
|
+
require_relative "display_code_with_line_numbers"
|
5
|
+
|
6
|
+
module DeadEnd
|
7
|
+
# Used for formatting invalid blocks
|
8
|
+
class DisplayInvalidBlocks
|
9
|
+
attr_reader :filename
|
10
|
+
|
11
|
+
def initialize(code_lines: ,blocks:, io: $stderr, filename: nil, terminal: false, invalid_obj: WhoDisSyntaxError::Null.new)
|
12
|
+
@terminal = terminal
|
13
|
+
@filename = filename
|
14
|
+
@io = io
|
15
|
+
|
16
|
+
@blocks = Array(blocks)
|
17
|
+
|
18
|
+
@invalid_lines = @blocks.map(&:lines).flatten
|
19
|
+
@code_lines = code_lines
|
20
|
+
|
21
|
+
@invalid_obj = invalid_obj
|
22
|
+
end
|
23
|
+
|
24
|
+
def call
|
25
|
+
if @blocks.any? { |b| !b.hidden? }
|
26
|
+
found_invalid_blocks
|
27
|
+
else
|
28
|
+
@io.puts "Syntax OK"
|
29
|
+
end
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
private def no_invalid_blocks
|
34
|
+
@io.puts <<~EOM
|
35
|
+
EOM
|
36
|
+
end
|
37
|
+
|
38
|
+
private def found_invalid_blocks
|
39
|
+
@io.puts
|
40
|
+
@io.puts banner
|
41
|
+
@io.puts
|
42
|
+
@io.puts("file: #{filename}") if filename
|
43
|
+
@io.puts <<~EOM
|
44
|
+
simplified:
|
45
|
+
|
46
|
+
#{indent(code_block)}
|
47
|
+
EOM
|
48
|
+
end
|
49
|
+
|
50
|
+
def banner
|
51
|
+
case @invalid_obj.error_symbol
|
52
|
+
when :missing_end
|
53
|
+
<<~EOM
|
54
|
+
DeadEnd: Missing `end` detected
|
55
|
+
|
56
|
+
This code has a missing `end`. Ensure that all
|
57
|
+
syntax keywords (`def`, `do`, etc.) have a matching `end`.
|
58
|
+
EOM
|
59
|
+
when :unmatched_syntax
|
60
|
+
case @invalid_obj.unmatched_symbol
|
61
|
+
when :end
|
62
|
+
<<~EOM
|
63
|
+
DeadEnd: Unmatched `end` detected
|
64
|
+
|
65
|
+
This code has an unmatched `end`. Ensure that all `end` lines
|
66
|
+
in your code have a matching syntax keyword (`def`, `do`, etc.)
|
67
|
+
and that you don't have any extra `end` lines.
|
68
|
+
EOM
|
69
|
+
when :|
|
70
|
+
<<~EOM
|
71
|
+
DeadEnd: Unmatched `|` character detected
|
72
|
+
|
73
|
+
Example:
|
74
|
+
|
75
|
+
`do |x` should be `do |x|`
|
76
|
+
EOM
|
77
|
+
when :"}"
|
78
|
+
<<~EOM
|
79
|
+
DeadEnd: Unmatched `}` character detected
|
80
|
+
|
81
|
+
This code has an unmatched `}`. Ensure that opening curl braces are
|
82
|
+
closed: `{ }`.
|
83
|
+
EOM
|
84
|
+
else
|
85
|
+
"DeadEnd: Unmatched #{@invalid_obj.unmatched_symbol}` detected"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
def indent(string, with: " ")
|
92
|
+
string.each_line.map {|l| with + l }.join
|
93
|
+
end
|
94
|
+
|
95
|
+
def code_block
|
96
|
+
string = String.new("")
|
97
|
+
string << code_with_context
|
98
|
+
string
|
99
|
+
end
|
100
|
+
|
101
|
+
def code_with_context
|
102
|
+
lines = CaptureCodeContext.new(
|
103
|
+
blocks: @blocks,
|
104
|
+
code_lines: @code_lines
|
105
|
+
).call
|
106
|
+
|
107
|
+
DisplayCodeWithLineNumbers.new(
|
108
|
+
lines: lines,
|
109
|
+
terminal: @terminal,
|
110
|
+
highlight_lines: @invalid_lines,
|
111
|
+
).call
|
112
|
+
end
|
113
|
+
|
114
|
+
def code_with_lines
|
115
|
+
DisplayCodeWithLineNumbers.new(
|
116
|
+
lines: @code_lines.select(&:visible?),
|
117
|
+
terminal: @terminal,
|
118
|
+
highlight_lines: @invalid_lines,
|
119
|
+
).call
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
data/lib/dead_end/fyi.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeadEnd
|
4
|
+
# Takes in a source, and returns blocks containing each heredoc
|
5
|
+
class HeredocBlockParse
|
6
|
+
private; attr_reader :code_lines, :lex; public
|
7
|
+
|
8
|
+
def initialize(source:, code_lines: )
|
9
|
+
@code_lines = code_lines
|
10
|
+
@lex = LexAll.new(source: source)
|
11
|
+
end
|
12
|
+
|
13
|
+
def call
|
14
|
+
blocks = []
|
15
|
+
beginning = []
|
16
|
+
@lex.each do |lex|
|
17
|
+
case lex.type
|
18
|
+
when :on_heredoc_beg
|
19
|
+
beginning << lex.line
|
20
|
+
when :on_heredoc_end
|
21
|
+
start_index = beginning.pop - 1
|
22
|
+
end_index = lex.line - 1
|
23
|
+
blocks << CodeBlock.new(lines: code_lines[start_index..end_index])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
blocks
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# This is the top level file, but is moved to `internals`
|
4
|
+
# so the top level file can instead enable the "automatic" behavior
|
5
|
+
|
6
|
+
require_relative "version"
|
7
|
+
|
8
|
+
require 'tmpdir'
|
9
|
+
require 'stringio'
|
10
|
+
require 'pathname'
|
11
|
+
require 'ripper'
|
12
|
+
require 'timeout'
|
13
|
+
|
14
|
+
module DeadEnd
|
15
|
+
class Error < StandardError; end
|
16
|
+
SEARCH_SOURCE_ON_ERROR_DEFAULT = true
|
17
|
+
TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 5).to_i
|
18
|
+
|
19
|
+
def self.handle_error(e, search_source_on_error: SEARCH_SOURCE_ON_ERROR_DEFAULT)
|
20
|
+
raise e if !e.message.include?("end-of-input")
|
21
|
+
|
22
|
+
filename = e.message.split(":").first
|
23
|
+
|
24
|
+
$stderr.sync = true
|
25
|
+
$stderr.puts "Run `$ dead_end #{filename}` for more options\n"
|
26
|
+
|
27
|
+
if search_source_on_error
|
28
|
+
self.call(
|
29
|
+
source: Pathname(filename).read,
|
30
|
+
filename: filename,
|
31
|
+
terminal: true,
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
$stderr.puts ""
|
36
|
+
$stderr.puts ""
|
37
|
+
raise e
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.call(source: , filename: , terminal: false, record_dir: nil, timeout: TIMEOUT_DEFAULT)
|
41
|
+
search = nil
|
42
|
+
Timeout.timeout(timeout) do
|
43
|
+
search = CodeSearch.new(source, record_dir: record_dir).call
|
44
|
+
end
|
45
|
+
|
46
|
+
blocks = search.invalid_blocks
|
47
|
+
DisplayInvalidBlocks.new(
|
48
|
+
blocks: blocks,
|
49
|
+
filename: filename,
|
50
|
+
terminal: terminal,
|
51
|
+
code_lines: search.code_lines,
|
52
|
+
invalid_obj: invalid_type(source),
|
53
|
+
io: $stderr
|
54
|
+
).call
|
55
|
+
rescue Timeout::Error
|
56
|
+
$stderr.puts "Search timed out DEAD_END_TIMEOUT=#{timeout}, run with DEBUG=1 for more info"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Used for counting spaces
|
60
|
+
module SpaceCount
|
61
|
+
def self.indent(string)
|
62
|
+
string.split(/\S/).first&.length || 0
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# This will tell you if the `code_lines` would be valid
|
67
|
+
# if you removed the `without_lines`. In short it's a
|
68
|
+
# way to detect if we've found the lines with syntax errors
|
69
|
+
# in our document yet.
|
70
|
+
#
|
71
|
+
# code_lines = [
|
72
|
+
# CodeLine.new(line: "def foo\n", index: 0)
|
73
|
+
# CodeLine.new(line: " def bar\n", index: 1)
|
74
|
+
# CodeLine.new(line: "end\n", index: 2)
|
75
|
+
# ]
|
76
|
+
#
|
77
|
+
# DeadEnd.valid_without?(
|
78
|
+
# without_lines: code_lines[1],
|
79
|
+
# code_lines: code_lines
|
80
|
+
# ) # => true
|
81
|
+
#
|
82
|
+
# DeadEnd.valid?(code_lines) # => false
|
83
|
+
def self.valid_without?(without_lines: , code_lines:)
|
84
|
+
lines = code_lines - Array(without_lines).flatten
|
85
|
+
|
86
|
+
if lines.empty?
|
87
|
+
return true
|
88
|
+
else
|
89
|
+
return valid?(lines)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.invalid?(source)
|
94
|
+
source = source.join if source.is_a?(Array)
|
95
|
+
source = source.to_s
|
96
|
+
|
97
|
+
Ripper.new(source).tap(&:parse).error?
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns truthy if a given input source is valid syntax
|
101
|
+
#
|
102
|
+
# DeadEnd.valid?(<<~EOM) # => true
|
103
|
+
# def foo
|
104
|
+
# end
|
105
|
+
# EOM
|
106
|
+
#
|
107
|
+
# DeadEnd.valid?(<<~EOM) # => false
|
108
|
+
# def foo
|
109
|
+
# def bar # Syntax error here
|
110
|
+
# end
|
111
|
+
# EOM
|
112
|
+
#
|
113
|
+
# You can also pass in an array of lines and they'll be
|
114
|
+
# joined before evaluating
|
115
|
+
#
|
116
|
+
# DeadEnd.valid?(
|
117
|
+
# [
|
118
|
+
# "def foo\n",
|
119
|
+
# "end\n"
|
120
|
+
# ]
|
121
|
+
# ) # => true
|
122
|
+
#
|
123
|
+
# DeadEnd.valid?(
|
124
|
+
# [
|
125
|
+
# "def foo\n",
|
126
|
+
# " def bar\n", # Syntax error here
|
127
|
+
# "end\n"
|
128
|
+
# ]
|
129
|
+
# ) # => false
|
130
|
+
#
|
131
|
+
# As an FYI the CodeLine class instances respond to `to_s`
|
132
|
+
# so passing a CodeLine in as an object or as an array
|
133
|
+
# will convert it to it's code representation.
|
134
|
+
def self.valid?(source)
|
135
|
+
!invalid?(source)
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
def self.invalid_type(source)
|
140
|
+
WhoDisSyntaxError.new(source).call
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
require_relative "code_line"
|
145
|
+
require_relative "code_block"
|
146
|
+
require_relative "code_frontier"
|
147
|
+
require_relative "display_invalid_blocks"
|
148
|
+
require_relative "around_block_scan"
|
149
|
+
require_relative "block_expand"
|
150
|
+
require_relative "parse_blocks_from_indent_line"
|
151
|
+
|
152
|
+
require_relative "code_search"
|
153
|
+
require_relative "who_dis_syntax_error"
|
154
|
+
require_relative "heredoc_block_parse"
|
155
|
+
require_relative "lex_all"
|
156
|
+
require_relative "trailing_slash_join"
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module DeadEnd
|
2
|
+
# Ripper.lex is not guaranteed to lex the entire source document
|
3
|
+
#
|
4
|
+
# lex = LexAll.new(source: source)
|
5
|
+
# lex.each do |value|
|
6
|
+
# puts value.line
|
7
|
+
# end
|
8
|
+
class LexAll
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
def initialize(source: )
|
12
|
+
@lex = Ripper.lex(source)
|
13
|
+
lineno = @lex.last&.first&.first + 1
|
14
|
+
source_lines = source.lines
|
15
|
+
last_lineno = source_lines.count
|
16
|
+
|
17
|
+
until lineno >= last_lineno
|
18
|
+
lines = source_lines[lineno..-1]
|
19
|
+
|
20
|
+
@lex.concat(Ripper.lex(lines.join, '-', lineno + 1))
|
21
|
+
lineno = @lex.last&.first&.first + 1
|
22
|
+
end
|
23
|
+
|
24
|
+
@lex.map! {|(line, _), type, token| LexValue.new(line, _, type, token) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def each
|
28
|
+
return @lex.each unless block_given?
|
29
|
+
@lex.each do |x|
|
30
|
+
yield x
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def last
|
35
|
+
@lex.last
|
36
|
+
end
|
37
|
+
|
38
|
+
# Value object for accessing lex values
|
39
|
+
#
|
40
|
+
# This lex:
|
41
|
+
#
|
42
|
+
# [1, 0], :on_ident, "describe", CMDARG
|
43
|
+
#
|
44
|
+
# Would translate into:
|
45
|
+
#
|
46
|
+
# lex.line # => 1
|
47
|
+
# lex.type # => :on_indent
|
48
|
+
# lex.token # => "describe"
|
49
|
+
class LexValue
|
50
|
+
attr_reader :line, :type, :token
|
51
|
+
def initialize(line, _, type, token)
|
52
|
+
@line = line
|
53
|
+
@type = type
|
54
|
+
@token = token
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|