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,26 @@
|
|
1
|
+
require 'lint_trappings/cli'
|
2
|
+
|
3
|
+
# Helper module for creating a LintTrappings-powered executable.
|
4
|
+
module LintTrappings::Executable
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# Runs the command line interface.
|
8
|
+
#
|
9
|
+
# This should be called from your application executable, like so:
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# #!/usr/bin/env ruby
|
13
|
+
# require 'my_linter_gem'
|
14
|
+
# require 'lint_trappings/executable'
|
15
|
+
# LintTrappings::Executable.run(MyLinter::Application, STDOUT, STDERR, ARGV)
|
16
|
+
#
|
17
|
+
# @param application_class [Class]
|
18
|
+
# @param stdout [IO] output stream
|
19
|
+
# @param arguments [Array<String>] command line arguments
|
20
|
+
def run(application_class, stdout, stderr, arguments)
|
21
|
+
output = LintTrappings::Output.new(stdout)
|
22
|
+
error_output = LintTrappings::Output.new(stderr)
|
23
|
+
application = application_class.new(output)
|
24
|
+
exit LintTrappings::Cli.new(application, error_output).run(arguments)
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'find'
|
2
|
+
|
3
|
+
module LintTrappings
|
4
|
+
# Finds files that should be linted.
|
5
|
+
module FileFinder
|
6
|
+
class << self
|
7
|
+
# Return list of lintable files given the specified set of paths and glob
|
8
|
+
# pattern search criteria.
|
9
|
+
#
|
10
|
+
# The distinction between paths and globs is so files with a `*` in their
|
11
|
+
# name can still be matched if necessary without treating them as a glob
|
12
|
+
# pattern.
|
13
|
+
#
|
14
|
+
# @param options [Hash]
|
15
|
+
# @option options :included_paths [Array<String>] files/directories to include
|
16
|
+
# @option options :excluded_paths [Array<String>] files/directories to exclude
|
17
|
+
# @option options :included_patterns [Array<String>] glob patterns to include
|
18
|
+
# @option options :excluded_patterns [Array<String>] glob patterns to exclude
|
19
|
+
# @option options :file_extensions [Array<String>] extensions of files to
|
20
|
+
# include when searching directories specified by included_paths
|
21
|
+
#
|
22
|
+
# @raise [InvalidFilePathError] if included_paths/excluded_paths don't exist
|
23
|
+
# @raise [InvalidFilePatternError] if any included_pattern doesn't match any files
|
24
|
+
#
|
25
|
+
# @return [Array<String>] list of matching files in lexicographic order
|
26
|
+
def find(options)
|
27
|
+
included_paths = options.fetch(:included_paths, []).map { |p| normalize_path(p) }
|
28
|
+
excluded_paths = options.fetch(:excluded_paths, []).map { |p| normalize_path(p) }
|
29
|
+
included_patterns = options.fetch(:included_patterns, []).map { |p| normalize_path(p) }
|
30
|
+
excluded_patterns = options.fetch(:excluded_patterns, []).map { |p| normalize_path(p) }
|
31
|
+
allowed_extensions = options.fetch(:allowed_extensions)
|
32
|
+
|
33
|
+
included_files = expand_paths(included_paths, included_patterns, allowed_extensions)
|
34
|
+
matching_files = filter_files(included_files, excluded_paths, excluded_patterns)
|
35
|
+
|
36
|
+
matching_files.uniq.sort
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Expand included paths to include lintable files under paths which are
|
42
|
+
# directories.
|
43
|
+
#
|
44
|
+
# @param paths [Array<String>]
|
45
|
+
# @param patterns [Array<String>]
|
46
|
+
# @param allowed_extensions [Array<String>]
|
47
|
+
#
|
48
|
+
# @return [Array<String>]
|
49
|
+
def expand_paths(paths, patterns, allowed_extensions)
|
50
|
+
find_files_in_paths(paths, allowed_extensions) + find_matching_files(patterns)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Exclude specified files from the list of included files, expanding the
|
54
|
+
# excluded paths as necessary.
|
55
|
+
#
|
56
|
+
# @param included_paths [Array<String>]
|
57
|
+
# @param excluded_paths [Array<String>]
|
58
|
+
# @param excluded_patterns [Array<String>]
|
59
|
+
#
|
60
|
+
# @return [Array<String>]
|
61
|
+
def filter_files(included_files, excluded_paths, excluded_patterns)
|
62
|
+
# Convert excluded paths to patterns so we don't need to actually hold
|
63
|
+
# all excluded files in memory
|
64
|
+
excluded_patterns = excluded_patterns.dup
|
65
|
+
excluded_paths.each do |file|
|
66
|
+
if File.directory?(file)
|
67
|
+
excluded_patterns << File.join(file, '**', '*')
|
68
|
+
elsif File.file?(file)
|
69
|
+
excluded_patterns << file
|
70
|
+
else
|
71
|
+
raise LintTrappings::InvalidFilePathError,
|
72
|
+
"Excluded path '#{path}' does not correspond to a valid file"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
included_files.reject do |file|
|
77
|
+
LintTrappings::Utils.any_glob_matches?(excluded_patterns, file)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Search the specified paths for lintable files.
|
82
|
+
#
|
83
|
+
# If path is a directory, searches the directory recursively.
|
84
|
+
#
|
85
|
+
# @param paths [Array<String>]
|
86
|
+
# @param allowed_extensions [Array<String>]
|
87
|
+
#
|
88
|
+
# @return [Array<String>]
|
89
|
+
def find_files_in_paths(paths, allowed_extensions)
|
90
|
+
files = []
|
91
|
+
|
92
|
+
paths.each do |path|
|
93
|
+
if File.directory?(path)
|
94
|
+
files += find_files_in_directory(path, allowed_extensions)
|
95
|
+
elsif File.file?(path)
|
96
|
+
files << path
|
97
|
+
else
|
98
|
+
raise LintTrappings::InvalidFilePathError,
|
99
|
+
"Path '#{path}' does not correspond to a valid file or directory"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
files.uniq.map { |path| normalize_path(path) }
|
104
|
+
end
|
105
|
+
|
106
|
+
# Recursively search the specified directory for lintable files.
|
107
|
+
#
|
108
|
+
# @param directory [String]
|
109
|
+
# @param allowed_extensions [Array<String>]
|
110
|
+
#
|
111
|
+
# @return [Array<String>]
|
112
|
+
def find_files_in_directory(directory, allowed_extensions)
|
113
|
+
files = []
|
114
|
+
|
115
|
+
::Find.find(directory) do |path|
|
116
|
+
files << path if lintable_file?(path, allowed_extensions)
|
117
|
+
end
|
118
|
+
|
119
|
+
files
|
120
|
+
end
|
121
|
+
|
122
|
+
# Find all files matching the specified glob patterns.
|
123
|
+
#
|
124
|
+
# @param patterns [Array<String>] glob patterns
|
125
|
+
#
|
126
|
+
# @return [Array<String>]
|
127
|
+
def find_matching_files(patterns)
|
128
|
+
files = []
|
129
|
+
|
130
|
+
patterns.each do |pattern|
|
131
|
+
matches = ::Dir.glob(pattern,
|
132
|
+
::File::FNM_PATHNAME | # Wildcards don't match path separators
|
133
|
+
::File::FNM_DOTMATCH) # `*` wildcard matches dotfiles
|
134
|
+
|
135
|
+
if matches.empty?
|
136
|
+
# One of the patterns specified does not match anything; raise a more
|
137
|
+
# descriptive exception so we know which one
|
138
|
+
raise LintTrappings::InvalidFilePatternError,
|
139
|
+
"Glob pattern '#{pattern}' does not match any file"
|
140
|
+
end
|
141
|
+
|
142
|
+
matches.each do |path|
|
143
|
+
files << path if File.file?(path)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
files.flatten.uniq.map { |path| normalize_path(path) }
|
148
|
+
end
|
149
|
+
|
150
|
+
# Trim "./" from the front of relative paths.
|
151
|
+
#
|
152
|
+
# @param path [String]
|
153
|
+
#
|
154
|
+
# @return [String]
|
155
|
+
def normalize_path(path)
|
156
|
+
path.start_with?(".#{File::SEPARATOR}") ? path[2..-1] : path
|
157
|
+
end
|
158
|
+
|
159
|
+
# Whether a file should be treated as lintable.
|
160
|
+
#
|
161
|
+
# @param file [String]
|
162
|
+
# @param allowed_extensions [Array<String>]
|
163
|
+
#
|
164
|
+
# @return [Boolean]
|
165
|
+
def lintable_file?(file, allowed_extensions)
|
166
|
+
return false unless ::File.file?(file)
|
167
|
+
allowed_extensions.include?(::File.extname(file))
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module LintTrappings::Formatter
|
2
|
+
# Abstract lint formatter. Subclass and override {#display_report} to
|
3
|
+
# implement a custom formatter.
|
4
|
+
#
|
5
|
+
# @abstract
|
6
|
+
class Base
|
7
|
+
# @param application [LintTrappings::Application]
|
8
|
+
# @param config [LintTrappings::Configuration]
|
9
|
+
# @param options [Hash]
|
10
|
+
# @param output [LintTrappings::Output]
|
11
|
+
def initialize(application, config, options, output)
|
12
|
+
@application = application
|
13
|
+
@config = config
|
14
|
+
@options = options
|
15
|
+
@output = output
|
16
|
+
|
17
|
+
# Used in helpers to determine if severity should be displayed as failure
|
18
|
+
# or not
|
19
|
+
severities = @config.fetch('severities', error: 'fail', warning: 'warn')
|
20
|
+
@fail_severities = severities.select { |_severity, action| action == 'fail' }.keys
|
21
|
+
@warn_severities = severities.select { |_severity, action| action == 'warn' }.keys
|
22
|
+
end
|
23
|
+
|
24
|
+
# Called at the start of the run once runner has determined all files it
|
25
|
+
# will lint.
|
26
|
+
def started(_files_to_lint)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Called at the beginning of a job for a linter run against a file.
|
30
|
+
#
|
31
|
+
# This can be called in parallel.
|
32
|
+
def job_started(_job)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Called at the end of a job for a linter run against a file.
|
36
|
+
#
|
37
|
+
# This can be called in parallel.
|
38
|
+
def job_finished(_job, _lints)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Called at the end of the run.
|
42
|
+
def finished(_report)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# @return [LintTrappings::Application]
|
48
|
+
attr_reader :application
|
49
|
+
|
50
|
+
# @return [LintTrappings::Configuration]
|
51
|
+
attr_reader :config
|
52
|
+
|
53
|
+
# @return [Hash]
|
54
|
+
attr_reader :options
|
55
|
+
|
56
|
+
# @return [LintTrappings::Output] stream to send output to
|
57
|
+
attr_reader :output
|
58
|
+
|
59
|
+
def failing_lint?(lint)
|
60
|
+
@fail_severities.include?(lint.severity)
|
61
|
+
end
|
62
|
+
|
63
|
+
def warning_lint?(lint)
|
64
|
+
@warn_severities.include?(lint.severity)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'lint_trappings/formatter/base'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
module LintTrappings::Formatter
|
5
|
+
# Outputs results in a Checkstyle-compatible format.
|
6
|
+
#
|
7
|
+
# @see http://checkstyle.sourceforge.net/
|
8
|
+
class Checkstyle < Base
|
9
|
+
def finished(report)
|
10
|
+
xml = StringIO.new
|
11
|
+
xml << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
|
12
|
+
|
13
|
+
xml << "<checkstyle version=\"1.5.6\">\n"
|
14
|
+
report.lints.group_by(&:path).each do |path, lints|
|
15
|
+
file_name_absolute = File.expand_path(path)
|
16
|
+
xml << " <file name=#{file_name_absolute.encode(xml: :attr)}>\n"
|
17
|
+
|
18
|
+
lints.each do |lint|
|
19
|
+
xml << " <error source=\"#{lint.linter.canonical_name if lint.linter}\" " \
|
20
|
+
"line=\"#{lint.location.line}\" " \
|
21
|
+
"column=\"#{lint.location.column}\" " \
|
22
|
+
"length=\"#{lint.location.length}\" " \
|
23
|
+
"severity=\"#{lint.severity}\" " \
|
24
|
+
"message=#{lint.message.encode(xml: :attr)} />\n"
|
25
|
+
end
|
26
|
+
|
27
|
+
xml << " </file>\n"
|
28
|
+
end
|
29
|
+
xml << "</checkstyle>\n"
|
30
|
+
|
31
|
+
output.print xml.string
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'lint_trappings/formatter/base'
|
2
|
+
|
3
|
+
module LintTrappings::Formatter
|
4
|
+
# Outputs lints in a simple format with the filename, line number, and lint
|
5
|
+
# message.
|
6
|
+
class Default < Base
|
7
|
+
def started(files_to_lint)
|
8
|
+
files = LintTrappings::Utils.pluralize('file', files_to_lint.count)
|
9
|
+
output.info "Scanning #{files_to_lint.count} #{files}..."
|
10
|
+
end
|
11
|
+
|
12
|
+
def job_finished(_job, lints)
|
13
|
+
@at_least_one_job_finished = true
|
14
|
+
|
15
|
+
if lints.any? { |lint| failing_lint?(lint) }
|
16
|
+
output.error 'F', false
|
17
|
+
elsif lints.any? { |lint| warning_lint?(lint) }
|
18
|
+
output.warning 'W', false
|
19
|
+
else
|
20
|
+
output.success '.', false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def finished(report)
|
25
|
+
# Ensure we have a newline after the last progress dot output
|
26
|
+
output.newline if @at_least_one_job_finished
|
27
|
+
|
28
|
+
report.lints.each do |lint|
|
29
|
+
print_location(lint)
|
30
|
+
print_message(lint)
|
31
|
+
end
|
32
|
+
|
33
|
+
output.newline
|
34
|
+
|
35
|
+
files = LintTrappings::Utils.pluralize('file', report.documents_inspected.count)
|
36
|
+
output.print "#{report.documents_inspected.count} #{files} inspected"
|
37
|
+
|
38
|
+
if report.failures?
|
39
|
+
failures = LintTrappings::Utils.pluralize('failure', report.failures.count)
|
40
|
+
output.print ', '
|
41
|
+
output.error "#{report.failures.count} #{failures} reported", false
|
42
|
+
end
|
43
|
+
|
44
|
+
if report.warnings?
|
45
|
+
warnings = LintTrappings::Utils.pluralize('warning', report.warnings.count)
|
46
|
+
output.print ', '
|
47
|
+
output.warning "#{report.warnings.count} #{warnings} reported", false
|
48
|
+
end
|
49
|
+
|
50
|
+
if report.success?
|
51
|
+
output.print ', '
|
52
|
+
output.success 'no issues reported', false
|
53
|
+
end
|
54
|
+
|
55
|
+
output.newline
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def print_location(lint)
|
61
|
+
output.info lint.path, false
|
62
|
+
output.puts ':', false
|
63
|
+
output.bold lint.source_range.begin.line, false
|
64
|
+
output.print ':'
|
65
|
+
output.bold lint.source_range.begin.column, false
|
66
|
+
output.print ' '
|
67
|
+
end
|
68
|
+
|
69
|
+
def print_message(lint)
|
70
|
+
if failing_lint?(lint)
|
71
|
+
output.error severity_character(lint.severity), false
|
72
|
+
elsif warning_lint?(lint)
|
73
|
+
output.warning severity_character(lint.severity), false
|
74
|
+
end
|
75
|
+
|
76
|
+
if lint.linter
|
77
|
+
output.notice("[#{lint.linter.canonical_name}] ", false)
|
78
|
+
end
|
79
|
+
|
80
|
+
message = lint.message
|
81
|
+
if lint.exception
|
82
|
+
message << ' (specify --debug flag to see backtrace)' unless options[:debug]
|
83
|
+
end
|
84
|
+
output.puts message
|
85
|
+
|
86
|
+
print_exception(lint)
|
87
|
+
end
|
88
|
+
|
89
|
+
def print_exception(lint)
|
90
|
+
return unless lint.exception && options[:debug]
|
91
|
+
output.error "#{lint.exception.class}: #{lint.exception.message}"
|
92
|
+
output.error lint.exception.backtrace.join("\n")
|
93
|
+
end
|
94
|
+
|
95
|
+
def severity_character(severity)
|
96
|
+
"#{severity.to_s[0].capitalize} "
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'lint_trappings/formatter/base'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module LintTrappings::Formatter
|
5
|
+
# Outputs report as a JSON document.
|
6
|
+
class JSON < Base
|
7
|
+
def finished(report)
|
8
|
+
lints = report.lints
|
9
|
+
grouped = lints.group_by(&:path)
|
10
|
+
|
11
|
+
report_hash = {
|
12
|
+
metadata: metadata,
|
13
|
+
files: grouped.map { |path, lints_for_path| path_hash(path, lints_for_path) },
|
14
|
+
summary: {
|
15
|
+
offense_count: lints.count,
|
16
|
+
offending_file_count: grouped.count,
|
17
|
+
inspected_file_count: report.documents_inspected.count,
|
18
|
+
},
|
19
|
+
}
|
20
|
+
|
21
|
+
output.puts ::JSON.pretty_generate(report_hash)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def metadata
|
27
|
+
{
|
28
|
+
linter_version: @application.version,
|
29
|
+
ruby_engine: RUBY_ENGINE,
|
30
|
+
ruby_version: RUBY_VERSION,
|
31
|
+
ruby_patchlevel: RUBY_PATCHLEVEL.to_s,
|
32
|
+
ruby_platform: RUBY_PLATFORM,
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def path_hash(path, lints)
|
37
|
+
{
|
38
|
+
path: path,
|
39
|
+
offenses: lints.map { |lint| lint_hash(lint) },
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def lint_hash(lint)
|
44
|
+
{
|
45
|
+
severity: lint.severity,
|
46
|
+
message: lint.message,
|
47
|
+
line: lint.source_range.begin.line,
|
48
|
+
column: lint.source_range.begin.column,
|
49
|
+
source_range: {
|
50
|
+
begin: {
|
51
|
+
line: lint.source_range.begin.line,
|
52
|
+
column: lint.source_range.begin.column,
|
53
|
+
},
|
54
|
+
end: {
|
55
|
+
line: lint.source_range.end.line,
|
56
|
+
column: lint.source_range.end.column,
|
57
|
+
},
|
58
|
+
},
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|