erb_lint 0.0.4

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
+ SHA1:
3
+ metadata.gz: 8866aae317226178c623de53d4c5e0791643c894
4
+ data.tar.gz: fac55efcf4a15f251f537af97039b8669960c00b
5
+ SHA512:
6
+ metadata.gz: b92425a8c4fbe2c65fde42a8dab091c841e2bc0f70b29833e49c06fc99acc85b42639dc9129f200b99195930e9f7fbb96d58fdcaaefb947cd40ad76008ffe765
7
+ data.tar.gz: aefbb18d6756d5e08045c4d5c8c0afb6ef8fb897f0b73c72fe905158b40d1cbc1114bfada2ec59029165b1a9d72930a172c17b6cfe059a11283bf8ec80e41bc9
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ERBLint
4
+ # Defines common functionality available to all linters.
5
+ class Linter
6
+ class << self
7
+ attr_accessor :simple_name
8
+
9
+ # When defining a Linter class, define its simple name as well. This
10
+ # assumes that the module hierarchy of every linter starts with
11
+ # `ERBLint::Linters::`, and removes this part of the class name.
12
+ #
13
+ # `ERBLint::Linters::Foo.simple_name` #=> "Foo"
14
+ # `ERBLint::Linters::Compass::Bar.simple_name` #=> "Compass::Bar"
15
+ 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
19
+ end
20
+ end
21
+
22
+ # Must be implemented by the concrete inheriting class.
23
+ def initialize(_config)
24
+ raise NotImplementedError, "must implement ##{__method__}"
25
+ end
26
+
27
+ def lint_file(file_content)
28
+ lines = file_content.scan(/[^\n]*\n|[^\n]+/)
29
+ lint_lines(lines)
30
+ end
31
+
32
+ protected
33
+
34
+ # The lint_lines method that contains the logic for the linter and returns a list of errors.
35
+ # Must be implemented by the concrete inheriting class.
36
+ def lint_lines(_lines)
37
+ raise NotImplementedError, "must implement ##{__method__}"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ERBLint
4
+ # Stores all linters available to the application.
5
+ module LinterRegistry
6
+ CUSTOM_LINTERS_DIR = '.erb-linters'
7
+ @linters = []
8
+
9
+ class << self
10
+ attr_reader :linters
11
+
12
+ def included(linter_class)
13
+ @linters << linter_class
14
+ end
15
+
16
+ def load_custom_linters(directory = CUSTOM_LINTERS_DIR)
17
+ ruby_files = Dir.glob(File.expand_path(File.join(directory, '**', '*.rb')))
18
+ ruby_files.each { |file| require file }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ERBLint
4
+ module Linters
5
+ # Checks for deprecated classes in the start tags of HTML elements.
6
+ class DeprecatedClasses < Linter
7
+ include LinterRegistry
8
+
9
+ def initialize(config)
10
+ @deprecated_ruleset = []
11
+ config.fetch('rule_set', []).each do |rule|
12
+ suggestion = rule.fetch('suggestion', '')
13
+ rule.fetch('deprecated', []).each do |class_expr|
14
+ @deprecated_ruleset.push(
15
+ class_expr: class_expr,
16
+ suggestion: suggestion
17
+ )
18
+ end
19
+ end
20
+ @deprecated_ruleset.freeze
21
+
22
+ @addendum = config.fetch('addendum', '')
23
+ end
24
+
25
+ protected
26
+
27
+ def lint_lines(lines)
28
+ errors = []
29
+
30
+ lines.each_with_index do |line, index|
31
+ start_tags = StartTagHelper.start_tags(line)
32
+ start_tags.each do |start_tag|
33
+ start_tag.attributes.select(&:class?).each do |class_attr|
34
+ class_attr.value.split(' ').each do |class_name|
35
+ errors.push(*generate_errors(class_name, index + 1))
36
+ end
37
+ end
38
+ end
39
+ end
40
+ errors
41
+ end
42
+
43
+ private
44
+
45
+ def generate_errors(class_name, line_number)
46
+ violated_rules(class_name).map do |violated_rule|
47
+ suggestion = " #{violated_rule[:suggestion]}".rstrip
48
+ message = "Deprecated class `%s` detected matching the pattern `%s`.%s #{@addendum}".strip
49
+ {
50
+ line: line_number,
51
+ message: format(message, class_name, violated_rule[:class_expr], suggestion)
52
+ }
53
+ end
54
+ end
55
+
56
+ def violated_rules(class_name)
57
+ @deprecated_ruleset.select do |deprecated_rule|
58
+ /\A#{deprecated_rule[:class_expr]}\z/.match(class_name)
59
+ end
60
+ end
61
+ end
62
+
63
+ # Provides methods and classes for finding HTML start tags and their attributes.
64
+ module StartTagHelper
65
+ # These patterns cover a superset of the W3 HTML5 specification.
66
+ # Additional cases not included in the spec include those that are still rendered by some browsers.
67
+
68
+ # Attribute Patterns
69
+ # https://www.w3.org/TR/html5/syntax.html#syntax-attributes
70
+
71
+ # attribute names must be non empty and can't contain a certain set of special characters
72
+ ATTRIBUTE_NAME_PATTERN = %r{[^\s"'>\/=]+}
73
+
74
+ ATTRIBUTE_VALUE_PATTERN = %r{
75
+ "([^"]*)" | # double-quoted value
76
+ '([^']*)' | # single-quoted value
77
+ ([^\s"'=<>`]+) # unquoted non-empty value without special characters
78
+ }x
79
+
80
+ # attributes can be empty or have an attribute value
81
+ ATTRIBUTE_PATTERN = %r{
82
+ #{ATTRIBUTE_NAME_PATTERN} # attribute name
83
+ (
84
+ \s*=\s* # any whitespace around equals sign
85
+ (#{ATTRIBUTE_VALUE_PATTERN}) # attribute value
86
+ )? # attributes can be empty or have an assignemnt.
87
+ }x
88
+
89
+ # Start tag Patterns
90
+ # https://www.w3.org/TR/html5/syntax.html#syntax-start-tag
91
+
92
+ TAG_NAME_PATTERN = /[A-Za-z0-9]+/ # maybe add _ < ? etc later since it gets interpreted by some browsers
93
+
94
+ START_TAG_PATTERN = %r{
95
+ <(#{TAG_NAME_PATTERN}) # start of tag with tag name
96
+ (
97
+ (
98
+ \s+ # required whitespace between tag name and first attribute and between attributes
99
+ #{ATTRIBUTE_PATTERN} # attributes
100
+ )*
101
+ )? # having an attribute block is optional
102
+ \/?> # void or foreign elements can have a slash before tag close
103
+ }x
104
+
105
+ # Represents and provides an interface for a start tag found in the HTML.
106
+ class StartTag
107
+ attr_accessor :tag_name, :attributes
108
+
109
+ def initialize(tag_name, attributes)
110
+ @tag_name = tag_name
111
+ @attributes = attributes
112
+ end
113
+ end
114
+
115
+ # Represents and provides an interface for an attribute found in a start tag in the HTML.
116
+ class Attribute
117
+ ATTR_NAME_CLASS_PATTERN = /\Aclass\z/i # attribute names are case-insensitive
118
+ attr_accessor :attribute_name, :value
119
+
120
+ def initialize(attribute_name, value)
121
+ @attribute_name = attribute_name
122
+ @value = value
123
+ end
124
+
125
+ def class?
126
+ ATTR_NAME_CLASS_PATTERN.match(@attribute_name)
127
+ end
128
+ end
129
+
130
+ class << self
131
+ def start_tags(line)
132
+ # TODO: Implement String Scanner to track quotes before the start tag begins to ensure that it is
133
+ # not enclosed inside of a string. Alternatively this problem would be solved by using
134
+ # a 3rd party parser like Nokogiri::XML
135
+
136
+ start_tag_matching_groups = line.scan(/(#{START_TAG_PATTERN})/)
137
+ start_tag_matching_groups.map do |start_tag_matching_group|
138
+ tag_name = start_tag_matching_group[1]
139
+
140
+ # attributes_string can be nil if there is no space after the tag name (and therefore no attributes).
141
+ attributes_string = start_tag_matching_group[2] || ''
142
+
143
+ attribute_list = attributes(attributes_string)
144
+
145
+ StartTag.new(tag_name, attribute_list)
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ def attributes(attributes_string)
152
+ attributes_string.scan(/(#{ATTRIBUTE_PATTERN})/).map do |attribute_matching_group|
153
+ entire_string = attribute_matching_group[0]
154
+ value_with_equal_sign = attribute_matching_group[1] || '' # This can be nil if attribute is empty
155
+ name = entire_string.sub(value_with_equal_sign, '')
156
+
157
+ # The 3 captures [3..5] are the possibilities specified in ATTRIBUTE_VALUE_PATTERN
158
+ possible_value_formats = attribute_matching_group[3..5]
159
+ value = possible_value_formats.reduce { |a, e| a.nil? ? e : a }
160
+
161
+ Attribute.new(name, value)
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ERBLint
4
+ module Linters
5
+ # Checks for final newlines at the end of a file.
6
+ class FinalNewline < Linter
7
+ include LinterRegistry
8
+
9
+ def initialize(config)
10
+ @new_lines_should_be_present = config['present'].nil? ? true : config['present']
11
+ end
12
+
13
+ protected
14
+
15
+ def lint_lines(lines)
16
+ errors = []
17
+ return errors if lines.empty?
18
+
19
+ ends_with_newline = lines.last.chars[-1] == "\n"
20
+
21
+ if @new_lines_should_be_present && !ends_with_newline
22
+ errors.push(
23
+ line: lines.length,
24
+ message: 'Missing a trailing newline at the end of the file.'
25
+ )
26
+ elsif !@new_lines_should_be_present && ends_with_newline
27
+ errors.push(
28
+ line: lines.length,
29
+ message: 'Remove the trailing newline at the end of the file.'
30
+ )
31
+ end
32
+ errors
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ERBLint
4
+ # Runs all enabled linters against an html.erb file.
5
+ class Runner
6
+ def initialize(config = {})
7
+ @config = default_config.merge(config || {})
8
+
9
+ LinterRegistry.load_custom_linters
10
+ @linters = LinterRegistry.linters.select { |linter_class| linter_enabled?(linter_class) }
11
+ @linters.map! do |linter_class|
12
+ linter_config = @config.dig('linters', linter_class.simple_name)
13
+ linter_class.new(linter_config)
14
+ end
15
+ end
16
+
17
+ def run(filename, file_content)
18
+ linters_for_file = @linters.select { |linter| !linter_excludes_file?(linter, filename) }
19
+ linters_for_file.map do |linter|
20
+ {
21
+ linter_name: linter.class.simple_name,
22
+ errors: linter.lint_file(file_content)
23
+ }
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def linter_enabled?(linter_class)
30
+ linter_config = @config.dig('linters', linter_class.simple_name)
31
+ return false if linter_config.nil?
32
+ linter_config['enabled'] || false
33
+ end
34
+
35
+ def linter_excludes_file?(linter, filename)
36
+ excluded_filepaths = @config.dig('linters', linter.class.simple_name, 'exclude') || []
37
+ excluded_filepaths.each do |path|
38
+ return true if File.fnmatch?(path, filename)
39
+ end
40
+ false
41
+ end
42
+
43
+ def default_config
44
+ {
45
+ 'linters' => {
46
+ 'FinalNewline' => {
47
+ 'enabled' => true
48
+ }
49
+ }
50
+ }
51
+ end
52
+ end
53
+ end
data/lib/erb_lint.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb_lint/linter_registry'
4
+ require 'erb_lint/linter'
5
+ require 'erb_lint/runner'
6
+
7
+ # Load linters
8
+ Dir[File.expand_path('erb_lint/linters/**/*.rb', File.dirname(__FILE__))].each do |file|
9
+ require file
10
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: erb_lint
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Justin Chan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-10-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
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'
41
+ description: ERB Linter tool.
42
+ email:
43
+ - justin.the.c@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - lib/erb_lint.rb
49
+ - lib/erb_lint/linter.rb
50
+ - lib/erb_lint/linter_registry.rb
51
+ - lib/erb_lint/linters/deprecated_classes.rb
52
+ - lib/erb_lint/linters/final_newline.rb
53
+ - lib/erb_lint/runner.rb
54
+ homepage: https://github.com/justinthec/erb-lint
55
+ licenses:
56
+ - MIT
57
+ metadata: {}
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubyforge_project:
74
+ rubygems_version: 2.5.2.1
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: ERB lint tool
78
+ test_files: []