erb_lint 0.0.9 → 0.0.11

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
  SHA1:
3
- metadata.gz: 7ee1658d922ffcd67d6fac26d449d14af651c4d4
4
- data.tar.gz: 23d7dabea75b0b9cfdd788ce547e33b2584ac240
3
+ metadata.gz: eb68fe352cf7b67dba7ec568ad6d40bc87e740f5
4
+ data.tar.gz: 6cafdb08d656bcfe14666a7e00b9f144e741b62a
5
5
  SHA512:
6
- metadata.gz: 00322ff00a84d68edf04bbaf8041647b5291f1ad33543970309d3546e34018d11e3fc506be00f3ace186c473e88d7ea24a32f11af4fff0d1d124a87d9507083c
7
- data.tar.gz: 532d556c69502798da046667b90c8fdfa8ce5782a8a0a6e4c88808ac145794a95a9ffbbf4cf4097da224076738035bed14ab6e2ea9b2fadd2ea91c221522e767
6
+ metadata.gz: 1fb5769c9cd386762eae39367c60ce941719017bf20eb911d478ca17e5d8dd5f923226408369a6b870707ee15eb5f3f5c8bc5b0133535346501190e998ca75da
7
+ data.tar.gz: fcd8c9009447122ded1b08a0855bd5a60a755a71fc341a03b84995d7cb7945c1a28338e5bb64f5368d021c2ee055ded8e2a1b906dd974ec11d6cc1f159f352ca
data/exe/erblint ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift("#{__dir__}/../lib")
5
+
6
+ require 'erb_lint/cli'
7
+
8
+ cli = ERBLint::CLI.new
9
+ exit cli.run
data/lib/erb_lint.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'erb_lint/version'
4
+ require 'erb_lint/linter_config'
3
5
  require 'erb_lint/linter_registry'
4
6
  require 'erb_lint/linter'
7
+ require 'erb_lint/runner_config'
5
8
  require 'erb_lint/runner'
6
9
  require 'erb_lint/file_loader'
