lint_trappings 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/LICENSE.md +21 -0
- data/lib/lint_trappings.rb +17 -0
- data/lib/lint_trappings/application.rb +138 -0
- data/lib/lint_trappings/arguments_parser.rb +145 -0
- data/lib/lint_trappings/cli.rb +61 -0
- data/lib/lint_trappings/command/base.rb +36 -0
- data/lib/lint_trappings/command/display_documentation.rb +65 -0
- data/lib/lint_trappings/command/display_formatters.rb +14 -0
- data/lib/lint_trappings/command/display_help.rb +8 -0
- data/lib/lint_trappings/command/display_linters.rb +24 -0
- data/lib/lint_trappings/command/display_version.rb +14 -0
- data/lib/lint_trappings/command/scan.rb +19 -0
- data/lib/lint_trappings/configuration.rb +94 -0
- data/lib/lint_trappings/configuration_loader.rb +98 -0
- data/lib/lint_trappings/configuration_resolver.rb +49 -0
- data/lib/lint_trappings/document.rb +45 -0
- data/lib/lint_trappings/errors.rb +127 -0
- data/lib/lint_trappings/executable.rb +26 -0
- data/lib/lint_trappings/file_finder.rb +171 -0
- data/lib/lint_trappings/formatter/base.rb +67 -0
- data/lib/lint_trappings/formatter/checkstyle.rb +34 -0
- data/lib/lint_trappings/formatter/default.rb +99 -0
- data/lib/lint_trappings/formatter/json.rb +62 -0
- data/lib/lint_trappings/formatter_forwarder.rb +23 -0
- data/lib/lint_trappings/formatter_loader.rb +45 -0
- data/lib/lint_trappings/lint.rb +37 -0
- data/lib/lint_trappings/linter.rb +182 -0
- data/lib/lint_trappings/linter_configuration_validator.rb +42 -0
- data/lib/lint_trappings/linter_loader.rb +44 -0
- data/lib/lint_trappings/linter_plugin.rb +35 -0
- data/lib/lint_trappings/linter_selector.rb +120 -0
- data/lib/lint_trappings/location.rb +39 -0
- data/lib/lint_trappings/output.rb +118 -0
- data/lib/lint_trappings/preprocessor.rb +41 -0
- data/lib/lint_trappings/rake_task.rb +145 -0
- data/lib/lint_trappings/report.rb +58 -0
- data/lib/lint_trappings/runner.rb +161 -0
- data/lib/lint_trappings/spec.rb +12 -0
- data/lib/lint_trappings/spec/directory_helpers.rb +22 -0
- data/lib/lint_trappings/spec/indentation_helpers.rb +7 -0
- data/lib/lint_trappings/spec/matchers/report_lint_matcher.rb +169 -0
- data/lib/lint_trappings/spec/shared_contexts/linter_shared_context.rb +35 -0
- data/lib/lint_trappings/utils.rb +123 -0
- data/lib/lint_trappings/version.rb +4 -0
- metadata +117 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
module LintTrappings::Command
|
2
|
+
# Displays all available formatters.
|
3
|
+
class DisplayFormatters < Base
|
4
|
+
def run
|
5
|
+
formatter_names = LintTrappings::Formatter::Base.descendants.map do |formatter_class|
|
6
|
+
formatter_class.name.split('::').last.sub(/Formatter$/, '').downcase
|
7
|
+
end
|
8
|
+
|
9
|
+
formatter_names.sort.each do |formatter_name|
|
10
|
+
output.puts " - #{formatter_name}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module LintTrappings::Command
|
4
|
+
# Displays all available linters and whether or not they are enabled.
|
5
|
+
class DisplayLinters < Base
|
6
|
+
def run
|
7
|
+
LintTrappings::LinterLoader.new(application, config).load(options)
|
8
|
+
|
9
|
+
linter_selector = LintTrappings::LinterSelector.new(config, options)
|
10
|
+
all_linter_names = linter_selector.all_linter_classes.map(&:canonical_name)
|
11
|
+
enabled_linter_names = linter_selector.enabled_linter_classes.map(&:canonical_name).to_set
|
12
|
+
|
13
|
+
all_linter_names.sort.each do |linter_name|
|
14
|
+
output.print(' - ')
|
15
|
+
output.bold(linter_name, false)
|
16
|
+
if enabled_linter_names.include?(linter_name)
|
17
|
+
output.success(' enabled')
|
18
|
+
else
|
19
|
+
output.error(' disabled')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module LintTrappings::Command
|
2
|
+
# Outputs application version information.
|
3
|
+
class DisplayVersion < Base
|
4
|
+
def run
|
5
|
+
output.bold(application.executable_name, false)
|
6
|
+
output.info(application.version)
|
7
|
+
|
8
|
+
if options[:verbose]
|
9
|
+
output.bold('Ruby version: ', false)
|
10
|
+
output.info(RUBY_VERSION)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module LintTrappings::Command
|
2
|
+
# Scan for lints, outputting report of results using the specified formatter.
|
3
|
+
class Scan < Base
|
4
|
+
def run
|
5
|
+
LintTrappings::LinterLoader.new(application, config).load(options)
|
6
|
+
|
7
|
+
runner = LintTrappings::Runner.new(application, config, output)
|
8
|
+
report = runner.run(options)
|
9
|
+
|
10
|
+
if report.failures?
|
11
|
+
raise LintTrappings::ScanFailed,
|
12
|
+
'High severity lints were reported!'
|
13
|
+
elsif report.warnings?
|
14
|
+
raise LintTrappings::ScanWarned,
|
15
|
+
'Warnings were reported.'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module LintTrappings
|
4
|
+
# Stores runtime configuration for the application.
|
5
|
+
class Configuration
|
6
|
+
# The path of this configuration, if it was loaded from a file.
|
7
|
+
#
|
8
|
+
# Used in certain scenarios to determine the absolute path of a file
|
9
|
+
# specified in a configuration (i.e. getting the path relative to the
|
10
|
+
# location of the configuration file itself).
|
11
|
+
#
|
12
|
+
# @return [String]
|
13
|
+
attr_accessor :path
|
14
|
+
|
15
|
+
# Creates a configuration from the given options hash.
|
16
|
+
#
|
17
|
+
# @param options [Hash]
|
18
|
+
def initialize(options = {})
|
19
|
+
@hash = options.dup
|
20
|
+
end
|
21
|
+
|
22
|
+
# Compares this configuration with another.
|
23
|
+
#
|
24
|
+
# @param other [LintTrappings::Configuration]
|
25
|
+
#
|
26
|
+
# @return [true,false] whether the given configuration is equivalent
|
27
|
+
def ==(other)
|
28
|
+
super || @hash == other.hash
|
29
|
+
end
|
30
|
+
|
31
|
+
def fetch(*args, &block)
|
32
|
+
@hash.fetch(*args, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
def [](key)
|
36
|
+
@hash[key]
|
37
|
+
end
|
38
|
+
|
39
|
+
def delete(key)
|
40
|
+
@hash.delete(key)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Merges the given configuration with this one.
|
44
|
+
#
|
45
|
+
# The provided configuration will either add to or replace any options
|
46
|
+
# defined in this configuration.
|
47
|
+
#
|
48
|
+
# @param config [LintTrappings::Configuration]
|
49
|
+
#
|
50
|
+
# @return [LintTrappings::Configuration]
|
51
|
+
def merge(config)
|
52
|
+
merged_hash = smart_merge(@hash, config.hash)
|
53
|
+
self.class.new(merged_hash)
|
54
|
+
end
|
55
|
+
|
56
|
+
def for_linter(linterish)
|
57
|
+
linter_name =
|
58
|
+
case linterish
|
59
|
+
when Class, LintTrappings::Linter
|
60
|
+
linterish.canonical_name
|
61
|
+
else
|
62
|
+
linterish.to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
conf = @hash.fetch('linters', {}).fetch(linter_name, {}).dup
|
66
|
+
conf['severity'] ||= @hash.fetch('default_severity', :error)
|
67
|
+
conf['severity'] = conf['severity'].to_sym
|
68
|
+
conf
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
# Internal hash storing the configuration.
|
74
|
+
attr_reader :hash
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# Merge two hashes such that nested hashes are merged rather than replaced.
|
79
|
+
#
|
80
|
+
# @param parent [Hash]
|
81
|
+
# @param child [Hash]
|
82
|
+
# @return [Hash]
|
83
|
+
def smart_merge(parent, child)
|
84
|
+
parent.merge(child) do |_key, old, new|
|
85
|
+
case old
|
86
|
+
when Hash
|
87
|
+
smart_merge(old, new)
|
88
|
+
else
|
89
|
+
new
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module LintTrappings
|
5
|
+
# Manages configuration file loading.
|
6
|
+
class ConfigurationLoader
|
7
|
+
def initialize(application)
|
8
|
+
@application = application
|
9
|
+
end
|
10
|
+
|
11
|
+
# Load configuration file given the current working directory the
|
12
|
+
# application is running within.
|
13
|
+
#
|
14
|
+
# @param options [Hash]
|
15
|
+
# @option options :working_directory [String] directory to start searching
|
16
|
+
# from
|
17
|
+
#
|
18
|
+
# @raise [NoConfigurationFileError] if no configuration file was found
|
19
|
+
#
|
20
|
+
# @return [LintTrappings::Configuration]
|
21
|
+
def load(options = {})
|
22
|
+
config_file_names = @application.configuration_file_names
|
23
|
+
working_directory = options.fetch(:working_directory, Dir.pwd)
|
24
|
+
|
25
|
+
directory = File.expand_path(working_directory)
|
26
|
+
config_file = possible_config_files(config_file_names, directory).find(&:file?)
|
27
|
+
|
28
|
+
if config_file
|
29
|
+
load_file(config_file.to_path)
|
30
|
+
else
|
31
|
+
raise NoConfigurationFileError,
|
32
|
+
"No configuration file #{config_file_names.join('/')} found in " \
|
33
|
+
'working directory or any parent directory'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Loads a configuration, ensuring it extends the base configuration.
|
38
|
+
#
|
39
|
+
# @param path [String]
|
40
|
+
#
|
41
|
+
# @raise [LintTrappings::ConfigurationParseError] YAML file could not be parsed
|
42
|
+
# @raise [LintTrappings::NoConfigurationFileError] specified file not found
|
43
|
+
#
|
44
|
+
# @return [LintTrappings::Configuration]
|
45
|
+
def load_file(path)
|
46
|
+
load_from_file(path)
|
47
|
+
rescue ::Psych::SyntaxError => error
|
48
|
+
raise ConfigurationParseError,
|
49
|
+
"Unable to parse configuration from '#{file}': #{error}",
|
50
|
+
error.backtrace
|
51
|
+
rescue Errno::ENOENT => error
|
52
|
+
raise NoConfigurationFileError,
|
53
|
+
"Unable to load configuration from '#{file}': #{error}"
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def configuration_class
|
59
|
+
@application.base_configuration.class
|
60
|
+
end
|
61
|
+
|
62
|
+
# Parses and loads a configuration from the given file.
|
63
|
+
#
|
64
|
+
# @param path [String]
|
65
|
+
#
|
66
|
+
# @return [LintTrappings::Configuration]
|
67
|
+
def load_from_file(path)
|
68
|
+
hash =
|
69
|
+
if yaml = YAML.load_file(path)
|
70
|
+
yaml.to_hash
|
71
|
+
else
|
72
|
+
{}
|
73
|
+
end
|
74
|
+
|
75
|
+
configuration_class.new(hash).tap do |config|
|
76
|
+
config.path = path
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns an enumerator for the possible configuration file paths given
|
81
|
+
# the context of the specified working directory.
|
82
|
+
#
|
83
|
+
# @param config_file_name [String]
|
84
|
+
# @param directory [String]
|
85
|
+
#
|
86
|
+
# @return [Array<Pathname>]
|
87
|
+
def possible_config_files(config_file_names, directory)
|
88
|
+
Enumerator.new do |y|
|
89
|
+
Pathname.new(directory)
|
90
|
+
.enum_for(:ascend)
|
91
|
+
.each do |path|
|
92
|
+
config_file_names.each { |config_name| y << (path + config_name) }
|
93
|
+
end
|
94
|
+
config_file_names.each { |config_name| y << Pathname.new(config_name) }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'lint_trappings/linter_plugin'
|
2
|
+
|
3
|
+
module LintTrappings
|
4
|
+
# Resolves a configuration to its final representation.
|
5
|
+
#
|
6
|
+
# This does the dirty work of loading and merging a configuration with the
|
7
|
+
# configurations it extends via the `extends` option or `linter_gems` option.
|
8
|
+
class ConfigurationResolver
|
9
|
+
# @param loader [LintTrappings::ConfigurationLoader]
|
10
|
+
def initialize(loader)
|
11
|
+
@loader = loader
|
12
|
+
end
|
13
|
+
|
14
|
+
# Resolves the given configuration, returning a configuration with all
|
15
|
+
# external configuration files merged into one {Configuration}.
|
16
|
+
#
|
17
|
+
# @param conf [LintTrappings::Configuration]
|
18
|
+
def resolve(conf, options)
|
19
|
+
configs_to_extend = Array(conf.delete('extends')).map do |extend_path|
|
20
|
+
# If the path is relative, expand it relative to the path of this config
|
21
|
+
config_path = File.expand_path(extend_path, conf.path)
|
22
|
+
|
23
|
+
# Recursively resolve this configuration (it may have `extends` of its own)
|
24
|
+
resolve(@loader.load_file(config_path))
|
25
|
+
end
|
26
|
+
|
27
|
+
# Load any configurations included by plugins
|
28
|
+
require_paths = Array(conf.delete('linter_plugins')) + options.fetch(:linter_plugins, [])
|
29
|
+
configs_to_extend += require_paths.map do |require_path|
|
30
|
+
plugin = LinterPlugin.new(require_path)
|
31
|
+
plugin.load
|
32
|
+
resolve(@loader.load_file(plugin.config_file_path))
|
33
|
+
end
|
34
|
+
|
35
|
+
conf = extend_configs(configs_to_extend, conf) if configs_to_extend.any?
|
36
|
+
conf
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Extend the given configurations with the specified config, merging into a
|
42
|
+
# single config.
|
43
|
+
def extend_configs(configs_to_extend, config)
|
44
|
+
configs_to_extend[1..-1].inject(configs_to_extend.first) do |merged, config_to_extend|
|
45
|
+
merged.merge(config_to_extend)
|
46
|
+
end.merge(config)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module LintTrappings
|
2
|
+
# Represents a parsed document and its associated metadata.
|
3
|
+
#
|
4
|
+
# Implementors should implement the {#process_source} method.
|
5
|
+
#
|
6
|
+
# @abstract
|
7
|
+
class Document
|
8
|
+
# @return [LintTrappings::Configuration] configuration used to parse template
|
9
|
+
attr_reader :config
|
10
|
+
|
11
|
+
# @return [String, nil] path of the file that was parsed, or nil if it was
|
12
|
+
# parsed directory from a string
|
13
|
+
attr_reader :path
|
14
|
+
|
15
|
+
# @return [String] original source code
|
16
|
+
attr_reader :source
|
17
|
+
|
18
|
+
# @return [Array<String>] original source code as an array of lines
|
19
|
+
attr_reader :source_lines
|
20
|
+
|
21
|
+
# Parses the specified Slim code into a {Document}.
|
22
|
+
#
|
23
|
+
# @param source [String] Source code to parse
|
24
|
+
# @param config [LintTrappings::Configuration]
|
25
|
+
# @param options [Hash]
|
26
|
+
# @option options :path [String] path of document that was parsed
|
27
|
+
def initialize(source, config, options = {})
|
28
|
+
@config = config
|
29
|
+
@path = options[:path]
|
30
|
+
@source = source
|
31
|
+
@source_lines = @source.split("\n")
|
32
|
+
|
33
|
+
process_source(source)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Processes the source code of the document, initializing the document.
|
39
|
+
#
|
40
|
+
# @raise [LintTrappings::ParseError] if there was a problem parsing the document
|
41
|
+
def process_source(_source)
|
42
|
+
raise NotImplementedError, 'Must implement #process_source'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# Collection of errors that can be raised by the framework.
|
2
|
+
module LintTrappings
|
3
|
+
# Abstract error. Separates LintTrappings errors from other kinds of
|
4
|
+
# errors in the exception hierarchy.
|
5
|
+
#
|
6
|
+
# @abstract
|
7
|
+
class LintTrappingsError < StandardError
|
8
|
+
# Returns the status code that should be output if this error goes
|
9
|
+
# unhandled.
|
10
|
+
#
|
11
|
+
# Ideally these should resemble exit codes from the sysexits documentation
|
12
|
+
# where it makes sense.
|
13
|
+
def self.exit_status(*args)
|
14
|
+
if args.any?
|
15
|
+
@exit_status = args.first
|
16
|
+
else
|
17
|
+
if @exit_status
|
18
|
+
@exit_status
|
19
|
+
else
|
20
|
+
ancestors[1..-1].each do |ancestor|
|
21
|
+
return 70 if ancestor == LintTrappingsError # No exit status defined
|
22
|
+
return ancestor.exit_status if ancestor.exit_status
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def exit_status
|
29
|
+
self.class.exit_status
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Superclass of all configuration-related errors.
|
34
|
+
# @abstract
|
35
|
+
class ConfigurationError < LintTrappingsError
|
36
|
+
exit_status 78 # EX_CONFIG
|
37
|
+
end
|
38
|
+
|
39
|
+
# Raised when a LintTrappings::Application subclass does not set a value for a
|
40
|
+
# required configuration attribute.
|
41
|
+
class ApplicationConfigurationError < ConfigurationError; end
|
42
|
+
|
43
|
+
# Raised when a configuration file could not be parsed.
|
44
|
+
class ConfigurationParseError < ConfigurationError; end
|
45
|
+
|
46
|
+
# Raised when configuration file was not found.
|
47
|
+
class NoConfigurationFileError < ConfigurationError; end
|
48
|
+
|
49
|
+
# Raised when a linter's configuration does not match its specification.
|
50
|
+
class LinterConfigurationError < ConfigurationError; end
|
51
|
+
|
52
|
+
# Superclass of all usage-related errors
|
53
|
+
# @abstract
|
54
|
+
class UsageError < LintTrappingsError
|
55
|
+
exit_status 64 # EX_USAGE
|
56
|
+
end
|
57
|
+
|
58
|
+
# Raised when there was a problem loading a formatter.
|
59
|
+
class FormatterLoadError < UsageError; end
|
60
|
+
|
61
|
+
# Raised when invalid/incompatible command line options are specified.
|
62
|
+
class InvalidCliOptionError < UsageError; end
|
63
|
+
|
64
|
+
# Raised when invalid command was specified.
|
65
|
+
class InvalidCommandError < UsageError; end
|
66
|
+
|
67
|
+
# Raised when an invalid file path is specified.
|
68
|
+
class InvalidFilePathError < UsageError; end
|
69
|
+
|
70
|
+
# Raised when an invalid file glob pattern is specified.
|
71
|
+
class InvalidFilePatternError < UsageError; end
|
72
|
+
|
73
|
+
# Raised when an invalid option specification is specified for a linter.
|
74
|
+
class InvalidOptionSpecificationError < ConfigurationError; end
|
75
|
+
|
76
|
+
# Raised when a linter raises an unexpected error.
|
77
|
+
class LinterError < LintTrappingsError; end
|
78
|
+
|
79
|
+
# Raised when an error occurs loading a linter file.
|
80
|
+
class LinterLoadError < ConfigurationError; end
|
81
|
+
|
82
|
+
# Raised when an external preprocessor command returns a non-zero exit status.
|
83
|
+
class PreprocessorError < LintTrappingsError
|
84
|
+
exit_status 84
|
85
|
+
end
|
86
|
+
|
87
|
+
# Raised when a report contains lints which qualify as warnings, but does not
|
88
|
+
# contain failures.
|
89
|
+
class ScanWarned < LintTrappingsError
|
90
|
+
exit_status 0 # Don't fail for warnings
|
91
|
+
end
|
92
|
+
|
93
|
+
# Raised when a report contains lints which qualify as failures.
|
94
|
+
class ScanFailed < LintTrappingsError
|
95
|
+
exit_status 2
|
96
|
+
end
|
97
|
+
|
98
|
+
# Raised when running with options that would result in no linters being
|
99
|
+
# enabled.
|
100
|
+
class NoLintersError < ConfigurationError; end
|
101
|
+
|
102
|
+
# Raised when there was a problem parsing a document.
|
103
|
+
class ParseError < LintTrappingsError
|
104
|
+
# @return [String] path to the file that failed to parse
|
105
|
+
attr_reader :path
|
106
|
+
|
107
|
+
# @return [Range<LintTrappings::Location>] source range of the parse error
|
108
|
+
attr_reader :source_range
|
109
|
+
|
110
|
+
def initialize(options)
|
111
|
+
@message = options[:message]
|
112
|
+
@path = options[:path]
|
113
|
+
|
114
|
+
if @source_range = options[:source_range]
|
115
|
+
@line = @source_range.begin.line
|
116
|
+
@column = @source_range.begin.column
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def message
|
121
|
+
msg = @message
|
122
|
+
msg << " on line #{@line}" if @line
|
123
|
+
msg << ", column #{@column}" if @column
|
124
|
+
msg
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|