slim_lint 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|