7
10
 
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb_lint'
4
+ require 'active_support'
5
+ require 'active_support/inflector'
6
+ require 'optparse'
7
+ require 'psych'
8
+ require 'yaml'
9
+ require 'colorize'
10
+
11
+ module ERBLint
12
+ class CLI
13
+ DEFAULT_CONFIG_FILENAME = '.erb-lint.yml'
14
+ DEFAULT_LINT_ALL_GLOB = "**/*.html{+*,}.erb"
15
+
16
+ class ExitWithFailure < RuntimeError; end
17
+ class ExitWithSuccess < RuntimeError; end
18
+
19
+ def initialize
20
+ @options = {}
21
+ @config = nil
22
+ @files = []
23
+ end
24
+
25
+ def run(args = ARGV)
26
+ load_options(args)
27
+ @files = args.dup
28
+
29
+ if lint_files.empty?
30
+ success!("no files given...\n#{option_parser}")
31
+ end
32
+
33
+ load_config
34
+ ensure_files_exist(lint_files)
35
+
36
+ puts "Linting #{lint_files.size} files with #{enabled_linter_classes.size} linters..."
37
+ puts
38
+
39
+ errors_found = false
40
+ runner_config = @config.merge(runner_config_override)
41
+ runner = ERBLint::Runner.new(file_loader, runner_config)
42
+ lint_files.each do |filename|
43
+ lint_result = runner.run(filename, File.read(filename))
44
+ errors = lint_result.map { |r| r[:errors] }.flatten
45
+ errors.each do |lint_error|
46
+ puts <<~EOF
47
+ #{lint_error[:message]}
48
+ In file: #{relative_filename(filename)}:#{lint_error[:line]}
49
+
50
+ EOF
51
+ errors_found = true
52
+ end
53
+ end
54
+
55
+ if errors_found
56
+ warn "Errors were found in ERB files".red
57
+ else
58
+ puts "No errors were found in ERB files".green
59
+ end
60
+
61
+ !errors_found
62
+ rescue OptionParser::InvalidOption, OptionParser::InvalidArgument, ExitWithFailure => e
63
+ warn e.message.red
64
+ false
65
+ rescue ExitWithSuccess => e
66
+ puts e.message
67
+ true
68
+ rescue => e
69
+ warn "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}".red
70
+ false
71
+ end
72
+
73
+ private
74
+
75
+ def config_filename
76
+ @config_filename ||= @options[:config] || DEFAULT_CONFIG_FILENAME
77
+ end
78
+
79
+ def load_config
80
+ if File.exist?(config_filename)
81
+ @config = RunnerConfig.new(file_loader.yaml(config_filename))
82
+ else
83
+ warn "#{config_filename} not found: using default config".yellow
84
+ @config = RunnerConfig.default
85
+ end
86
+ rescue Psych::SyntaxError => e
87
+ failure!("error parsing config: #{e.message}")
88
+ end
89
+
90
+ def file_loader
91
+ @file_loader ||= ERBLint::FileLoader.new(Dir.pwd)
92
+ end
93
+
94
+ def load_options(args)
95
+ option_parser.parse!(args)
96
+ end
97
+
98
+ def lint_files
99
+ if @options[:lint_all]
100
+ pattern = File.expand_path(DEFAULT_LINT_ALL_GLOB, Dir.pwd)
101
+ Dir[pattern]
102
+ else
103
+ @files.map { |f| f.include?('*') ? Dir[f] : f }.flatten.map { |f| File.expand_path(f, Dir.pwd) }
104
+ end
105
+ end
106
+
107
+ def failure!(msg)
108
+ raise ExitWithFailure, msg
109
+ end
110
+
111
+ def success!(msg)
112
+ raise ExitWithSuccess, msg
113
+ end
114
+
115
+ def ensure_files_exist(files)
116
+ files.each do |filename|
117
+ unless File.exist?(filename)
118
+ failure!("#{filename}: does not exist")
119
+ end
120
+ end
121
+ end
122
+
123
+ def known_linter_names
124
+ @known_linter_names ||= ERBLint::LinterRegistry.linters
125
+ .map(&:simple_name)
126
+ .map(&:underscore)
127
+ end
128
+
129
+ def enabled_linter_names
130
+ @enabled_linter_names ||=
131
+ @options[:enabled_linters] ||
132
+ known_linter_names.select { |klass| @config.for_linter(klass).enabled? }
133
+ end
134
+
135
+ def enabled_linter_classes
136
+ @enabled_linter_classes ||= ERBLint::LinterRegistry.linters
137
+ .select { |klass| enabled_linter_names.include?(klass.simple_name.underscore) }
138
+ end
139
+
140
+ def relative_filename(filename)
141
+ filename.sub("#{File.expand_path('.', Dir.pwd)}/", '')
142
+ end
143
+
144
+ def runner_config_override
145
+ if @options[:enabled_linters].present?
146
+ RunnerConfig.new(
147
+ linters: {}.tap do |linters|
148
+ ERBLint::LinterRegistry.linters.map do |klass|
149
+ linters[klass.simple_name] = { 'enabled' => enabled_linter_classes.include?(klass) }
150
+ end
151
+ end
152
+ )
153
+ else
154
+ RunnerConfig.new
155
+ end
156
+ end
157
+
158
+ def option_parser
159
+ OptionParser.new do |opts|
160
+ opts.banner = "Usage: erblint [options] [file1, file2, ...]"
161
+
162
+ opts.on("--config FILENAME", "Config file [default: #{DEFAULT_CONFIG_FILENAME}]") do |config|
163
+ if File.exist?(config)
164
+ @options[:config] = config
165
+ else
166
+ failure!("#{config}: does not exist")
167
+ end
168
+ end
169
+
170
+ opts.on("--lint-all", "Lint all files matching #{DEFAULT_LINT_ALL_GLOB}") do |config|
171
+ @options[:lint_all] = config
172
+ end
173
+
174
+ opts.on("--enable-all-linters", "Enable all known linters") do
175
+ @options[:enabled_linters] = known_linter_names
176
+ end
177
+
178
+ opts.on("--enable-linters LINTER[,LINTER,...]", Array,
179
+ "Only use specified linter", "Known linters are: #{known_linter_names.join(', ')}") do |linters|
180
+ linters.each do |linter|
181
+ unless known_linter_names.include?(linter)
182
+ failure!("#{linter}: not a valid linter name (#{known_linter_names.join(', ')})")
183
+ end
184
+ end
185
+ @options[:enabled_linters] = linters
186
+ end
187
+
188
+ opts.on_tail("-h", "--help", "Show this message") do
189
+ success!(opts)
190
+ end
191
+
192
+ opts.on_tail("--version", "Show version") do
193
+ success!(ERBLint::VERSION)
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
@@ -5,6 +5,7 @@ module ERBLint
5
5
  class Linter
