slim_lint 0.1.0
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/bin/slim-lint +7 -0
- data/config/default.yml +38 -0
- data/lib/slim_lint/cli.rb +122 -0
- data/lib/slim_lint/configuration.rb +101 -0
- data/lib/slim_lint/configuration_loader.rb +68 -0
- data/lib/slim_lint/constants.rb +8 -0
- data/lib/slim_lint/document.rb +52 -0
- data/lib/slim_lint/engine.rb +27 -0
- data/lib/slim_lint/exceptions.rb +15 -0
- data/lib/slim_lint/file_finder.rb +69 -0
- data/lib/slim_lint/filters/inject_line_numbers.rb +35 -0
- data/lib/slim_lint/filters/sexp_converter.rb +11 -0
- data/lib/slim_lint/lint.rb +25 -0
- data/lib/slim_lint/linter/line_length.rb +19 -0
- data/lib/slim_lint/linter/redundant_div.rb +17 -0
- data/lib/slim_lint/linter/rubocop.rb +73 -0
- data/lib/slim_lint/linter/trailing_whitespace.rb +17 -0
- data/lib/slim_lint/linter.rb +49 -0
- data/lib/slim_lint/linter_registry.rb +26 -0
- data/lib/slim_lint/logger.rb +107 -0
- data/lib/slim_lint/options.rb +89 -0
- data/lib/slim_lint/rake_task.rb +107 -0
- data/lib/slim_lint/report.rb +16 -0
- data/lib/slim_lint/reporter/default_reporter.rb +39 -0
- data/lib/slim_lint/reporter/json_reporter.rb +44 -0
- data/lib/slim_lint/reporter.rb +36 -0
- data/lib/slim_lint/ruby_extract_engine.rb +43 -0
- data/lib/slim_lint/ruby_extractor.rb +91 -0
- data/lib/slim_lint/ruby_parser.rb +29 -0
- data/lib/slim_lint/runner.rb +72 -0
- data/lib/slim_lint/sexp.rb +90 -0
- data/lib/slim_lint/sexp_visitor.rb +105 -0
- data/lib/slim_lint/version.rb +4 -0
- data/lib/slim_lint.rb +40 -0
- metadata +149 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ea825ed5f8f37b9f5c0f65af4cce47ee9289227c
|
4
|
+
data.tar.gz: 31f794679bd7928f0e7990e3802319e2aeaa1764
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 60c07b39dab5f98178fa7a01318e42c97e64676810309a03932b836fe1800bb9265203da373a601aa9882d53afc5b23d15c487efcab2d0a74871d7fcbe6f15b3
|
7
|
+
data.tar.gz: 8a65468ddb8de4b2834178f5e89ce90203281678d3d1cb2c0eeb42a22ffc6d6f10ef115ad65e7ab3d237043d2730f9ed4d5969add7f465becbd0ae99b485606a
|
data/bin/slim-lint
ADDED
data/config/default.yml
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# Default application configuration that all configurations inherit from.
|
2
|
+
#
|
3
|
+
# This is an opinionated list of which hooks are valuable to run and what their
|
4
|
+
# out of the box settings should be.
|
5
|
+
|
6
|
+
# Whether to ignore frontmatter at the beginning of Slim documents for
|
7
|
+
# frameworks such as Jekyll/Middleman
|
8
|
+
skip_frontmatter: false
|
9
|
+
|
10
|
+
linters:
|
11
|
+
ExplicitDiv:
|
12
|
+
enabled: true
|
13
|
+
|
14
|
+
LineLength:
|
15
|
+
enabled: true
|
16
|
+
max: 80
|
17
|
+
|
18
|
+
RuboCop:
|
19
|
+
enabled: true
|
20
|
+
# These cops are incredibly noisy since the Ruby we extract from Slim
|
21
|
+
# templates isn't well-formatted, so we ignore them.
|
22
|
+
ignored_cops:
|
23
|
+
- Lint/BlockAlignment
|
24
|
+
- Lint/EndAlignment
|
25
|
+
- Lint/Void
|
26
|
+
- Metrics/LineLength
|
27
|
+
- Style/AlignParameters
|
28
|
+
- Style/BlockNesting
|
29
|
+
- Style/FileName
|
30
|
+
- Style/IfUnlessModifier
|
31
|
+
- Style/IndentationWidth
|
32
|
+
- Style/Next
|
33
|
+
- Style/TrailingBlankLines
|
34
|
+
- Style/TrailingWhitespace
|
35
|
+
- Style/WhileUntilModifier
|
36
|
+
|
37
|
+
TrailingWhitespace:
|
38
|
+
enabled: true
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'slim_lint/options'
|
2
|
+
|
3
|
+
require 'sysexits'
|
4
|
+
|
5
|
+
module SlimLint
|
6
|
+
# Command line application interface.
|
7
|
+
class CLI
|
8
|
+
attr_accessor :options
|
9
|
+
|
10
|
+
# @param logger [SlimLint::Logger]
|
11
|
+
def initialize(logger)
|
12
|
+
@log = logger
|
13
|
+
end
|
14
|
+
|
15
|
+
# Parses the given command-line arguments and executes appropriate logic
|
16
|
+
# based on those arguments.
|
17
|
+
#
|
18
|
+
# @param args [Array<String>] command line arguments
|
19
|
+
# @return [Fixnum] exit status returned by the application
|
20
|
+
def run(args)
|
21
|
+
options = SlimLint::Options.new.parse(args)
|
22
|
+
act_on_options(options)
|
23
|
+
rescue => ex
|
24
|
+
handle_exception(ex)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :log
|
30
|
+
|
31
|
+
def act_on_options(options)
|
32
|
+
log.color_enabled = options.fetch(:color, log.tty?)
|
33
|
+
|
34
|
+
if options[:help]
|
35
|
+
print_help(options)
|
36
|
+
Sysexits::EX_OK
|
37
|
+
elsif options[:version]
|
38
|
+
print_version
|
39
|
+
Sysexits::EX_OK
|
40
|
+
elsif options[:show_linters]
|
41
|
+
print_available_linters
|
42
|
+
Sysexits::EX_OK
|
43
|
+
elsif options[:show_reporters]
|
44
|
+
print_available_reporters
|
45
|
+
Sysexits::EX_OK
|
46
|
+
else
|
47
|
+
scan_for_lints(options)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def handle_exception(ex)
|
52
|
+
case ex
|
53
|
+
when SlimLint::Exceptions::ConfigurationError
|
54
|
+
log.error ex.message
|
55
|
+
Sysexits::EX_CONFIG
|
56
|
+
when SlimLint::Exceptions::InvalidCLIOption
|
57
|
+
log.error ex.message
|
58
|
+
log.log "Run `#{APP_NAME}` --help for usage documentation"
|
59
|
+
Sysexits::EX_USAGE
|
60
|
+
when SlimLint::Exceptions::InvalidFilePath
|
61
|
+
log.error ex.message
|
62
|
+
Sysexits::EX_NOINPUT
|
63
|
+
when SlimLint::Exceptions::NoLintersError
|
64
|
+
log.error ex.message
|
65
|
+
Sysexits::EX_NOINPUT
|
66
|
+
else
|
67
|
+
print_unexpected_exception(ex)
|
68
|
+
Sysexits::EX_SOFTWARE
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def scan_for_lints(options)
|
73
|
+
report = Runner.new.run(options)
|
74
|
+
print_report(report, options)
|
75
|
+
report.failed? ? Sysexits::EX_DATAERR : Sysexits::EX_OK
|
76
|
+
end
|
77
|
+
|
78
|
+
def print_report(report, options)
|
79
|
+
reporter = options.fetch(:reporter, Reporter::DefaultReporter).new(log, report)
|
80
|
+
reporter.report_lints
|
81
|
+
end
|
82
|
+
|
83
|
+
def print_available_linters
|
84
|
+
log.info 'Available linters:'
|
85
|
+
|
86
|
+
linter_names = LinterRegistry.linters.map do |linter|
|
87
|
+
linter.name.split('::').last
|
88
|
+
end
|
89
|
+
|
90
|
+
linter_names.sort.each do |linter_name|
|
91
|
+
log.log " - #{linter_name}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def print_available_reporters
|
96
|
+
log.info 'Available reporters:'
|
97
|
+
|
98
|
+
reporter_names = Reporter.descendants.map do |reporter|
|
99
|
+
reporter.name.split('::').last.sub(/Reporter$/, '').downcase
|
100
|
+
end
|
101
|
+
|
102
|
+
reporter_names.sort.each do |reporter_name|
|
103
|
+
log.log " - #{reporter_name}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def print_help(options)
|
108
|
+
log.log options[:help]
|
109
|
+
end
|
110
|
+
|
111
|
+
def print_version
|
112
|
+
log.log "#{APP_NAME} #{SlimLint::VERSION}"
|
113
|
+
end
|
114
|
+
|
115
|
+
def print_unexpected_exception(ex)
|
116
|
+
log.bold_error ex.message
|
117
|
+
log.error ex.backtrace.join("\n")
|
118
|
+
log.warning 'Report this bug at ', false
|
119
|
+
log.info SlimLint::BUG_REPORT_URL
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module SlimLint
|
2
|
+
# Stores runtime configuration for the application.
|
3
|
+
class Configuration
|
4
|
+
attr_reader :hash
|
5
|
+
|
6
|
+
# Creates a configuration from the given options hash.
|
7
|
+
#
|
8
|
+
# @param options [Hash]
|
9
|
+
def initialize(options)
|
10
|
+
@hash = options
|
11
|
+
validate
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param key [String]
|
15
|
+
# @return [Array,Hash,Number,String]
|
16
|
+
def [](key)
|
17
|
+
@hash[key]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Compares this configuration with another.
|
21
|
+
#
|
22
|
+
# @param other [SlimLint::Configuration]
|
23
|
+
# @return [true,false] whether the given configuration is equivalent
|
24
|
+
def ==(other)
|
25
|
+
super || @hash == other.hash
|
26
|
+
end
|
27
|
+
alias_method :eql?, :==
|
28
|
+
|
29
|
+
# Returns a non-modifiable configuration for the specified linter.
|
30
|
+
#
|
31
|
+
# @param linter [SlimLint::Linter,Class]
|
32
|
+
def for_linter(linter)
|
33
|
+
linter_name =
|
34
|
+
case linter
|
35
|
+
when Class
|
36
|
+
linter.name.split('::').last
|
37
|
+
when SlimLint::Linter
|
38
|
+
linter.name
|
39
|
+
else
|
40
|
+
linter.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
smart_merge(@hash['linters']['ALL'],
|
44
|
+
@hash['linters'].fetch(linter_name, {})).freeze
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns whether the specified linter is enabled by this configuration.
|
48
|
+
#
|
49
|
+
# @param linter [SlimLint::Linter,String]
|
50
|
+
def linter_enabled?(linter)
|
51
|
+
for_linter(linter)['enabled'] != false
|
52
|
+
end
|
53
|
+
|
54
|
+
# Merges the given configuration with this one, returning a new
|
55
|
+
# {Configuration}. The provided configuration will either add to or replace
|
56
|
+
# any options defined in this configuration.
|
57
|
+
#
|
58
|
+
# @param config [SlimLint::Configuration]
|
59
|
+
def merge(config)
|
60
|
+
self.class.new(smart_merge(@hash, config.hash))
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# Validates the configuration for any invalid options, normalizing it where
|
66
|
+
# possible.
|
67
|
+
def validate
|
68
|
+
@hash = convert_nils_to_empty_hashes(@hash)
|
69
|
+
ensure_linter_section_exists(@hash)
|
70
|
+
end
|
71
|
+
|
72
|
+
def smart_merge(parent, child)
|
73
|
+
parent.merge(child) do |_key, old, new|
|
74
|
+
case old
|
75
|
+
when Hash
|
76
|
+
smart_merge(old, new)
|
77
|
+
else
|
78
|
+
new
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def ensure_linter_section_exists(hash)
|
84
|
+
hash['linters'] ||= {}
|
85
|
+
hash['linters']['ALL'] ||= {}
|
86
|
+
end
|
87
|
+
|
88
|
+
def convert_nils_to_empty_hashes(hash)
|
89
|
+
hash.each_with_object({}) do |(key, value), h|
|
90
|
+
h[key] =
|
91
|
+
case value
|
92
|
+
when nil then {}
|
93
|
+
when Hash then convert_nils_to_empty_hashes(value)
|
94
|
+
else
|
95
|
+
value
|
96
|
+
end
|
97
|
+
h
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module SlimLint
|
5
|
+
# Manages configuration file loading.
|
6
|
+
class ConfigurationLoader
|
7
|
+
DEFAULT_CONFIG_PATH = File.join(SlimLint::HOME, 'config', 'default.yml')
|
8
|
+
CONFIG_FILE_NAME = '.slim-lint.yml'
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def load_applicable_config
|
12
|
+
directory = File.expand_path(Dir.pwd)
|
13
|
+
config_file = possible_config_files(directory).find(&:file?)
|
14
|
+
|
15
|
+
if config_file
|
16
|
+
load_file(config_file.to_path)
|
17
|
+
else
|
18
|
+
default_configuration
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def default_configuration
|
23
|
+
@default_config ||= load_from_file(DEFAULT_CONFIG_PATH)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Loads a configuration, ensuring it extends the default configuration.
|
27
|
+
def load_file(file)
|
28
|
+
config = load_from_file(file)
|
29
|
+
|
30
|
+
default_configuration.merge(config)
|
31
|
+
rescue => error
|
32
|
+
raise SlimLint::Exceptions::ConfigurationError,
|
33
|
+
"Unable to load configuration from '#{file}': #{error}",
|
34
|
+
error.backtrace
|
35
|
+
end
|
36
|
+
|
37
|
+
def load_hash(hash)
|
38
|
+
config = SlimLint::Configuration.new(hash)
|
39
|
+
|
40
|
+
default_configuration.merge(config)
|
41
|
+
rescue => error
|
42
|
+
raise SlimLint::Exceptions::ConfigurationError,
|
43
|
+
"Unable to load configuration from '#{file}': #{error}",
|
44
|
+
error.backtrace
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def load_from_file(file)
|
50
|
+
hash =
|
51
|
+
if yaml = YAML.load_file(file)
|
52
|
+
yaml.to_hash
|
53
|
+
else
|
54
|
+
{}
|
55
|
+
end
|
56
|
+
|
57
|
+
SlimLint::Configuration.new(hash)
|
58
|
+
end
|
59
|
+
|
60
|
+
def possible_config_files(directory)
|
61
|
+
files = Pathname.new(directory)
|
62
|
+
.enum_for(:ascend)
|
63
|
+
.map { |path| path + CONFIG_FILE_NAME }
|
64
|
+
files << Pathname.new(CONFIG_FILE_NAME)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module SlimLint
|
2
|
+
# Represents a parsed Slim document and its associated metadata.
|
3
|
+
class Document
|
4
|
+
attr_reader :config, :file, :sexp, :source, :source_lines
|
5
|
+
|
6
|
+
# Parses the specified Slim code into a {Document}.
|
7
|
+
#
|
8
|
+
# @param source [String] Slim code to parse
|
9
|
+
# @param options [Hash]
|
10
|
+
# @option file [String] file name of document that was parsed
|
11
|
+
# @raise [Slim::Parser::Error] if there was a problem parsing the document
|
12
|
+
def initialize(source, options)
|
13
|
+
@config = options[:config]
|
14
|
+
@file = options.fetch(:file, '(string)')
|
15
|
+
|
16
|
+
process_source(source)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# @param source [String] Slim code to parse
|
22
|
+
# @raise [Slim::Parser::Error] if there was a problem parsing the document
|
23
|
+
def process_source(source)
|
24
|
+
@source = strip_frontmatter(source)
|
25
|
+
@source_lines = @source.split("\n")
|
26
|
+
|
27
|
+
@engine = SlimLint::Engine.new(file: @file)
|
28
|
+
@sexp = @engine.call(source)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Removes YAML frontmatter
|
32
|
+
def strip_frontmatter(source)
|
33
|
+
if config['skip_frontmatter'] &&
|
34
|
+
source =~ /
|
35
|
+
# From the start of the string
|
36
|
+
\A
|
37
|
+
# First-capture match --- followed by optional whitespace up
|
38
|
+
# to a newline then 0 or more chars followed by an optional newline.
|
39
|
+
# This matches the --- and the contents of the frontmatter
|
40
|
+
(---\s*\n.*?\n?)
|
41
|
+
# From the start of the line
|
42
|
+
^
|
43
|
+
# Second capture match --- or ... followed by optional whitespace
|
44
|
+
# and newline. This matches the closing --- for the frontmatter.
|
45
|
+
(---|\.\.\.)\s*$\n?/mx
|
46
|
+
source = $POSTMATCH
|
47
|
+
end
|
48
|
+
|
49
|
+
source
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module SlimLint
|
2
|
+
# Temple engine used to generate a {Sexp} parse tree for use by linters.
|
3
|
+
#
|
4
|
+
# We omit a lot of the filters that are in {Slim::Engine} because they result
|
5
|
+
# in information potentially being removed from the parse tree (since some
|
6
|
+
# Sexp abstractions are optimized/removed or otherwise transformed). In order
|
7
|
+
# for linters to be useful, they need to operate on the original parse tree.
|
8
|
+
#
|
9
|
+
# The other key task this engine accomplishes is converting the Array-based
|
10
|
+
# S-expressions into {SlimLint::Sexp} objects, which have a number of helper
|
11
|
+
# methods that makes working with them easier. It also annotates these
|
12
|
+
# {SlimLint::Sexp} objects with line numbers so it's easy to cross reference
|
13
|
+
# with the original source code.
|
14
|
+
class Engine < Temple::Engine
|
15
|
+
filter :Encoding
|
16
|
+
filter :RemoveBOM
|
17
|
+
|
18
|
+
# Parse into S-expression using Slim parser
|
19
|
+
use Slim::Parser
|
20
|
+
|
21
|
+
# Converts Array-based S-expressions into SlimLint::Sexp objects
|
22
|
+
use SlimLint::Filters::SexpConverter
|
23
|
+
|
24
|
+
# Annotates Sexps with line numbers
|
25
|
+
use SlimLint::Filters::InjectLineNumbers
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Collection of exceptions that can be raised by the HAML Lint application.
|
2
|
+
module SlimLint::Exceptions
|
3
|
+
# Raised when a {Configuration} could not be loaded from a file.
|
4
|
+
class ConfigurationError < StandardError; end
|
5
|
+
|
6
|
+
# Raised when invalid/incompatible command line options are provided.
|
7
|
+
class InvalidCLIOption < StandardError; end
|
8
|
+
|
9
|
+
# Raised when an invalid file path is specified
|
10
|
+
class InvalidFilePath < StandardError; end
|
11
|
+
|
12
|
+
# Raised when attempting to execute `Runner` with options that would result in
|
13
|
+
# no linters being enabled.
|
14
|
+
class NoLintersError < StandardError; end
|
15
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'find'
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Finds Slim files that should be linted given a specified list of paths, glob
|
5
|
+
# patterns, and configuration.
|
6
|
+
class FileFinder
|
7
|
+
# List of extensions of files to include under a directory when a directory
|
8
|
+
# is specified instead of a file.
|
9
|
+
VALID_EXTENSIONS = %w[.slim]
|
10
|
+
|
11
|
+
# @param config [SlimLint::Configuration]
|
12
|
+
def initialize(config)
|
13
|
+
@config = config
|
14
|
+
end
|
15
|
+
|
16
|
+
# Return list of files to lint given the specified set of paths and glob
|
17
|
+
# patterns.
|
18
|
+
# @param patterns [Array<String>]
|
19
|
+
# @param excluded_patterns [Array<String>]
|
20
|
+
# @raise [SlimLint::Exceptions::InvalidFilePath]
|
21
|
+
# @return [Array<String>] list of actual files
|
22
|
+
def find(patterns, excluded_patterns)
|
23
|
+
extract_files_from(patterns).reject do |file|
|
24
|
+
excluded_patterns.any? do |exclusion_glob|
|
25
|
+
::File.fnmatch?(exclusion_glob, file,
|
26
|
+
::File::FNM_PATHNAME | # Wildcards don't match path separators
|
27
|
+
::File::FNM_DOTMATCH) # `*` wildcard matches dotfiles
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def extract_files_from(patterns) # rubocop:disable MethodLength
|
35
|
+
files = []
|
36
|
+
|
37
|
+
patterns.each do |pattern|
|
38
|
+
if File.file?(pattern)
|
39
|
+
files << pattern
|
40
|
+
else
|
41
|
+
begin
|
42
|
+
::Find.find(pattern) do |file|
|
43
|
+
files << file if slim_file?(file)
|
44
|
+
end
|
45
|
+
rescue ::Errno::ENOENT
|
46
|
+
# File didn't exist; it might be a file glob pattern
|
47
|
+
matches = ::Dir.glob(pattern)
|
48
|
+
if matches.any?
|
49
|
+
files += matches
|
50
|
+
else
|
51
|
+
# One of the paths specified does not exist; raise a more
|
52
|
+
# descriptive exception so we know which one
|
53
|
+
raise SlimLint::Exceptions::InvalidFilePath,
|
54
|
+
"File path '#{pattern}' does not exist"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
files.uniq
|
61
|
+
end
|
62
|
+
|
63
|
+
def slim_file?(file)
|
64
|
+
return false unless ::FileTest.file?(file)
|
65
|
+
|
66
|
+
VALID_EXTENSIONS.include?(::File.extname(file))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module SlimLint::Filters
|
2
|
+
# Traverses a Temple S-expression (that has already been converted to
|
3
|
+
# {SlimLint::Sexp} instances) and annotates them with line numbers.
|
4
|
+
#
|
5
|
+
# This is a hack that allows us to access line information directly from the
|
6
|
+
# S-expressions, which makes a lot of other tasks easier.
|
7
|
+
class InjectLineNumbers < Temple::Filter
|
8
|
+
NEWLINE_SEXP = [:newline]
|
9
|
+
|
10
|
+
def call(sexp)
|
11
|
+
@line_number = 1
|
12
|
+
traverse(sexp)
|
13
|
+
sexp
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Traverses an {Sexp}, annotating it with line numbers by searching for
|
19
|
+
# :newline abstractions within it.
|
20
|
+
#
|
21
|
+
# @param sexp [SlimLint::Sexp]
|
22
|
+
def traverse(sexp)
|
23
|
+
sexp.line = @line_number
|
24
|
+
|
25
|
+
if sexp == NEWLINE_SEXP
|
26
|
+
@line_number += 1
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
sexp.each do |nested_sexp|
|
31
|
+
traverse(nested_sexp) if nested_sexp.is_a?(SlimLint::Sexp)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module SlimLint::Filters
|
2
|
+
# Converts a Temple S-expression comprised of {Array}s into {SlimLint::Sexp}s.
|
3
|
+
#
|
4
|
+
# These {SlimLint::Sexp}s include additional helpers that makes working with
|
5
|
+
# them more pleasant.
|
6
|
+
class SexpConverter < Temple::Filter
|
7
|
+
def call(array_sexp)
|
8
|
+
SlimLint::Sexp.new(array_sexp)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SlimLint
|
2
|
+
# Contains information about a problem or issue with a HAML document.
|
3
|
+
class Lint
|
4
|
+
attr_reader :filename, :line, :linter, :message, :severity
|
5
|
+
|
6
|
+
# Creates a new lint.
|
7
|
+
#
|
8
|
+
# @param linter [SlimLint::Linter]
|
9
|
+
# @param filename [String]
|
10
|
+
# @param line [Fixnum]
|
11
|
+
# @param message [String]
|
12
|
+
# @param severity [Symbol]
|
13
|
+
def initialize(linter, filename, line, message, severity = :warning)
|
14
|
+
@linter = linter
|
15
|
+
@filename = filename
|
16
|
+
@line = line || 0
|
17
|
+
@message = message
|
18
|
+
@severity = severity
|
19
|
+
end
|
20
|
+
|
21
|
+
def error?
|
22
|
+
@severity == :error
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module SlimLint
|
2
|
+
# Checks for lines longer than a maximum number of columns.
|
3
|
+
class Linter::LineLength < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
MSG = 'Line is too long. [%d/%d]'
|
7
|
+
|
8
|
+
on_start do |_sexp|
|
9
|
+
max_length = config['max']
|
10
|
+
dummy_node = Struct.new(:line)
|
11
|
+
|
12
|
+
document.source_lines.each_with_index do |line, index|
|
13
|
+
next if line.length <= max_length
|
14
|
+
|
15
|
+
report_lint(dummy_node.new(index + 1), format(MSG, line.length, max_length))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module SlimLint
|
2
|
+
# Checks for unnecessary uses of the `div` tag where a class name or ID
|
3
|
+
# already implies a div.
|
4
|
+
class Linter::RedundantDiv < Linter
|
5
|
+
include LinterRegistry
|
6
|
+
|
7
|
+
MESSAGE = '`div` is redundant when %s attribute shortcut is present'
|
8
|
+
|
9
|
+
on [:html, :tag, 'div', [:html, :attrs, [:html, :attr, 'class', [:static]]]] do |sexp|
|
10
|
+
report_lint(sexp, MESSAGE % 'class')
|
11
|
+
end
|
12
|
+
|
13
|
+
on [:html, :tag, 'div', [:html, :attrs, [:html, :attr, 'id', [:static]]]] do |sexp|
|
14
|
+
report_lint(sexp, MESSAGE % 'id')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|