erb_lint 0.0.4
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 +7 -0
- data/lib/erb_lint/linter.rb +40 -0
- data/lib/erb_lint/linter_registry.rb +22 -0
- data/lib/erb_lint/linters/deprecated_classes.rb +167 -0
- data/lib/erb_lint/linters/final_newline.rb +36 -0
- data/lib/erb_lint/runner.rb +53 -0
- data/lib/erb_lint.rb +10 -0
- metadata +78 -0
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
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: []
|