rail_inspector 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8e98b33289ed86129f0bf1680f8b164a9431a7e91aca0a3ee22c3e8702272a6b
4
+ data.tar.gz: f5cbdb492740f6c4c6e0c0b3bf394476fcbb2fe8e3cd6b94b2fddc29cf25437a
5
+ SHA512:
6
+ metadata.gz: 5a1df1044157d495d1df59f772012cdef5f3dcc097cdf4a0afc3ebe287b1faa2ea01a689ad28bbb6f9bbaf401881b8b37f115326c5010c8cc3e8aa5398ad0a94
7
+ data.tar.gz: 622294ea305ca60aa40acf4b72188cc9ac7c1934760831019d745fc847264d78a18d703c8b2f1f8657ff5d162232db4bb6110a11565d5a7ac11f9d5b53b21036
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in rail_inspector.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "minitest", "~> 5.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,27 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rail_inspector (0.0.1)
5
+ syntax_tree (= 6.1.1)
6
+ thor (~> 1.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ minitest (5.18.0)
12
+ prettier_print (1.2.1)
13
+ rake (13.0.6)
14
+ syntax_tree (6.1.1)
15
+ prettier_print (>= 1.2.0)
16
+ thor (1.2.1)
17
+
18
+ PLATFORMS
19
+ x86_64-linux
20
+
21
+ DEPENDENCIES
22
+ minitest (~> 5.0)
23
+ rail_inspector!
24
+ rake (~> 13.0)
25
+
26
+ BUNDLED WITH
27
+ 2.3.19
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Hartley McGuire
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # RailInspector
2
+
3
+ A collection of linters for [`rails/rails`](https://github.com/rails/rails)
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ $ bundle add rail_inspector
10
+
11
+ If bundler is not being used to manage dependencies, install the gem by
12
+ executing:
13
+
14
+ $ gem install rail_inspector
15
+
16
+ ## Usage
17
+
18
+ TODO: Write usage instructions here
19
+
20
+ ## Development
21
+
22
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
23
+ `rake test` to run the tests. You can also run `bin/console` for an interactive
24
+ prompt that will allow you to experiment.
25
+
26
+ To install this gem onto your local machine, run `bundle exec rake install`. To
27
+ release a new version, update the version number in `version.rb`, and then run
28
+ `bundle exec rake release`, which will create a git tag for the version, push
29
+ git commits and the created tag, and push the `.gem` file to
30
+ [rubygems.org](https://rubygems.org).
31
+
32
+ ## Contributing
33
+
34
+ Bug reports and pull requests are welcome on GitHub at
35
+ https://github.com/skipkayhil/rail_inspector.
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT
40
+ License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+ require "syntax_tree/rake_tasks"
6
+
7
+ Minitest::TestTask.create
8
+
9
+ SyntaxTree::Rake::CheckTask.new
10
+ SyntaxTree::Rake::WriteTask.new
11
+
12
+ task default: [:test, "stree:check"]
data/exe/railspect ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/rail_inspector/cli"
4
+
5
+ RailInspector::Cli.start
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "strscan"
5
+
6
+ class Changelog
7
+ class Offense
8
+ attr_reader :line, :line_number, :range, :message
9
+
10
+ def initialize(line, line_number, range, message)
11
+ @line = line
12
+ @line_number = line_number
13
+ @range = range
14
+ @message = message
15
+ end
16
+ end
17
+
18
+ class Entry
19
+ attr_reader :lines, :offenses
20
+
21
+ def initialize(lines, starting_line)
22
+ @lines = lines
23
+ @starting_line = starting_line
24
+
25
+ @offenses = []
26
+
27
+ validate_authors
28
+ validate_leading_whitespace
29
+ validate_trailing_whitespace
30
+ end
31
+
32
+ private
33
+
34
+ def header
35
+ lines.first
36
+ end
37
+
38
+ def validate_authors
39
+ authors =
40
+ lines.reverse.find { |line| line.match?(/\*[^\d\s]+(\s[^\d\s]+)*\*/) }
41
+
42
+ return if authors
43
+
44
+ add_offense(
45
+ header,
46
+ line_in_file(0),
47
+ 1..header.length,
48
+ "CHANGELOG entry is missing authors."
49
+ )
50
+ end
51
+
52
+ def validate_leading_whitespace
53
+ unless header.match?(/\* {3}\S/)
54
+ add_offense(
55
+ header,
56
+ line_in_file(0),
57
+ 1..4,
58
+ "CHANGELOG header must start with '*' and 3 spaces"
59
+ )
60
+ end
61
+
62
+ lines.each_with_index do |line, i|
63
+ next if i == 0
64
+ next if line.strip.empty?
65
+ next if line.start_with?(" " * 4)
66
+
67
+ add_offense(
68
+ line,
69
+ line_in_file(i),
70
+ 1..4,
71
+ "CHANGELOG line must be indented 4 spaces"
72
+ )
73
+ end
74
+ end
75
+
76
+ def validate_trailing_whitespace
77
+ lines.each_with_index do |line, i|
78
+ next unless line.end_with?(" ", "\t")
79
+
80
+ add_offense(
81
+ line,
82
+ line_in_file(i),
83
+ (line.rstrip.length + 1)..line.length,
84
+ "Trailing whitespace detected."
85
+ )
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def add_offense(...)
92
+ @offenses << Offense.new(...)
93
+ end
94
+
95
+ def line_in_file(line_in_entry)
96
+ @starting_line + line_in_entry
97
+ end
98
+ end
99
+
100
+ class Parser
101
+ def self.call(file)
102
+ new(file).parse
103
+ end
104
+
105
+ def self.to_proc
106
+ method(:call).to_proc
107
+ end
108
+
109
+ def initialize(file)
110
+ @buffer = StringScanner.new(file)
111
+ @lines = []
112
+ @current_line = 1
113
+
114
+ @entries = []
115
+ end
116
+
117
+ def parse
118
+ until @buffer.eos?
119
+ if peek_footer?
120
+ pop_entry
121
+ next parse_footer
122
+ end
123
+
124
+ pop_entry if peek_probably_header?
125
+
126
+ parse_line
127
+ end
128
+
129
+ @entries
130
+ end
131
+
132
+ private
133
+
134
+ def parse_line
135
+ @current_line += 1
136
+ @lines << @buffer.scan_until(/\n/)[0...-1]
137
+ end
138
+
139
+ FOOTER_TEXT = "Please check"
140
+
141
+ def parse_footer
142
+ @buffer.scan(
143
+ /#{FOOTER_TEXT} \[\d-\d-stable\]\(.*\) for previous changes\.\n/
144
+ )
145
+ end
146
+
147
+ def peek_probably_header?
148
+ return false unless @buffer.peek(1) == "*"
149
+
150
+ maybe_header = @buffer.check_until(/\n/).strip
151
+
152
+ # If there are an odd number of *, then the line is almost certainly a
153
+ # header since bolding requires pairs.
154
+ return true unless maybe_header.count("*").even?
155
+
156
+ !maybe_header.end_with?("*")
157
+ end
158
+
159
+ def peek_footer?
160
+ @buffer.peek(FOOTER_TEXT.length) == FOOTER_TEXT
161
+ end
162
+
163
+ def pop_entry
164
+ # Ensure we don't pop an entry if we only see newlines and the footer
165
+ return unless @lines.any? { |line| line.match?(/\S/) }
166
+
167
+ @entries << Changelog::Entry.new(@lines, @current_line - @lines.length)
168
+ @lines = []
169
+ end
170
+ end
171
+
172
+ class Formatter
173
+ def initialize
174
+ @changelog_count = 0
175
+ @offense_count = 0
176
+ end
177
+
178
+ def to_proc
179
+ method(:call).to_proc
180
+ end
181
+
182
+ def call(changelog)
183
+ @changelog_count += 1
184
+
185
+ changelog.offenses.each { |o| process_offense(changelog, o) }
186
+ end
187
+
188
+ def finish
189
+ puts "#{@changelog_count} changelogs inspected, #{@offense_count} offense#{"s" unless @offense_count == 1} detected"
190
+ end
191
+
192
+ private
193
+
194
+ def process_offense(file, offense)
195
+ @offense_count += 1
196
+
197
+ puts "#{file.path}:#{offense.line_number} #{offense.message}"
198
+ puts offense.line
199
+ puts ("^" * offense.range.count).rjust(offense.range.end)
200
+ end
201
+ end
202
+
203
+ class Runner
204
+ attr_reader :formatter, :rails_path
205
+
206
+ def initialize(rails_path)
207
+ @formatter = Formatter.new
208
+ @rails_path = Pathname.new(rails_path)
209
+ end
210
+
211
+ def call
212
+ invalid_changelogs =
213
+ changelogs.reject do |changelog|
214
+ output = changelog.valid? ? "." : "E"
215
+ $stdout.write(output)
216
+
217
+ changelog.valid?
218
+ end
219
+
220
+ puts "\n\n"
221
+ puts "Offenses:\n\n" unless invalid_changelogs.empty?
222
+
223
+ changelogs.each(&formatter)
224
+ formatter.finish
225
+
226
+ invalid_changelogs.empty?
227
+ end
228
+
229
+ private
230
+
231
+ def changelogs
232
+ changelog_paths.map { |path| Changelog.new(path, File.read(path)) }
233
+ end
234
+
235
+ def changelog_paths
236
+ Dir[rails_path.join("*/CHANGELOG.md")]
237
+ end
238
+ end
239
+
240
+ attr_reader :path, :content, :entries
241
+
242
+ def initialize(path, content)
243
+ @path = path
244
+ @content = content
245
+ @entries = parser.parse
246
+ end
247
+
248
+ def valid?
249
+ offenses.empty?
250
+ end
251
+
252
+ def offenses
253
+ @offenses ||= entries.flat_map(&:offenses)
254
+ end
255
+
256
+ private
257
+
258
+ def parser
259
+ @parser ||= Parser.new(content)
260
+ end
261
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module RailInspector
6
+ class Cli < Thor
7
+ class << self
8
+ def exit_on_failure? = true
9
+ end
10
+
11
+ desc "changelogs RAILS_PATH", "Check CHANGELOG files for common issues"
12
+ def changelogs(rails_path)
13
+ require_relative "./changelog"
14
+
15
+ exit Changelog::Runner.new(rails_path).call
16
+ end
17
+
18
+ desc "configuration RAILS_PATH", "Check various Configuration issues"
19
+ option :autocorrect, type: :boolean, aliases: :a
20
+ def configuration(rails_path)
21
+ require_relative "./configuring"
22
+
23
+ checker = Configuring.new(rails_path)
24
+ checker.check
25
+
26
+ puts checker.errors unless checker.errors.empty?
27
+ exit checker.errors.empty? unless options[:autocorrect]
28
+
29
+ checker.write!
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+ require_relative "../../visitor/framework_default"
5
+
6
+ class Configuring
7
+ module Check
8
+ class FrameworkDefaults
9
+ class NewFrameworkDefaultsFile
10
+ attr_reader :checker, :visitor
11
+
12
+ def initialize(checker, visitor)
13
+ @checker = checker
14
+ @visitor = visitor
15
+ end
16
+
17
+ def check
18
+ visitor.config_map[checker.rails_version].each_key do |config|
19
+ app_config = config.gsub(/^self/, "config")
20
+
21
+ next if defaults_file_content.include? app_config
22
+
23
+ add_error(config)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def add_error(config)
30
+ checker.errors << <<~MESSAGE
31
+ #{new_framework_defaults_path}
32
+ Missing: #{config}
33
+
34
+ MESSAGE
35
+ end
36
+
37
+ def defaults_file_content
38
+ @defaults_file_content ||= checker.read(new_framework_defaults_path)
39
+ end
40
+
41
+ def new_framework_defaults_path
42
+ NEW_FRAMEWORK_DEFAULTS_PATH %
43
+ { version: checker.rails_version.gsub(".", "_") }
44
+ end
45
+ end
46
+
47
+ attr_reader :checker
48
+
49
+ def initialize(checker)
50
+ @checker = checker
51
+ end
52
+
53
+ def check
54
+ header, *defaults_by_version = documented_defaults
55
+
56
+ NewFrameworkDefaultsFile.new(checker, visitor).check
57
+
58
+ checker.doc.versioned_defaults =
59
+ header +
60
+ defaults_by_version
61
+ .map { |defaults| check_defaults(defaults) }
62
+ .flatten
63
+ end
64
+
65
+ private
66
+
67
+ def app_config_tree
68
+ checker.parse(APPLICATION_CONFIGURATION_PATH)
69
+ end
70
+
71
+ def check_defaults(defaults)
72
+ header, configs = defaults[0], defaults[2, defaults.length - 3]
73
+
74
+ version = header.match(/\d\.\d/)[0]
75
+
76
+ generated_doc =
77
+ visitor.config_map[version]
78
+ .map do |config, value|
79
+ full_config =
80
+ case config
81
+ when /^[A-Z]/
82
+ config
83
+ when /^self/
84
+ config.sub("self", "config")
85
+ else
86
+ "config.#{config}"
87
+ end
88
+
89
+ "- [`#{full_config}`](##{full_config.tr("._", "-").downcase}): `#{value}`"
90
+ end
91
+ .sort
92
+
93
+ config_diff =
94
+ Tempfile.create("expected") do |doc|
95
+ doc << generated_doc.join("\n")
96
+ doc.flush
97
+
98
+ Tempfile.create("actual") do |code|
99
+ code << configs.join("\n")
100
+ code.flush
101
+
102
+ `git diff --color --no-index #{doc.path} #{code.path}`
103
+ end
104
+ end
105
+
106
+ checker.errors << <<~MESSAGE unless config_diff.empty?
107
+ #{APPLICATION_CONFIGURATION_PATH}: Incorrect load_defaults docs
108
+ --- Expected
109
+ +++ Actual
110
+ #{config_diff.split("\n")[5..].join("\n")}
111
+ MESSAGE
112
+
113
+ [header, "", *generated_doc, ""]
114
+ end
115
+
116
+ def documented_defaults
117
+ checker
118
+ .doc
119
+ .versioned_defaults
120
+ .slice_before { |line| line.start_with?("####") }
121
+ .to_a
122
+ end
123
+
124
+ def visitor
125
+ @visitor ||=
126
+ begin
127
+ visitor = Visitor::FrameworkDefault.new
128
+ visitor.visit(app_config_tree)
129
+ visitor
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../visitor/attribute"
4
+
5
+ class Configuring
6
+ module Check
7
+ class GeneralConfiguration
8
+ attr_reader :checker
9
+
10
+ def initialize(checker)
11
+ @checker = checker
12
+ end
13
+
14
+ def check
15
+ header, *config_sections = documented_general_config
16
+
17
+ non_nested_accessors =
18
+ general_accessors.reject do |a|
19
+ config_sections.any? { |section| /\.#{a}\./.match?(section[0]) }
20
+ end
21
+
22
+ non_nested_accessors.each do |accessor|
23
+ config_header = "#### `config.#{accessor}`"
24
+
25
+ unless config_sections.any? { |section| section[0] == config_header }
26
+ checker.errors << config_header
27
+ config_sections << [config_header, "", "FIXME", ""]
28
+ end
29
+ end
30
+
31
+ checker.doc.general_config =
32
+ [header] +
33
+ config_sections.sort_by { |section| section[0].split("`")[1] }
34
+ end
35
+
36
+ private
37
+
38
+ APP_CONFIG_CONST = "Rails::Application::Configuration"
39
+
40
+ def app_config_tree
41
+ checker.parse(APPLICATION_CONFIGURATION_PATH)
42
+ end
43
+
44
+ def documented_general_config
45
+ checker
46
+ .doc
47
+ .general_config
48
+ .slice_before { |line| line.start_with?("####") }
49
+ .to_a
50
+ end
51
+
52
+ def general_accessors
53
+ visitor.attribute_map[APP_CONFIG_CONST]["attr_accessor"]
54
+ end
55
+
56
+ def visitor
57
+ @visitor ||=
58
+ begin
59
+ visitor = Visitor::Attribute.new
60
+ visitor.visit(app_config_tree)
61
+ visitor
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./configuring/check/general_configuration"
4
+ require_relative "./configuring/check/framework_defaults"
5
+
6
+ class Configuring
7
+ class CachedParser
8
+ def initialize
9
+ @cache = {}
10
+ end
11
+
12
+ def call(path)
13
+ @cache[path] ||= SyntaxTree.parse(SyntaxTree.read(path))
14
+ end
15
+ end
16
+
17
+ DOC_PATH = "guides/source/configuring.md"
18
+ APPLICATION_CONFIGURATION_PATH =
19
+ "railties/lib/rails/application/configuration.rb"
20
+ NEW_FRAMEWORK_DEFAULTS_PATH =
21
+ "railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_%{version}.rb.tt"
22
+
23
+ class Doc
24
+ attr_accessor :general_config, :versioned_defaults
25
+
26
+ def initialize(content)
27
+ @before, @versioned_defaults, @general_config, @after =
28
+ content
29
+ .split("\n")
30
+ .slice_before do |line|
31
+ [
32
+ "### Versioned Default Values",
33
+ "### Rails General Configuration",
34
+ "### Configuring Assets"
35
+ ].include?(line)
36
+ end
37
+ .to_a
38
+ end
39
+
40
+ def to_s
41
+ (@before + @versioned_defaults + @general_config + @after).join("\n") +
42
+ "\n"
43
+ end
44
+ end
45
+
46
+ attr_reader :errors, :parser
47
+
48
+ def initialize(rails_path)
49
+ @errors = []
50
+ @parser = CachedParser.new
51
+ @rails_path = Pathname.new(rails_path)
52
+ end
53
+
54
+ def check
55
+ [Check::GeneralConfiguration, Check::FrameworkDefaults].each do |check|
56
+ check.new(self).check
57
+ end
58
+ end
59
+
60
+ def doc
61
+ @doc ||=
62
+ begin
63
+ content = File.read(doc_path)
64
+ Configuring::Doc.new(content)
65
+ end
66
+ end
67
+
68
+ def parse(relative_path)
69
+ parser.call(@rails_path.join(relative_path))
70
+ end
71
+
72
+ def read(relative_path)
73
+ File.read(@rails_path.join(relative_path))
74
+ end
75
+
76
+ def rails_version
77
+ @rails_version ||= File.read(@rails_path.join("RAILS_VERSION")).to_f.to_s
78
+ end
79
+
80
+ def write!
81
+ File.write(doc_path, doc.to_s)
82
+ end
83
+
84
+ private
85
+
86
+ def doc_path
87
+ @rails_path.join(DOC_PATH)
88
+ end
89
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailInspector
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "syntax_tree"
4
+
5
+ module Visitor
6
+ class Attribute < SyntaxTree::Visitor
7
+ attr_reader :attribute_map
8
+
9
+ def initialize
10
+ @attribute_map = {}
11
+ @namespace_stack = []
12
+ end
13
+
14
+ def with_namespace(node)
15
+ @namespace_stack << node.constant.constant.value
16
+ visit_child_nodes(node)
17
+ @namespace_stack.pop
18
+ end
19
+
20
+ visit_method alias_method :visit_module, :with_namespace
21
+
22
+ visit_method alias_method :visit_class, :with_namespace
23
+
24
+ visit_method def visit_command(node)
25
+ attr_access = node.message.value
26
+ return unless ATTRIBUTE_METHODS.include?(attr_access)
27
+
28
+ full_namespace = @namespace_stack.join("::")
29
+
30
+ @attribute_map[full_namespace] ||= {}
31
+ @attribute_map[full_namespace][attr_access] ||= Set.new
32
+
33
+ attributes = node.arguments.parts.map { |p| p.value.value }
34
+
35
+ @attribute_map[full_namespace][attr_access].merge(attributes)
36
+ end
37
+
38
+ private
39
+
40
+ ATTRIBUTE_METHODS = %w[attr_accessor attr_reader attr_writer]
41
+ end
42
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "syntax_tree"
4
+
5
+ require_relative "./hash_to_string"
6
+ require_relative "./multiline_to_string"
7
+
8
+ module Visitor
9
+ class FrameworkDefault < SyntaxTree::Visitor
10
+ attr_reader :config_map
11
+
12
+ def initialize
13
+ @config_map = {}
14
+ @target_version_block = false
15
+ @current_version = nil
16
+ @current_framework = nil
17
+ end
18
+
19
+ visit_method def visit_case(node)
20
+ return unless target_version_case?(node.value)
21
+
22
+ @target_version_block = true
23
+ visit_child_nodes(node)
24
+ @target_version_block = false
25
+ end
26
+
27
+ visit_method def visit_when(node)
28
+ return unless @target_version_block
29
+
30
+ @current_version = node.arguments.parts[0].parts[0].value
31
+
32
+ @config_map[@current_version] = {}
33
+ visit_child_nodes(node)
34
+
35
+ @current_version = nil
36
+ end
37
+
38
+ visit_method def visit_if(node)
39
+ return unless @target_version_block
40
+
41
+ @current_framework =
42
+ case node
43
+ in predicate: SyntaxTree::CallNode[message: { value: "respond_to?" }]
44
+ node.predicate.arguments.arguments.parts[0].value.value
45
+ else
46
+ nil
47
+ end
48
+
49
+ visit_child_nodes(node)
50
+
51
+ @current_framework = nil
52
+ end
53
+
54
+ visit_method def visit_assign(node)
55
+ return unless @target_version_block
56
+
57
+ assert_framework(node)
58
+
59
+ target = SyntaxTree::Formatter.format(nil, node.target)
60
+ value =
61
+ case node.value
62
+ when SyntaxTree::HashLiteral
63
+ HashToString.new.tap { |v| v.visit(node.value) }.to_s
64
+ when SyntaxTree::StringConcat
65
+ MultilineToString.new.tap { |v| v.visit(node.value) }.to_s
66
+ else
67
+ SyntaxTree::Formatter.format(nil, node.value)
68
+ end
69
+ @config_map[@current_version][target] = value
70
+ end
71
+
72
+ private
73
+
74
+ def target_version_case?(node)
75
+ node in SyntaxTree::CallNode[
76
+ receiver: SyntaxTree::VarRef[
77
+ value: SyntaxTree::Ident[value: "target_version"]
78
+ ]
79
+ ]
80
+ end
81
+
82
+ def assert_framework(node)
83
+ framework =
84
+ case node.target.parent
85
+ in { value: SyntaxTree::Const } |
86
+ { value: SyntaxTree::Kw[value: "self"] }
87
+ nil
88
+ in receiver: { value: { value: framework } }
89
+ framework
90
+ in value: { value: framework }
91
+ framework
92
+ end
93
+
94
+ return if @current_framework == framework
95
+
96
+ raise "Expected #{@current_framework} to match #{framework}"
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "syntax_tree"
4
+
5
+ module Visitor
6
+ class HashToString < SyntaxTree::Visitor
7
+ attr_reader :to_s
8
+
9
+ def initialize
10
+ @to_s = +""
11
+ end
12
+
13
+ visit_methods do
14
+ def visit_assoc(node)
15
+ @to_s << " "
16
+ visit(node.key)
17
+
18
+ case node.key
19
+ when SyntaxTree::StringLiteral
20
+ @to_s << " => "
21
+ end
22
+
23
+ visit(node.value)
24
+ end
25
+
26
+ def visit_hash(node)
27
+ @to_s << "{"
28
+
29
+ if node.assocs.length > 0
30
+ visit(node.assocs[0])
31
+
32
+ if node.assocs.length > 1
33
+ node.assocs[1..-1].each do |a|
34
+ @to_s << ","
35
+ visit(a)
36
+ end
37
+ end
38
+ @to_s << " "
39
+ end
40
+
41
+ @to_s << "}"
42
+ end
43
+
44
+ def visit_int(node)
45
+ @to_s << node.value
46
+ end
47
+
48
+ def visit_kw(node)
49
+ @to_s << node.value
50
+ end
51
+
52
+ def visit_label(node)
53
+ @to_s << node.value
54
+ @to_s << " "
55
+ end
56
+
57
+ def visit_tstring_content(node)
58
+ @to_s << '"'
59
+ @to_s << node.value
60
+ @to_s << '"'
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "syntax_tree"
4
+
5
+ module Visitor
6
+ class MultilineToString < SyntaxTree::Visitor
7
+ attr_reader :to_s
8
+
9
+ def initialize
10
+ @to_s = +""
11
+ end
12
+
13
+ visit_methods do
14
+ def visit_string_concat(node)
15
+ @to_s << '"'
16
+ super(node)
17
+ @to_s << '"'
18
+ end
19
+
20
+ def visit_tstring_content(node)
21
+ @to_s << node.value
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rail_inspector/version"
4
+
5
+ module RailInspector
6
+ # Your code goes here...
7
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rail_inspector
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Hartley McGuire
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-03-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: syntax_tree
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 6.1.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 6.1.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: thor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ description:
42
+ email:
43
+ - skipkayhil@gmail.com
44
+ executables:
45
+ - railspect
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - Gemfile
50
+ - Gemfile.lock
51
+ - LICENSE
52
+ - README.md
53
+ - Rakefile
54
+ - exe/railspect
55
+ - lib/rail_inspector.rb
56
+ - lib/rail_inspector/changelog.rb
57
+ - lib/rail_inspector/cli.rb
58
+ - lib/rail_inspector/configuring.rb
59
+ - lib/rail_inspector/configuring/check/framework_defaults.rb
60
+ - lib/rail_inspector/configuring/check/general_configuration.rb
61
+ - lib/rail_inspector/version.rb
62
+ - lib/rail_inspector/visitor/attribute.rb
63
+ - lib/rail_inspector/visitor/framework_default.rb
64
+ - lib/rail_inspector/visitor/hash_to_string.rb
65
+ - lib/rail_inspector/visitor/multiline_to_string.rb
66
+ homepage: https://github.com/skipkayhil/rail_inspector
67
+ licenses:
68
+ - MIT
69
+ metadata:
70
+ homepage_uri: https://github.com/skipkayhil/rail_inspector
71
+ source_code_uri: https://github.com/skipkayhil/rail_inspector
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: 3.0.0
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubygems_version: 3.4.10
88
+ signing_key:
89
+ specification_version: 4
90
+ summary: A collection of linters for rails/rails
91
+ test_files: []