6
6
  class << self
7
7
  attr_accessor :simple_name
8
+ attr_accessor :config_schema
8
9
 
9
10
  # When defining a Linter class, define its simple name as well. This
10
11
  # assumes that the module hierarchy of every linter starts with
@@ -13,15 +14,31 @@ module ERBLint
13
14
  # `ERBLint::Linters::Foo.simple_name` #=> "Foo"
14
15
  # `ERBLint::Linters::Compass::Bar.simple_name` #=> "Compass::Bar"
15
16
  def inherited(linter)
16
- name_parts = linter.name.split('::')
17
- name = name_parts.length < 3 ? '' : name_parts[2..-1].join('::')
18
- linter.simple_name = name
17
+ linter.simple_name = if linter.name.start_with?('ERBLint::Linters::')
18
+ name_parts = linter.name.split('::')
19
+ name_parts[2..-1].join('::')
20
+ else
21
+ linter.name
22
+ end
23
+
24
+ linter.config_schema = LinterConfig
19
25
  end
20
26
  end
21
27
 
22
28
  # Must be implemented by the concrete inheriting class.
23
- def initialize(file_loader, _config)
29
+ def initialize(file_loader, config)
24
30
  @file_loader = file_loader
31
+ @config = config
32
+ raise ArgumentError, "expect `config` to be #{self.class.config_schema} instance, "\
33
+ "not #{config.class}" unless config.is_a?(self.class.config_schema)
34
+ end
35
+
36
+ def enabled?
37
+ @config.enabled?
38
+ end
39
+
40
+ def excludes_file?(filename)
41
+ @config.excludes_file?(filename)
25
42
  end
26
43
 
27
44
  def lint_file(file_content)
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'smart_properties'
5
+
6
+ module ERBLint
7
+ class LinterConfig
8
+ include SmartProperties
9
+
10
+ class Error < StandardError; end
11
+
12
+ class << self
13
+ def array_of?(klass)
14
+ lambda { |value| value.is_a?(Array) && value.all? { |s| s.is_a?(klass) } }
15
+ end
16
+
17
+ def to_array_of(klass)
18
+ lambda { |entries| entries.map { |entry| klass.new(entry) } }
19
+ end
20
+ end
21
+
22
+ property :enabled, accepts: [true, false], default: false, reader: :enabled?
23
+ property :exclude, accepts: array_of?(String), default: []
24
+
25
+ def initialize(config = {})
26
+ config = config.dup.deep_stringify_keys
27
+ allowed_keys = self.class.properties.keys.map(&:to_s)
28
+ given_keys = config.keys
29
+ if (extra_keys = given_keys - allowed_keys).any?
30
+ raise Error, "Given key is not allowed: #{extra_keys.join(', ')}"
31
+ end
32
+ super(config)
33
+ rescue SmartProperties::InitializationError => e
34
+ raise Error, "The following properties are required to be set: #{e.properties}"
35
+ rescue SmartProperties::InvalidValueError => e
36
+ raise Error, e.message
37
+ end
38
+
39
+ def [](name)
40
+ unless self.class.properties.key?(name)
41
+ raise Error, "No such property: #{name}"
42
+ end
43
+ super
44
+ end
45
+
46
+ def to_hash
47
+ {}.tap do |hash|
48
+ self.class.properties.to_hash.each_key do |key|
49
+ hash[key.to_s] = self[key]
50
+ end
51
+ end
52
+ end
53
+
54
+ def excludes_file?(filename)
55
+ exclude.any? do |path|
56
+ File.fnmatch?(path, filename)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -13,6 +13,10 @@ module ERBLint
13
13
  @linters << linter_class
14
14
  end
15
15
 
16
+ def find_by_name(name)
17
+ linters.detect { |linter| linter.simple_name == name }
18
+ end
19
+
16
20
  def load_custom_linters(directory = CUSTOM_LINTERS_DIR)
17
21
  ruby_files = Dir.glob(File.expand_path(File.join(directory, '**', '*.rb')))
18
22
  ruby_files.each { |file| require file }
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'better_html'
3
+ require 'better_html/parser'
4
4
 
