Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert Nokogiri refactor #11

Merged
merged 4 commits into from
Oct 26, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 0 additions & 4 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,3 @@ Style/IndentHash:

Style/RegexpLiteral:
EnforcedStyle: mixed

Metrics/ModuleLength:
CountComments: false
Max: 120
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
language: ruby
rvm:
- 2.3.1
- 2.3.0
script:
- bundle exec rspec spec
- bundle exec rubocop
44 changes: 1 addition & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ default_config = {

## Linters

`erb-lint` comes with 3 linters on-board: `DeprecatedClasses`, `ContentStyle`, and `FinalNewline`, each with their own linter-specific options.
`erb-lint` comes with 2 linters on-board: `DeprecatedClasses` and `FinalNewline`, each with their own linter-specific options.

### DeprecatedClasses

Expand Down Expand Up @@ -139,48 +139,6 @@ Linter-Specific Option | Description
`suggestion` | A string to be included in the rule's error message. Make this informative and specific to the rule that it is contained in.
`addendum` | A string to be included at the end of every error message of the rule set. (Optional)

### ContentStyle

ContentStyle will find any words or phrases that
violate the rule set that you provide.

This `rule_set` is specified as a list of rules, each with a `violation` set and
a corresponding `suggestion`. Optionally, you can also add a `case_insensitive:
true` value to make ContentStyle ignore case when searching for violations.
If your `violation` is a regex pattern, you can add a `pattern_description` string
to replace the pattern in the error message.

```ruby
'rule_set' => [
{
'violation' => ['application', 'program'],
'suggestion' => 'app'
'case_insensitive' => true
},
{
'violation' => 'support page',
'suggestion' => 'Lintercorp Help Center'
},
'violation' => '\d+ ?(—|-) ?\d+'
'suggestion' => '— (en dash) in number ranges'
'pattern_description' => '- (hyphen) or — (em dash) in number ranges'
}
]
```

You can also specify an addendum to be added to the end of each error message
using the `addendum` option. The error message format is: `"Don't use #{violation}. Do use #{suggestion}"`
or `"Don't use #{violation}. Do use #{suggestion}. #{addendum}"` if an `addendum` is present.

Linter-Specific Option | Description
-----------------------|-----------------------------------------------------------------------------------
`rule_set` | A list of rules, each with a `violation` and `suggestion` option.
`violation` | A list of strings or regex patterns that specify unwanted text content.
`suggestion` | A suggested replacement for the unwanted text content defined in `violation`.
`case_insensitive` | A Boolean value that determines whether the rule is case sensitive. (Optional, defaults to false if not included)
`pattern_description` | A string that appears in place of the regex pattern as the violation in the error message. (Optional)
`addendum` | A string to be included at the end of every error message of the rule set. (Optional)

### FinalNewline

Files should always have a final newline. This results in better diffs when
Expand Down
2 changes: 0 additions & 2 deletions erb_lint.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ Gem::Specification.new do |s|

s.files = Dir['lib/**/*.rb']

s.add_dependency 'nokogiri'
s.add_dependency 'htmlentities'
s.add_development_dependency 'rspec'
s.add_development_dependency 'rubocop'
end
1 change: 0 additions & 1 deletion lib/erb_lint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

require 'erb_lint/linter_registry'
require 'erb_lint/linter'
require 'erb_lint/parser'
require 'erb_lint/runner'

# Load linters
Expand Down
11 changes: 9 additions & 2 deletions lib/erb_lint/linter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,16 @@ def initialize(_config)
raise NotImplementedError, "must implement ##{__method__}"
end

# The lint_file method that contains the logic for the linter and returns a list of errors.
def lint_file(file_content)
lines = file_content.scan(/[^\n]*\n|[^\n]+/)
lint_lines(lines)
end

protected

# The lint_lines method that contains the logic for the linter and returns a list of errors.
# Must be implemented by the concrete inheriting class.
def lint_file(_file_tree)
def lint_lines(_lines)
raise NotImplementedError, "must implement ##{__method__}"
end
end
Expand Down
100 changes: 0 additions & 100 deletions lib/erb_lint/linters/content_style.rb

This file was deleted.

130 changes: 118 additions & 12 deletions lib/erb_lint/linters/deprecated_classes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,33 @@ def initialize(config)
@addendum = config.fetch('addendum', '')
end

def lint_file(file_tree)
protected

def lint_lines(lines)
errors = []

elements_with_class_attr = file_tree.search('[class]')
elements_with_class_attr.each do |element|
class_names = element.attribute('class').value.split(' ')
line_number = element.attribute('class').line
class_names.each do |class_name|
errors.push(*generate_errors(element_name: element.name, class_name: class_name, line_number: line_number))
lines.each_with_index do |line, index|
start_tags = StartTagHelper.start_tags(line)
start_tags.each do |start_tag|
start_tag.attributes.select(&:class?).each do |class_attr|
class_attr.value.split(' ').each do |class_name|
errors.push(*generate_errors(class_name, index + 1))
end
end
end
end
errors
end

private

def generate_errors(element_name:, class_name:, line_number:)
def generate_errors(class_name, line_number)
violated_rules(class_name).map do |violated_rule|
message = 'Deprecated class `%s` detected matching the pattern `%s` on the surrounding `%s` element.'\
"%s #{@addendum}".strip
suggestion = " #{violated_rule[:suggestion]}".rstrip

message = "Deprecated class `%s` detected matching the pattern `%s`.%s #{@addendum}".strip
{
line: line_number,
message: format(message, class_name, violated_rule[:class_expr], element_name, suggestion)
message: format(message, class_name, violated_rule[:class_expr], suggestion)
}
end
end
Expand All @@ -57,5 +59,109 @@ def violated_rules(class_name)
end
end
end

# Provides methods and classes for finding HTML start tags and their attributes.
module StartTagHelper
# These patterns cover a superset of the W3 HTML5 specification.
# Additional cases not included in the spec include those that are still rendered by some browsers.

# Attribute Patterns
# https://www.w3.org/TR/html5/syntax.html#syntax-attributes

# attribute names must be non empty and can't contain a certain set of special characters
ATTRIBUTE_NAME_PATTERN = %r{[^\s"'>\/=]+}

ATTRIBUTE_VALUE_PATTERN = %r{
"([^"]*)" | # double-quoted value
'([^']*)' | # single-quoted value
([^\s"'=<>`]+) # unquoted non-empty value without special characters
}x

# attributes can be empty or have an attribute value
ATTRIBUTE_PATTERN = %r{
#{ATTRIBUTE_NAME_PATTERN} # attribute name
(
\s*=\s* # any whitespace around equals sign
(#{ATTRIBUTE_VALUE_PATTERN}) # attribute value
)? # attributes can be empty or have an assignemnt.
}x

# Start tag Patterns
# https://www.w3.org/TR/html5/syntax.html#syntax-start-tag

TAG_NAME_PATTERN = /[A-Za-z0-9]+/ # maybe add _ < ? etc later since it gets interpreted by some browsers

START_TAG_PATTERN = %r{
<(#{TAG_NAME_PATTERN}) # start of tag with tag name
(
(
\s+ # required whitespace between tag name and first attribute and between attributes
#{ATTRIBUTE_PATTERN} # attributes
)*
)? # having an attribute block is optional
\/?> # void or foreign elements can have a slash before tag close
}x

# Represents and provides an interface for a start tag found in the HTML.
class StartTag
attr_accessor :tag_name, :attributes

def initialize(tag_name, attributes)
@tag_name = tag_name
@attributes = attributes
end
end

# Represents and provides an interface for an attribute found in a start tag in the HTML.
class Attribute
ATTR_NAME_CLASS_PATTERN = /\Aclass\z/i # attribute names are case-insensitive
attr_accessor :attribute_name, :value

def initialize(attribute_name, value)
@attribute_name = attribute_name
@value = value
end

def class?
ATTR_NAME_CLASS_PATTERN.match(@attribute_name)
end
end

class << self
def start_tags(line)
# TODO: Implement String Scanner to track quotes before the start tag begins to ensure that it is
# not enclosed inside of a string. Alternatively this problem would be solved by using
# a 3rd party parser like Nokogiri::XML

start_tag_matching_groups = line.scan(/(#{START_TAG_PATTERN})/)
start_tag_matching_groups.map do |start_tag_matching_group|
tag_name = start_tag_matching_group[1]

# attributes_string can be nil if there is no space after the tag name (and therefore no attributes).
attributes_string = start_tag_matching_group[2] || ''

attribute_list = attributes(attributes_string)

StartTag.new(tag_name, attribute_list)
end
end

private

def attributes(attributes_string)
attributes_string.scan(/(#{ATTRIBUTE_PATTERN})/).map do |attribute_matching_group|
entire_string = attribute_matching_group[0]
value_with_equal_sign = attribute_matching_group[1] || '' # This can be nil if attribute is empty
name = entire_string.sub(value_with_equal_sign, '')

# The 3 captures [3..5] are the possibilities specified in ATTRIBUTE_VALUE_PATTERN
possible_value_formats = attribute_matching_group[3..5]
value = possible_value_formats.reduce { |a, e| a.nil? ? e : a }

Attribute.new(name, value)
end
end
end
end
end
end