erb_lint 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|