5
5
  module ERBLint
6
6
  module Linters
@@ -8,67 +8,79 @@ module ERBLint
8
8
  class DeprecatedClasses < Linter
9
9
  include LinterRegistry
10
10
 
11
- def initialize(file_loader, config)
12
- super
11
+ class RuleSet
12
+ include SmartProperties
13
+ property :suggestion, accepts: String, default: ''
14
+ property :deprecated, accepts: LinterConfig.array_of?(String), default: []
15
+ end
13
16
 
14
- @deprecated_ruleset = []
15
- config.fetch('rule_set', []).each do |rule|
16
- suggestion = rule.fetch('suggestion', '')
17
- rule.fetch('deprecated', []).each do |class_expr|
18
- @deprecated_ruleset.push(
19
- class_expr: class_expr,
20
- suggestion: suggestion
21
- )
22
- end
23
- end
24
- @deprecated_ruleset.freeze
17
+ class ConfigSchema < LinterConfig
18
+ property :rule_set,
19
+ default: [],
20
+ accepts: array_of?(RuleSet),
21
+ converts: to_array_of(RuleSet)
22
+ property :addendum, accepts: String
23
+ end
24
+ self.config_schema = ConfigSchema
25
25
 
26
- @addendum = config.fetch('addendum', '')
26
+ def initialize(file_loader, config)
27
+ super
28
+ @addendum = @config.addendum
27
29
  end
28
30
 
29
31
  def lint_file(file_content)
30
32
  errors = []
31
- iterator = build_iterator(file_content)
32
- each_class_name_with_line(iterator) do |class_name, line|
33
- errors.push(*generate_errors(class_name, line))
33
+ parser = build_parser(file_content)
34
+ class_name_with_loc(parser).each do |class_name, loc|
35
+ errors.push(*generate_errors(class_name, loc.line))
34
36
  end
35
- each_texthtml_script(iterator) do |text_node|
36
- errors.push(*lint_file(text_node.content))
37
+ text_tags_content(parser).each do |content|
38
+ errors.push(*lint_file(content))
37
39
  end
38
40
  errors
39
41
  end
40
42
 
41
43
  private
42
44
 
43
- def build_iterator(file_content)
44
- BetterHtml::NodeIterator.new(file_content, template_language: :html)
45
+ def build_parser(file_content)
46
+ BetterHtml::Parser.new(file_content, template_language: :html)
45
47
  end
46
48
 
47
- def each_class_name_with_line(iterator)
48
- each_element_with_index(iterator) do |element, _index|
49
- klass = element.find_attr('class')
50
- next unless klass
51
- klass.value_without_quotes.split(' ').each do |class_name|
52
- yield class_name, klass.name_parts.first.location.line
49
+ def class_name_with_loc(parser)
50
+ Enumerator.new do |yielder|
51
+ tags(parser).each do |tag|
52
+ class_value = tag.attributes['class']&.value
53
+ next unless class_value
54
+ class_value.split(' ').each do |class_name|
55
+ yielder.yield(class_name, tag.loc)
56
+ end
53
57
  end
54
58
  end
55
59
  end
56
60
 
57
- def each_element_with_index(iterator)
58
- iterator.nodes.each_with_index do |node, index|
59
- yield node, index if node.element?
61
+ def text_tags_content(parser)
62
+ Enumerator.new do |yielder|
63
+ script_tags(parser)
64
+ .select { |tag| tag.attributes['type']&.value == 'text/html' }
65
+ .each do |tag|
66
+ index = parser.ast.to_a.find_index(tag.node)
67
+ next_node = parser.ast.to_a[index + 1]
68
+
69
+ yielder.yield(next_node.loc.source) if next_node.type == :text
70
+ end
60
71
  end
61
72
  end
62
73
 
63
- def each_texthtml_script(iterator)
64
- each_element_with_index(iterator) do |element, index|
65
- type = element.find_attr('type')
66
- next unless type
67
- next unless type.value_without_quotes == 'text/html'
68
- next_node = iterator.nodes[index + 1]
74
+ def script_tags(parser)
75
+ tags(parser).select { |tag| tag.name == 'script' }
76
+ end
69
77
 
