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,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
|