lint_trappings 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +21 -0
  3. data/lib/lint_trappings.rb +17 -0
  4. data/lib/lint_trappings/application.rb +138 -0
  5. data/lib/lint_trappings/arguments_parser.rb +145 -0
  6. data/lib/lint_trappings/cli.rb +61 -0
  7. data/lib/lint_trappings/command/base.rb +36 -0
  8. data/lib/lint_trappings/command/display_documentation.rb +65 -0
  9. data/lib/lint_trappings/command/display_formatters.rb +14 -0
  10. data/lib/lint_trappings/command/display_help.rb +8 -0
  11. data/lib/lint_trappings/command/display_linters.rb +24 -0
  12. data/lib/lint_trappings/command/display_version.rb +14 -0
  13. data/lib/lint_trappings/command/scan.rb +19 -0
  14. data/lib/lint_trappings/configuration.rb +94 -0
  15. data/lib/lint_trappings/configuration_loader.rb +98 -0
  16. data/lib/lint_trappings/configuration_resolver.rb +49 -0
  17. data/lib/lint_trappings/document.rb +45 -0
  18. data/lib/lint_trappings/errors.rb +127 -0
  19. data/lib/lint_trappings/executable.rb +26 -0
  20. data/lib/lint_trappings/file_finder.rb +171 -0
  21. data/lib/lint_trappings/formatter/base.rb +67 -0
  22. data/lib/lint_trappings/formatter/checkstyle.rb +34 -0
  23. data/lib/lint_trappings/formatter/default.rb +99 -0
  24. data/lib/lint_trappings/formatter/json.rb +62 -0
  25. data/lib/lint_trappings/formatter_forwarder.rb +23 -0
  26. data/lib/lint_trappings/formatter_loader.rb +45 -0
  27. data/lib/lint_trappings/lint.rb +37 -0
  28. data/lib/lint_trappings/linter.rb +182 -0
  29. data/lib/lint_trappings/linter_configuration_validator.rb +42 -0
  30. data/lib/lint_trappings/linter_loader.rb +44 -0
  31. data/lib/lint_trappings/linter_plugin.rb +35 -0
  32. data/lib/lint_trappings/linter_selector.rb +120 -0
  33. data/lib/lint_trappings/location.rb +39 -0
  34. data/lib/lint_trappings/output.rb +118 -0
  35. data/lib/lint_trappings/preprocessor.rb +41 -0
  36. data/lib/lint_trappings/rake_task.rb +145 -0
  37. data/lib/lint_trappings/report.rb +58 -0
  38. data/lib/lint_trappings/runner.rb +161 -0
  39. data/lib/lint_trappings/spec.rb +12 -0
  40. data/lib/lint_trappings/spec/directory_helpers.rb +22 -0
  41. data/lib/lint_trappings/spec/indentation_helpers.rb +7 -0
  42. data/lib/lint_trappings/spec/matchers/report_lint_matcher.rb +169 -0
  43. data/lib/lint_trappings/spec/shared_contexts/linter_shared_context.rb +35 -0
  44. data/lib/lint_trappings/utils.rb +123 -0
  45. data/lib/lint_trappings/version.rb +4 -0
  46. 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