70
- yield next_node if next_node&.text?
71
- end
78
+ def tags(parser)
79
+ tag_nodes(parser).map { |tag_node| BetterHtml::Tree::Tag.from_node(tag_node) }
80
+ end
81
+
82
+ def tag_nodes(parser)
83
+ parser.nodes_with_type(:tag)
72
84
  end
73
85
 
74
86
  def generate_errors(class_name, line_number)
@@ -83,8 +95,17 @@ module ERBLint
83
95
  end
84
96
 
85
97
  def violated_rules(class_name)
86
- @deprecated_ruleset.select do |deprecated_rule|
87
- /\A#{deprecated_rule[:class_expr]}\z/.match(class_name)
98
+ [].tap do |result|
99
+ @config.rule_set.each do |rule|
100
+ rule.deprecated.each do |deprecated|
101
+ next unless /\A#{deprecated}\z/ =~ class_name
102
+
103
+ result << {
104
+ suggestion: rule.suggestion,
105
+ class_expr: deprecated,
106
+ }
107
+ end
108
+ end
88
109
  end
89
110
  end
90
111
  end
@@ -10,9 +10,15 @@ module ERBLint
10
10
  include LinterRegistry
11
11
  include BetterHtml::TestHelper::SafeErbTester
12
12
 
13
+ class ConfigSchema < LinterConfig
14
+ property :better_html_config, accepts: String
15
+ end
16
+
17
+ self.config_schema = ConfigSchema
18
+
13
19
  def initialize(file_loader, config)
14
20
  super
15
- @config_filename = config['better-html-config']
21
+ @config_filename = @config.better_html_config
16
22
  end
17
23
 
18
24
  def lint_file(file_content)
@@ -6,9 +6,14 @@ module ERBLint
6
6
  class FinalNewline < Linter
7
7
  include LinterRegistry
8
8
 
9
+ class ConfigSchema < LinterConfig
10
+ property :present, accepts: [true, false], default: true, reader: :present?
11
+ end
12
+ self.config_schema = ConfigSchema
13
+
9
14
  def initialize(file_loader, config)
10
15
  super
11
- @new_lines_should_be_present = config['present'].nil? ? true : config['present']
16
+ @new_lines_should_be_present = @config.present?
12
17
  end
13
18
 
14
19
  protected
@@ -10,27 +10,34 @@ module ERBLint
10
10
  class Rubocop < Linter
11
11
  include LinterRegistry
12
12
 
13
+ class ConfigSchema < LinterConfig
14
+ property :only, accepts: array_of?(String)
15
+ property :rubocop_config, accepts: Hash
16
+ end
17
+
18
+ self.config_schema = ConfigSchema
19
+
13
20
  # copied from Rails: action_view/template/handlers/erb/erubi.rb
14
21
  BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
15
22
 
16
- def initialize(file_loader, config_hash)
23
+ def initialize(file_loader, config)
17
24
  super
18
- @enabled_cops = config_hash.delete('only')
19
- tempfile_from('.erblint-rubocop', config_hash.except('enabled').to_yaml) do |tempfile|
20
- custom_config = RuboCop::ConfigLoader.load_file(tempfile.path)
21
- @config = RuboCop::ConfigLoader.merge_with_default(custom_config, '')
22
- end
25
+ @only_cops = @config.only
26
+ custom_config = config_from_hash(@config.rubocop_config)
27
+ @rubocop_config = RuboCop::ConfigLoader.merge_with_default(custom_config, '')
23
28
  end
24
29
 
25
30
  def lint_file(file_content)
26
31
  errors = []
27
- erb = BetterHtml::NodeIterator::HtmlErb.new(file_content)
28
- erb.tokens.each do |token|
29
- next unless [:stmt, :expr_literal, :expr_escaped].include?(token.type)
30
- ruby_code = token.code.sub(BLOCK_EXPR, '')
31
- offenses = inspect_content(ruby_code)
32
+ parser = BetterHtml::Parser.new(file_content, template_language: :html)
33
+ parser.ast.descendants(:erb).each do |erb_node|
34
+ _, _, code_node, = *erb_node
35
+ code = code_node.loc.source.sub(/\A[[:blank:]]*/, '')
36
+ code = "#{' ' * erb_node.loc.column}#{code}"
37
+ code = code.sub(BLOCK_EXPR, '')
38
+ offenses = inspect_content(code)
32
39
  offenses&.each do |offense|
