offense_to_corrector 0.0.1 → 0.0.2
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 +4 -4
- data/CHANGELOG.md +12 -2
- data/Gemfile.lock +35 -0
- data/Guardfile +14 -0
- data/lib/offense_to_corrector/ast_tools.rb +93 -0
- data/lib/offense_to_corrector/atom_node.rb +36 -0
- data/lib/offense_to_corrector/offense.rb +35 -0
- data/lib/offense_to_corrector/offense_parser.rb +86 -0
- data/lib/offense_to_corrector/offense_template.rb +30 -0
- data/lib/offense_to_corrector/version.rb +1 -1
- data/lib/offense_to_corrector.rb +6 -238
- metadata +21 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 633ca47b27f11bd775d9fd27182d22586603b950b3e3c8ce6e3976162ead1fb1
|
4
|
+
data.tar.gz: 43e72f100670136da7907c3ea63d3298242974b40b2cf1f330cae73f5f366d60
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cb3b1ebad0fb6a238d839cca290065d806ada04fbb9a03858e663befbf1994f5bb5bda222a9cd7bca5a3b0b299733f179fb1f5ce0a201be261101e3a908e0972
|
7
|
+
data.tar.gz: 7608dfcd7d6d04a8c4e53f3fc1f448ba5c89585f67a4c15ab25fe87cb0992ed731fdc3ee9590fb1f01b6145ef48f47f4c16f813d894cac91982cab1276ee6f2c
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
-
## [0.
|
3
|
+
## [0.0.2] - 2022-02-21
|
4
4
|
|
5
|
-
-
|
5
|
+
- Refined underlying algorithms for computing nearest nodes
|
6
|
+
- Range intersection dropped for string overlaps.
|
7
|
+
- Closest node implemented on length of overlap and percentage of node source
|
8
|
+
overlap to prevent matching higher-up parent nodes, but also avoid matching
|
9
|
+
entire small child nodes that aren't as relevant.
|
10
|
+
- Fixed errors with unfindable atom nodes.
|
11
|
+
- Broke apart mono-file into related subdirectories, added basic specs
|
12
|
+
|
13
|
+
## [0.0.1] - 2022-02-20
|
14
|
+
|
15
|
+
- Initial release.
|
data/Gemfile.lock
CHANGED
@@ -8,12 +8,44 @@ GEM
|
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
10
|
ast (2.4.2)
|
11
|
+
coderay (1.1.3)
|
11
12
|
diff-lcs (1.5.0)
|
13
|
+
ffi (1.15.5)
|
14
|
+
formatador (1.1.0)
|
15
|
+
guard (2.18.0)
|
16
|
+
formatador (>= 0.2.4)
|
17
|
+
listen (>= 2.7, < 4.0)
|
18
|
+
lumberjack (>= 1.0.12, < 2.0)
|
19
|
+
nenv (~> 0.1)
|
20
|
+
notiffany (~> 0.0)
|
21
|
+
pry (>= 0.13.0)
|
22
|
+
shellany (~> 0.0)
|
23
|
+
thor (>= 0.18.1)
|
24
|
+
guard-compat (1.2.1)
|
25
|
+
guard-rspec (4.7.3)
|
26
|
+
guard (~> 2.1)
|
27
|
+
guard-compat (~> 1.1)
|
28
|
+
rspec (>= 2.99.0, < 4.0)
|
29
|
+
listen (3.7.1)
|
30
|
+
rb-fsevent (~> 0.10, >= 0.10.3)
|
31
|
+
rb-inotify (~> 0.9, >= 0.9.10)
|
32
|
+
lumberjack (1.2.8)
|
33
|
+
method_source (1.0.0)
|
34
|
+
nenv (0.3.0)
|
35
|
+
notiffany (0.1.3)
|
36
|
+
nenv (~> 0.1)
|
37
|
+
shellany (~> 0.0)
|
12
38
|
parallel (1.21.0)
|
13
39
|
parser (3.1.0.0)
|
14
40
|
ast (~> 2.4.1)
|
41
|
+
pry (0.14.1)
|
42
|
+
coderay (~> 1.1)
|
43
|
+
method_source (~> 1.0)
|
15
44
|
rainbow (3.1.1)
|
16
45
|
rake (13.0.6)
|
46
|
+
rb-fsevent (0.11.1)
|
47
|
+
rb-inotify (0.10.1)
|
48
|
+
ffi (~> 1.0)
|
17
49
|
regexp_parser (2.2.1)
|
18
50
|
rexml (3.2.5)
|
19
51
|
rspec (3.11.0)
|
@@ -41,12 +73,15 @@ GEM
|
|
41
73
|
rubocop-ast (1.15.2)
|
42
74
|
parser (>= 3.0.1.1)
|
43
75
|
ruby-progressbar (1.11.0)
|
76
|
+
shellany (0.0.1)
|
77
|
+
thor (1.2.1)
|
44
78
|
unicode-display_width (2.1.0)
|
45
79
|
|
46
80
|
PLATFORMS
|
47
81
|
x86_64-darwin-20
|
48
82
|
|
49
83
|
DEPENDENCIES
|
84
|
+
guard-rspec
|
50
85
|
offense_to_corrector!
|
51
86
|
rake (~> 13.0)
|
52
87
|
rspec (~> 3.0)
|
data/Guardfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
guard :rspec, cmd: "bundle exec rspec" do
|
2
|
+
require "guard/rspec/dsl"
|
3
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
4
|
+
|
5
|
+
# RSpec files
|
6
|
+
rspec = dsl.rspec
|
7
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
8
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
9
|
+
watch(rspec.spec_files)
|
10
|
+
|
11
|
+
# Ruby files
|
12
|
+
ruby = dsl.ruby
|
13
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
14
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module OffenseToCorrector
|
2
|
+
module AstTools
|
3
|
+
def atom?(value)
|
4
|
+
!value.is_a?(RuboCop::AST::Node)
|
5
|
+
end
|
6
|
+
|
7
|
+
# I may have to work on getting this one later to try and
|
8
|
+
# narrow the band of `AtomNode`
|
9
|
+
# def childless?(node)
|
10
|
+
# false # TODO
|
11
|
+
# end
|
12
|
+
|
13
|
+
def string_intersection(a, b)
|
14
|
+
# Smaller string inside larger string
|
15
|
+
target, search_string = a.size < b.size ? [a, b] : [b, a]
|
16
|
+
|
17
|
+
last_char_index = target.size.downto(1).find do |i|
|
18
|
+
search_string.include?(target[0..i])
|
19
|
+
end or return ""
|
20
|
+
|
21
|
+
target[0..last_char_index]
|
22
|
+
end
|
23
|
+
|
24
|
+
# How much do two ranges overlap? Used to see how well a node
|
25
|
+
# matches with the associated underline
|
26
|
+
def range_overlap_count(a, b)
|
27
|
+
return [a.end, b.end].min - b.begin if a.cover?(b.begin)
|
28
|
+
return [a.end, b.end].min - a.begin if b.cover?(a.begin)
|
29
|
+
|
30
|
+
0
|
31
|
+
end
|
32
|
+
|
33
|
+
# See if a range overlaps another one, bidirectional
|
34
|
+
def range_overlap?(a, b)
|
35
|
+
a.cover?(b.begin) || b.cover?(a.begin)
|
36
|
+
end
|
37
|
+
|
38
|
+
# To get an AST we need the processed source of a string
|
39
|
+
def processed_source_from(string)
|
40
|
+
RuboCop::ProcessedSource.new(string, RUBY_VERSION.to_f)
|
41
|
+
end
|
42
|
+
|
43
|
+
# So why bother with the above then? If we end up into correctors
|
44
|
+
# and tree-rewrites we need that original processed source to be
|
45
|
+
# the basis of the AST, otherwise we get object ID mismatches.
|
46
|
+
def ast_from(value)
|
47
|
+
case value
|
48
|
+
when RuboCop::ProcessedSource
|
49
|
+
value.ast
|
50
|
+
else
|
51
|
+
processed_source_from(value).ast
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Not needed quite yet, but could very potentially be used to verify
|
56
|
+
# how accurate generated cops are.
|
57
|
+
def get_corrector(value)
|
58
|
+
RuboCop::Cop::Corrector.new(value.buffer)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Descendants leaves out a _lot_ of detail potentially. There has to
|
62
|
+
# be a better way to deal with this, but not thinking of one right now.
|
63
|
+
def get_children(source_node)
|
64
|
+
recurse = -> parent do
|
65
|
+
collected_children = []
|
66
|
+
parent.children.each do |child|
|
67
|
+
next if child.nil?
|
68
|
+
|
69
|
+
# If it's a regular parent carry on as you were
|
70
|
+
unless atom?(child) # || childless?(child)
|
71
|
+
collected_children.concat([child, *recurse[child]])
|
72
|
+
next
|
73
|
+
end
|
74
|
+
|
75
|
+
# Otherwise we want to find where that parent is in the source code,
|
76
|
+
# and the range it exists in, to create an `AtomNode`
|
77
|
+
source = child.to_s
|
78
|
+
|
79
|
+
parent_begin = parent.location.expression.to_range.begin
|
80
|
+
range_begin = source_node.source.index(source, parent_begin)
|
81
|
+
range_end = range_begin + source.size
|
82
|
+
range = range_begin..range_end
|
83
|
+
|
84
|
+
collected_children << AtomNode.new(parent:, source:, range:)
|
85
|
+
end
|
86
|
+
|
87
|
+
collected_children
|
88
|
+
end
|
89
|
+
|
90
|
+
[source_node, *recurse[source_node]]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module OffenseToCorrector
|
2
|
+
# The annoying thing is that `RuboCop::AST::Node` 's children / descendant
|
3
|
+
# methods don't capture all the relevant data, so we have to cheat a bit
|
4
|
+
# by wrapping atoms (String, Symbol, Int, etc) in a class to get around that.
|
5
|
+
class AtomNode
|
6
|
+
attr_reader :source, :range, :parent
|
7
|
+
|
8
|
+
def initialize(source:, range:, parent:)
|
9
|
+
@source = source
|
10
|
+
@range = range
|
11
|
+
@parent = parent
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
relevant_children = @parent.children.map do |child|
|
16
|
+
next "..." unless child.to_s == self.source.to_s # Wildcard
|
17
|
+
|
18
|
+
case child
|
19
|
+
when String then %("#{child}") # Literal string
|
20
|
+
when Symbol then ":#{child}" # Literal symbol
|
21
|
+
when nil then "nil?"
|
22
|
+
else child
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# The trick here is that these aren't nodes, but we do care about
|
27
|
+
# what the "parent" is that contains it to get something we can
|
28
|
+
# work with. All other children are replaced with wildcards.
|
29
|
+
"(#{self.parent.type} #{relevant_children.join(' ')})"
|
30
|
+
end
|
31
|
+
|
32
|
+
def deconstruct_keys(keys)
|
33
|
+
{ source:, range:, parent: }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module OffenseToCorrector
|
2
|
+
class Offense
|
3
|
+
# How many underline carots (^), and potentially an error after
|
4
|
+
OFFENSE_MATCH = /^ *?(?<underline>\^+) *?(?<error>.*)?$/
|
5
|
+
|
6
|
+
attr_reader :error, :range, :source
|
7
|
+
|
8
|
+
def initialize(error:, range:, source:)
|
9
|
+
@error = error
|
10
|
+
@range = range
|
11
|
+
@source = source
|
12
|
+
end
|
13
|
+
|
14
|
+
def deconstruct_keys(keys)
|
15
|
+
{ error:, range:, source: }
|
16
|
+
end
|
17
|
+
|
18
|
+
# See if there's an underline, if so get how long it is and
|
19
|
+
# the error message after it
|
20
|
+
#
|
21
|
+
def self.parse(line:, previous_line:)
|
22
|
+
match_data = OFFENSE_MATCH.match(line) or return nil
|
23
|
+
underline = match_data[:underline]
|
24
|
+
error = match_data[:error].lstrip
|
25
|
+
|
26
|
+
underline_start = line.index(underline)
|
27
|
+
underline_end = underline_start + underline.size
|
28
|
+
range = underline_start...underline_end
|
29
|
+
|
30
|
+
source = previous_line.slice(range).chomp
|
31
|
+
|
32
|
+
new(error:, range:, source:)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module OffenseToCorrector
|
2
|
+
class OffenseParser
|
3
|
+
include AstTools
|
4
|
+
|
5
|
+
attr_reader :ast, :source, :offense, :ast_nodes
|
6
|
+
|
7
|
+
def initialize(string)
|
8
|
+
@ast_lines, @offense = parse_string(string)
|
9
|
+
@source = processed_source_from(@ast_lines.join("\n"))
|
10
|
+
@ast = ast_from(@source)
|
11
|
+
@ast_nodes = get_children(@ast)
|
12
|
+
@template = OffenseTemplate.new
|
13
|
+
end
|
14
|
+
|
15
|
+
# Render that into the cop skeleton. Perhaps having too much
|
16
|
+
# fun here with rightward assignment.
|
17
|
+
def render
|
18
|
+
call => {
|
19
|
+
offense: { error: },
|
20
|
+
node_offense_info: { offending_node:, offending_node_matcher: }
|
21
|
+
}
|
22
|
+
|
23
|
+
@template.render(
|
24
|
+
class_name: "TODO",
|
25
|
+
match_pattern: offending_node_matcher,
|
26
|
+
error_message: error,
|
27
|
+
node_type: offending_node.type,
|
28
|
+
cop_type: "Lint",
|
29
|
+
node_location: "selector",
|
30
|
+
offense_severity: "warning"
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Quick info on what all is being worked on and what info we got
|
35
|
+
def deconstruct_keys(keys)
|
36
|
+
{ ast:, source:, offense:, node_offense_info: }
|
37
|
+
end
|
38
|
+
|
39
|
+
def node_offense_info
|
40
|
+
return @node_offense_info if defined?(@node_offense_info)
|
41
|
+
|
42
|
+
# Find the node, or atom, with the most overlap with the offense
|
43
|
+
# range defined by that underline. That's defined as a pair of
|
44
|
+
# the length of the intersection as well as what percentage that
|
45
|
+
# intersection makes of the full node source.
|
46
|
+
offending_node = @ast_nodes.max_by do |node|
|
47
|
+
intersection = string_intersection(node.source, @offense.source)
|
48
|
+
[intersection.size, intersection.size.fdiv(node.source.size)]
|
49
|
+
end
|
50
|
+
|
51
|
+
@node_offense_info ||= {
|
52
|
+
offending_node:,
|
53
|
+
offending_node_matcher: offending_node.to_s
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
private def node_range(node)
|
58
|
+
if node.is_a?(AtomNode)
|
59
|
+
node.range
|
60
|
+
else
|
61
|
+
node.location.expression.to_range
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Figure which part of the passed in code string is AST vs offense
|
66
|
+
private def parse_string(string)
|
67
|
+
ast_lines = []
|
68
|
+
offense = nil
|
69
|
+
lines = string.lines
|
70
|
+
|
71
|
+
lines.each_with_index do |line, i|
|
72
|
+
# No sense of a meta-line being above every other line
|
73
|
+
meta_info = i > 0 && Offense.parse(line:, previous_line: lines[i - 1])
|
74
|
+
|
75
|
+
if meta_info
|
76
|
+
raise "Cannot have multiple offenses" unless offense.nil?
|
77
|
+
offense = meta_info
|
78
|
+
else
|
79
|
+
ast_lines << line
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
[ast_lines, offense]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module OffenseToCorrector
|
2
|
+
# ERB template for rendering a cop skeleton, may make this more useful
|
3
|
+
# later, but mostly quick templating for now.
|
4
|
+
class OffenseTemplate
|
5
|
+
def initialize(name: "autocorrector_template.erb")
|
6
|
+
@template = File.read(OffenseToCorrector.load_template(name))
|
7
|
+
@erb = ERB.new(@template)
|
8
|
+
end
|
9
|
+
|
10
|
+
def render(
|
11
|
+
class_name: "TODO",
|
12
|
+
match_pattern:,
|
13
|
+
error_message: "",
|
14
|
+
node_type:,
|
15
|
+
cop_type: "Lint",
|
16
|
+
node_location: "selector",
|
17
|
+
offense_severity: "warning"
|
18
|
+
)
|
19
|
+
@erb.result_with_hash(
|
20
|
+
class_name:,
|
21
|
+
match_pattern:,
|
22
|
+
error_message:,
|
23
|
+
node_type:,
|
24
|
+
cop_type:,
|
25
|
+
node_location:,
|
26
|
+
offense_severity:
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/offense_to_corrector.rb
CHANGED
@@ -5,6 +5,12 @@ require_relative "offense_to_corrector/version"
|
|
5
5
|
require "rubocop"
|
6
6
|
require "erb"
|
7
7
|
|
8
|
+
require "offense_to_corrector/atom_node"
|
9
|
+
require "offense_to_corrector/ast_tools"
|
10
|
+
require "offense_to_corrector/offense"
|
11
|
+
require "offense_to_corrector/offense_parser"
|
12
|
+
require "offense_to_corrector/offense_template"
|
13
|
+
|
8
14
|
module OffenseToCorrector
|
9
15
|
module_function def load_template(name)
|
10
16
|
File.join(File.dirname(__FILE__), "offense_to_corrector/templates", name)
|
@@ -17,242 +23,4 @@ module OffenseToCorrector
|
|
17
23
|
module_function def offense_to_cop(code)
|
18
24
|
OffenseParser.new(code).render
|
19
25
|
end
|
20
|
-
|
21
|
-
# The annoying thing is that `RuboCop::AST::Node` 's children / descendant
|
22
|
-
# methods don't capture all the relevant data, so we have to cheat a bit
|
23
|
-
# by wrapping atoms (String, Symbol, Int, etc) in a class to get around that.
|
24
|
-
AtomNode = Struct.new(:source, :range, :parent, keyword_init: true) do
|
25
|
-
def type
|
26
|
-
self.parent.type
|
27
|
-
end
|
28
|
-
|
29
|
-
def to_s
|
30
|
-
relevant_children = self.parent.children.map do |c|
|
31
|
-
next "..." unless c.to_s == self.source.to_s # Wildcard
|
32
|
-
|
33
|
-
case c
|
34
|
-
when String then %("#{c}") # Literal string
|
35
|
-
when Symbol then ":#{c}" # Literal symbol
|
36
|
-
else c
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
# The trick here is that these aren't nodes, but we do care about
|
41
|
-
# what the "parent" is that contains it to get something we can
|
42
|
-
# work with. All other children are replaced with wildcards.
|
43
|
-
"(#{self.parent.type} #{relevant_children.join(' ')})"
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
module AstTools
|
48
|
-
def atom?(value)
|
49
|
-
!value.is_a?(RuboCop::AST::Node)
|
50
|
-
end
|
51
|
-
|
52
|
-
# I may have to work on getting this one later to try and
|
53
|
-
# narrow the band of `AtomNode`
|
54
|
-
# def childless?(node)
|
55
|
-
# false # TODO
|
56
|
-
# end
|
57
|
-
|
58
|
-
# How much do two ranges overlap? Used to see how well a node
|
59
|
-
# matches with the associated underline
|
60
|
-
def range_overlap_count(a, b)
|
61
|
-
return (b.begin...[a.end, b.end].min).size if a.cover?(b.begin)
|
62
|
-
return (a.begin...[a.end, b.end].min).size if b.cover?(a.begin)
|
63
|
-
|
64
|
-
0
|
65
|
-
end
|
66
|
-
|
67
|
-
# See if a range overlaps another one, bidirectional
|
68
|
-
def range_overlap?(a, b)
|
69
|
-
a.cover?(b.begin) || b.cover?(a.begin)
|
70
|
-
end
|
71
|
-
|
72
|
-
# To get an AST we need the processed source of a string
|
73
|
-
def processed_source_from(string)
|
74
|
-
RuboCop::ProcessedSource.new(string, RUBY_VERSION.to_f)
|
75
|
-
end
|
76
|
-
|
77
|
-
# So why bother with the above then? If we end up into correctors
|
78
|
-
# and tree-rewrites we need that original processed source to be
|
79
|
-
# the basis of the AST, otherwise we get object ID mismatches.
|
80
|
-
def ast_from(value)
|
81
|
-
case value
|
82
|
-
when RuboCop::ProcessedSource
|
83
|
-
value.ast
|
84
|
-
else
|
85
|
-
processed_source_from(value).ast
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
# Not needed quite yet, but could very potentially be used to verify
|
90
|
-
# how accurate generated cops are.
|
91
|
-
def get_corrector(value)
|
92
|
-
RuboCop::Cop::Corrector.new(value.buffer)
|
93
|
-
end
|
94
|
-
|
95
|
-
# Descendants leaves out a _lot_ of detail potentially. There has to
|
96
|
-
# be a better way to deal with this, but not thinking of one right now.
|
97
|
-
def get_children(source_node)
|
98
|
-
recurse = -> node do
|
99
|
-
collected_children = []
|
100
|
-
node.children.each do |child|
|
101
|
-
next if child.nil?
|
102
|
-
|
103
|
-
# If it's a regular node carry on as you were
|
104
|
-
unless atom?(child) # || childless?(child)
|
105
|
-
collected_children.concat([child, *recurse[child]])
|
106
|
-
next
|
107
|
-
end
|
108
|
-
|
109
|
-
# Otherwise we want to find where that node is in the source code,
|
110
|
-
# and the range it exists in, to create an `AtomNode`
|
111
|
-
child_string = child.to_s
|
112
|
-
range_begin = source_node.source.index(/\b#{child_string}\b/)
|
113
|
-
range_end = range_begin + child_string.size
|
114
|
-
|
115
|
-
collected_children << AtomNode.new(
|
116
|
-
parent: node,
|
117
|
-
source: child_string,
|
118
|
-
range: range_begin..range_end
|
119
|
-
)
|
120
|
-
end
|
121
|
-
|
122
|
-
collected_children
|
123
|
-
end
|
124
|
-
|
125
|
-
[source_node, *recurse[source_node]]
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
# ERB template for rendering a cop skeleton, may make this more useful
|
130
|
-
# later, but mostly quick templating for now.
|
131
|
-
class OffenseTemplate
|
132
|
-
def initialize(name: "autocorrector_template.erb")
|
133
|
-
@template = File.read(OffenseToCorrector.load_template(name))
|
134
|
-
@erb = ERB.new(@template)
|
135
|
-
end
|
136
|
-
|
137
|
-
def render(
|
138
|
-
class_name: "TODO",
|
139
|
-
match_pattern:,
|
140
|
-
error_message: "",
|
141
|
-
node_type:,
|
142
|
-
cop_type: "Lint",
|
143
|
-
node_location: "selector",
|
144
|
-
offense_severity: "warning"
|
145
|
-
)
|
146
|
-
@erb.result_with_hash(
|
147
|
-
class_name:,
|
148
|
-
match_pattern:,
|
149
|
-
error_message:,
|
150
|
-
node_type:,
|
151
|
-
cop_type:,
|
152
|
-
node_location:,
|
153
|
-
offense_severity:
|
154
|
-
)
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
# Bit more of a structured container for an offense (the underline)
|
159
|
-
Offense = Struct.new(:line, :error, :range, keyword_init: true)
|
160
|
-
|
161
|
-
class OffenseParser
|
162
|
-
include AstTools
|
163
|
-
|
164
|
-
# How many underline carots (^), and potentially an error after
|
165
|
-
OFFENSE_MATCH = /^ *?(?<underline>\^+) *?(?<error>.*)?$/
|
166
|
-
|
167
|
-
attr_reader :ast, :source, :offense
|
168
|
-
|
169
|
-
def initialize(string)
|
170
|
-
@ast_lines, @offense = parse_string(string)
|
171
|
-
@source = processed_source_from(@ast_lines.join("\n"))
|
172
|
-
@ast = ast_from(@source)
|
173
|
-
@ast_nodes = get_children(@ast)
|
174
|
-
@template = OffenseTemplate.new
|
175
|
-
end
|
176
|
-
|
177
|
-
# Render that into the cop skeleton. Perhaps having too much
|
178
|
-
# fun here with rightward assignment.
|
179
|
-
def render
|
180
|
-
call => {
|
181
|
-
offense: { error: },
|
182
|
-
node_offense_info: { offending_node:, offending_node_matcher: }
|
183
|
-
}
|
184
|
-
|
185
|
-
@template.render(
|
186
|
-
class_name: "TODO",
|
187
|
-
match_pattern: offending_node_matcher,
|
188
|
-
error_message: error,
|
189
|
-
node_type: offending_node.type,
|
190
|
-
cop_type: "Lint",
|
191
|
-
node_location: "selector",
|
192
|
-
offense_severity: "warning"
|
193
|
-
)
|
194
|
-
end
|
195
|
-
|
196
|
-
# Quick info on what all is being worked on and what info we got
|
197
|
-
def call
|
198
|
-
{ ast:, offense:, node_offense_info: }
|
199
|
-
end
|
200
|
-
|
201
|
-
def node_offense_info
|
202
|
-
return @node_offense_info if defined?(@node_offense_info)
|
203
|
-
|
204
|
-
# Find the node, or atom, with the most overlap with the offense
|
205
|
-
# range defined by that underline.
|
206
|
-
offending_node = @ast_nodes.max do |node|
|
207
|
-
node_range = if node.is_a?(AtomNode)
|
208
|
-
node.range
|
209
|
-
else
|
210
|
-
node.location.expression.to_range
|
211
|
-
end
|
212
|
-
|
213
|
-
# Except we're doing it as a percentage, otherwise parent nodes
|
214
|
-
# will dominate that count potentially. The closer to 100% overlap
|
215
|
-
# the better.
|
216
|
-
overlap_count = range_overlap_count(node_range, @offense[:range])
|
217
|
-
overlap_count.fdiv(node.source.size)
|
218
|
-
end
|
219
|
-
|
220
|
-
@node_offense_info ||= {
|
221
|
-
offending_node:,
|
222
|
-
offending_node_matcher: offending_node.to_s
|
223
|
-
}
|
224
|
-
end
|
225
|
-
|
226
|
-
# See if there's an underline, if so get how long it is and
|
227
|
-
# the error message after it
|
228
|
-
private def offending_meta_from(line)
|
229
|
-
match_data = OFFENSE_MATCH.match(line) or return nil
|
230
|
-
underline = match_data[:underline]
|
231
|
-
error = match_data[:error].lstrip
|
232
|
-
|
233
|
-
underline_start = line.index(underline)
|
234
|
-
underline_end = underline_start + underline.size
|
235
|
-
|
236
|
-
{ match_data:, error:, range: underline_start..underline_end }
|
237
|
-
end
|
238
|
-
|
239
|
-
# Figure which part of the passed in code string is AST vs offense
|
240
|
-
private def parse_string(string)
|
241
|
-
ast_lines = []
|
242
|
-
offense = nil
|
243
|
-
|
244
|
-
string.lines.each_with_index do |line, i|
|
245
|
-
meta_info = offending_meta_from(line)
|
246
|
-
|
247
|
-
if meta_info
|
248
|
-
raise "Cannot have multiple offenses" unless offense.nil?
|
249
|
-
offense = Offense.new(line: i, **meta_info.slice(:error, :range))
|
250
|
-
else
|
251
|
-
ast_lines << line
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
[ast_lines, offense]
|
256
|
-
end
|
257
|
-
end
|
258
26
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: offense_to_corrector
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brandon Weaver
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: guard-rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
27
41
|
description:
|
28
42
|
email:
|
29
43
|
- keystonelemur@gmail.com
|
@@ -36,10 +50,16 @@ files:
|
|
36
50
|
- CODE_OF_CONDUCT.md
|
37
51
|
- Gemfile
|
38
52
|
- Gemfile.lock
|
53
|
+
- Guardfile
|
39
54
|
- LICENSE.txt
|
40
55
|
- README.md
|
41
56
|
- Rakefile
|
42
57
|
- lib/offense_to_corrector.rb
|
58
|
+
- lib/offense_to_corrector/ast_tools.rb
|
59
|
+
- lib/offense_to_corrector/atom_node.rb
|
60
|
+
- lib/offense_to_corrector/offense.rb
|
61
|
+
- lib/offense_to_corrector/offense_parser.rb
|
62
|
+
- lib/offense_to_corrector/offense_template.rb
|
43
63
|
- lib/offense_to_corrector/templates/autocorrector_template.erb
|
44
64
|
- lib/offense_to_corrector/version.rb
|
45
65
|
- sig/offense_to_corrector.rbs
|