lint_trappings 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/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
|