33
- errors << format_error(token, offense)
40
+ errors << format_error(code_node, offense)
34
41
  end
35
42
  end
36
43
  errors
@@ -57,23 +64,61 @@ module ERBLint
57
64
  def processed_source(content)
58
65
  RuboCop::ProcessedSource.new(
59
66
  content,
60
- @config.target_ruby_version,
67
+ @rubocop_config.target_ruby_version,
61
68
  '(erb)'
62
69
  )
63
70
  end
64
71
 
65
72
  def team
66
- selected_cops = RuboCop::Cop::Cop.all.select { |cop| cop.match?(@enabled_cops) }
67
- cop_classes = RuboCop::Cop::Registry.new(selected_cops)
68
- RuboCop::Cop::Team.new(cop_classes, @config, extra_details: true, display_cop_names: true)
73
+ cop_classes =
74
+ if @only_cops.present?
75
+ selected_cops = RuboCop::Cop::Cop.all.select { |cop| cop.match?(@only_cops) }
76
+ RuboCop::Cop::Registry.new(selected_cops)
77
+ elsif @rubocop_config['Rails']['Enabled']
78
+ RuboCop::Cop::Registry.new(RuboCop::Cop::Cop.all)
79
+ else
80
+ RuboCop::Cop::Cop.non_rails
81
+ end
82
+ RuboCop::Cop::Team.new(cop_classes, @rubocop_config, extra_details: true, display_cop_names: true)
69
83
  end
70
84
 
71
- def format_error(token, offense)
85
+ def format_error(code_node, offense)
72
86
  {
73
- line: token.location.line + offense.line - 1,
87
+ line: code_node.loc.line + offense.line - 1,
74
88
  message: offense.message.strip
75
89
  }
76
90
  end
91
+
92
+ def config_from_hash(hash)
93
+ inherit_from = hash.delete('inherit_from')
94
+ resolve_inheritance(hash, inherit_from)
95
+
96
+ tempfile_from('.erblint-rubocop', hash.to_yaml) do |tempfile|
97
+ RuboCop::ConfigLoader.load_file(tempfile.path)
98
+ end
99
+ end
100
+
101
+ def resolve_inheritance(hash, inherit_from)
102
+ base_configs(inherit_from)
103
+ .reverse_each do |base_config|
104
+ base_config.each do |k, v|
105
+ hash[k] = hash.key?(k) ? RuboCop::ConfigLoader.merge(v, hash[k]) : v if v.is_a?(Hash)
106
+ end
107
+ end
108
+ end
109
+
110
+ def base_configs(inherit_from)
111
+ regex = URI::DEFAULT_PARSER.make_regexp(%w(http https))
112
+ configs = Array(inherit_from).compact.map do |base_name|
113
+ if base_name =~ /\A#{regex}\z/
114
+ RuboCop::ConfigLoader.load_file(RuboCop::RemoteConfig.new(base_name, Dir.pwd))
115
+ else
116
+ config_from_hash(@file_loader.yaml(base_name))
117
+ end
118
+ end
119
+
120
+ configs.compact
121
+ end
77
122
  end
78
123
  end
79
124
  end
@@ -3,20 +3,20 @@
3
3
  module ERBLint
4
4
  # Runs all enabled linters against an html.erb file.
5
5
  class Runner
6
- def initialize(file_loader, config = {})
6
+ def initialize(file_loader, config)
7
7
  @file_loader = file_loader
8
- @config = default_config.merge(config || {})
8
+ @config = config || RunnerConfig.default
9
+ raise ArgumentError, 'expect `config` to be a RunnerConfig instance' unless @config.is_a?(RunnerConfig)
9
10
 
10
11
  LinterRegistry.load_custom_linters
11
- @linters = LinterRegistry.linters.select { |linter_class| linter_enabled?(linter_class) }
12
- @linters.map! do |linter_class|
13
- linter_config = @config.dig('linters', linter_class.simple_name)
14
- linter_class.new(@file_loader, linter_config)
12
+ linter_classes = LinterRegistry.linters.select { |klass| @config.for_linter(klass).enabled? }
13
+ @linters = linter_classes.map do |linter_class|
14
+ linter_class.new(@file_loader, @config.for_linter(linter_class))
15
15
  end
16
16
  end
17
17
 
18
18
  def run(filename, file_content)
