rail_inspector 0.0.1

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 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: []