19
- linters_for_file = @linters.reject { |linter| linter_excludes_file?(linter, filename) }
19
+ linters_for_file = @linters.reject { |linter| linter.excludes_file?(filename) }
20
20
  linters_for_file.map do |linter|
21
21
  {
22
22
  linter_name: linter.class.simple_name,
@@ -24,31 +24,5 @@ module ERBLint
24
24
  }
25
25
  end
26
26
  end
27
-
28
- private
29
-
30
- def linter_enabled?(linter_class)
31
- linter_config = @config.dig('linters', linter_class.simple_name)
32
- return false if linter_config.nil?
33
- linter_config['enabled'] || false
34
- end
35
-
36
- def linter_excludes_file?(linter, filename)
37
- excluded_filepaths = @config.dig('linters', linter.class.simple_name, 'exclude') || []
38
- excluded_filepaths.each do |path|
39
- return true if File.fnmatch?(path, filename)
40
- end
41
- false
42
- end
43
-
44
- def default_config
45
- {
46
- 'linters' => {
47
- 'FinalNewline' => {
48
- 'enabled' => true
49
- }
50
- }
51
- }
52
- end
53
27
  end
54
28
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ERBLint
4
+ class RunnerConfig
5
+ class Error < StandardError; end
6
+
7
+ def initialize(config = nil)
8
+ @config = (config || {}).dup.deep_stringify_keys
9
+ end
10
+
11
+ def to_hash
12
+ @config.dup
13
+ end
14
+
15
+ def for_linter(klass)
16
+ klass_name = if klass.is_a?(String)
17
+ klass.to_s
18
+ elsif klass.is_a?(Class) && klass <= ERBLint::Linter
19
+ klass.simple_name
20
+ else
21
+ raise ArgumentError, 'expected String or linter class'
22
+ end
23
+ linter_klass = LinterRegistry.find_by_name(klass_name)
24
+ raise Error, "#{klass_name}: linter not found (is it loaded?)" unless linter_klass
25
+ linter_klass.config_schema.new(linters_config[klass_name] || {})
26
+ end
27
+
28
+ def merge(other_config)
29
+ self.class.new(@config.deep_merge(other_config.to_hash))
30
+ end
31
+
32
+ def merge!(other_config)
33
+ @config.deep_merge!(other_config.to_hash)
34
+ self
35
+ end
36
+
37
+ class << self
38
+ def default
39
+ new(
40
+ linters: {
41
+ FinalNewline: {
42
+ enabled: true,
43
+ },
44
+ },
45
+ )
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def linters_config
52
+ @config['linters'] || {}
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ERBLint
4
+ VERSION = '0.0.11'
5
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: erb_lint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Chan
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2017-11-14 00:00:00.000000000 Z
11
+ date: 2018-01-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: better_html
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.0.10
19
+ version: 1.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.0.10
26
+ version: 1.0.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: html_tokenizer
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -40,6 +40,48 @@ dependencies:
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.51'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.51'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: smart_properties
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: colorize
43
85
  requirement: !ruby/object:Gem::Requirement
44
86
  requirements:
45
87
  - - ">="
@@ -83,19 +125,25 @@ dependencies:
83
125
  description: ERB Linter tool.
84
126
  email:
85
127
  - justin.the.c@gmail.com
86
- executables: []
128
+ executables:
129
+ - erblint
87
130
  extensions: []
88
131
  extra_rdoc_files: []
89
132
  files:
133
+ - exe/erblint
90
134
  - lib/erb_lint.rb
135
+ - lib/erb_lint/cli.rb
91
136
  - lib/erb_lint/file_loader.rb
92
137
  - lib/erb_lint/linter.rb
138
+ - lib/erb_lint/linter_config.rb
93
139
  - lib/erb_lint/linter_registry.rb
94
140
  - lib/erb_lint/linters/deprecated_classes.rb
95
141
  - lib/erb_lint/linters/erb_safety.rb
96
142
  - lib/erb_lint/linters/final_newline.rb
97
143
  - lib/erb_lint/linters/rubocop.rb
98
144
  - lib/erb_lint/runner.rb
145
+ - lib/erb_lint/runner_config.rb
146
+ - lib/erb_lint/version.rb
99
147
  homepage: https://github.com/justinthec/erb-lint
100
148
  licenses:
101
149
  - MIT