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.
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,12 @@
1
+ require 'lint_trappings'
2
+
3
+ # Stub declaration so nested modules can reference it
4
+ module LintTrappings::Spec; end
5
+
6
+ Dir[File.join(File.dirname(__FILE__), 'spec', '**', '*.rb')].each do |file|
7
+ require file
8
+ end
9
+
10
+ RSpec.configure do |config|
11
+ config.include LintTrappings::Matchers
12
+ end
@@ -0,0 +1,22 @@
1
+ require 'tmpdir'
2
+
3
+ # Helpers for creating temporary directories for testing.
4
+ module LintTrappings::Spec::DirectoryHelpers
5
+ module_function
6
+
7
+ # Creates a directory in a temporary directory which will automatically be
8
+ # destroyed at the end of the spec run. Any block passed to this will be
9
+ # executed with the created directory as the working directory.
10
+ #
11
+ # @return [String] The full path of the directory.
12
+ def directory(name = 'some-dir', &block)
13
+ tmpdir = Dir.mktmpdir.tap do |path|
14
+ Dir.chdir(path) do
15
+ Dir.mkdir(name)
16
+ Dir.chdir(name, &block) if block_given?
17
+ end
18
+ end
19
+
20
+ File.join(tmpdir, name)
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ module LintTrappings::Spec::IndentationHelpers
2
+ # Strips off excess leading indentation from each line so we can use Heredocs
3
+ # for writing code without having the leading indentation count.
4
+ def normalize_indent(code)
5
+ LintTrappings::Utils.normalize_indent(code)
6
+ end
7
+ end
@@ -0,0 +1,169 @@
1
+ # rubocop:disable Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/LineLength
2
+
3
+ module LintTrappings::Matchers
4
+ # RSpec matcher that returns whether or not a linter reported lints matching
5
+ # the specified criteria.
6
+ #
7
+ # @example
8
+ #
9
+ # it { should report_lint line: 2 }
10
+ class ReportLintMatcher
11
+ ALLOWED_KEYWORD_ARGUMENTS = %i[line message]
12
+
13
+ def initialize(*args, **kwargs)
14
+ @options = kwargs
15
+
16
+ if args.any?
17
+ raise ArgumentError,
18
+ '`report_lint` was given more than one argument!' if args.length > 1
19
+
20
+ if @options[:line]
21
+ raise ArgumentError,
22
+ '`line` keyword argument cannot be specified when Range is given'
23
+ end
24
+
25
+ @range = args.first
26
+ if @range.is_a?(Array)
27
+ if @range.length != 2 ||
28
+ !@range.first.is_a?(Integer) ||
29
+ !@range.last.is_a?(Integer)
30
+ raise ArgumentError,
31
+ 'Location tuple must be an Array of two integers ' \
32
+ "representing line and column, but was #{@range.inspect}"
33
+ end
34
+
35
+ # Convert to an actual range by assuming it spans nothing
36
+ @range = @range..@range
37
+ elsif !@range.is_a?(Range)
38
+ raise ArgumentError, '`report_lint` must be given a Range e.g. [1, 2]..[3, 4]'
39
+ elsif !(@range.begin.is_a?(Array) && @range.end.is_a?(Array))
40
+ raise ArgumentError, 'Source range must have Array tuple endpoints'
41
+ end
42
+ else
43
+ # Otherwise no explicit range was specified, so verify the options
44
+ @options.keys.each do |key|
45
+ raise ArgumentError,
46
+ "Unknown keyword argument #{key}" unless ALLOWED_KEYWORD_ARGUMENTS.include?(key)
47
+ end
48
+
49
+ @line = @options[:line]
50
+ end
51
+
52
+ @message = @options[:message] if @options[:message]
53
+ end
54
+
55
+ def matches?(linter)
56
+ # We're cheating by accessing private values here, but it will allow us to
57
+ # present more-helpful error messages since we get access to so much more
58
+ # information by passing the linter instead of just a list of lints.
59
+ @linter = linter
60
+ @lints = linter.instance_variable_get(:@lints)
61
+
62
+ any_lint_matches?
63
+ end
64
+
65
+ def failure_message
66
+ output = 'expected that a lint would be reported'
67
+
68
+ if !any_range_matches?
69
+ output <<
70
+ if @line
71
+ " on line #{@line}"
72
+ elsif @range
73
+ " on #{range_to_str(@range)}"
74
+ end.to_s
75
+
76
+ output <<
77
+ case @lints.count
78
+ when 0
79
+ ', but none were'
80
+ when 1
81
+ if @line
82
+ ", but was reported on line #{@lints.first.source_range.begin.line}"
83
+ elsif @range
84
+ ", but was reported on #{range_to_str(@lints.first.source_range)}"
85
+ end.to_s
86
+ else
87
+ if @line
88
+ ", but lints were reported on the following lines instead:\n" +
89
+ @lints.map { |lint| lint.source_range.line }.sort.join(', ')
90
+ elsif @range
91
+ ", but lints were reported on the following ranges instead:\n" +
92
+ @lints.map { |lint| range_to_str(lint.source_range) }.join("\n")
93
+ end.to_s
94
+ end
95
+ elsif @message
96
+ matching_lints = lints_matching_range
97
+ output <<
98
+ if @message.is_a?(Regexp)
99
+ " with message matching pattern #{@message.inspect} "
100
+ else
101
+ " with message #{@message.inspect} "
102
+ end
103
+
104
+ output << "but got:\n" << matching_lints.map(&:message).join("\n")
105
+ end
106
+
107
+ output
108
+ end
109
+
110
+ def failure_message_when_negated
111
+ 'expected that a lint would NOT be reported'
112
+ end
113
+
114
+ def description
115
+ output = 'report a lint'
116
+ output <<
117
+ if @line
118
+ " on line #{@line}"
119
+ elsif @range
120
+ " on #{range_to_str(@range)}"
121
+ end.to_s
122
+
123
+ output
124
+ end
125
+
126
+ private
127
+
128
+ def lints_matching_range
129
+ @lints.select { |lint| range_matches?(lint) }
130
+ end
131
+
132
+ def any_lint_matches?
133
+ return true if !@line && !@range && @lints.any?
134
+ lints_matching_range.any? { |lint| message_matches?(lint) }
135
+ end
136
+
137
+ def any_range_matches?
138
+ @lints.any? do |lint|
139
+ range_matches?(lint)
140
+ end
141
+ end
142
+
143
+ def range_matches?(lint)
144
+ if @line
145
+ lint.source_range.begin.line == @line
146
+ else
147
+ lint.source_range == @range
148
+ end
149
+ end
150
+
151
+ def message_matches?(lint)
152
+ if @message.nil?
153
+ true
154
+ elsif @message.is_a?(Regexp)
155
+ lint.message =~ @message
156
+ elsif @message.is_a?(String)
157
+ lint.message == @message
158
+ end
159
+ end
160
+
161
+ def range_to_str(range)
162
+ "(L#{range.begin.line},C#{range.begin.column})..(L#{range.end.line},C#{range.end.column})"
163
+ end
164
+ end
165
+
166
+ def report_lint(*args, **kwargs)
167
+ ReportLintMatcher.new(*args, **kwargs)
168
+ end
169
+ end
@@ -0,0 +1,35 @@
1
+ # Makes writing tests for linters a lot DRYer by taking any `src` variable
2
+ # defined via `let` and normalizing it (removing indentation that would be
3
+ # inserted by using Heredocs) and setting up the subject to be the lints
4
+ # returned by Linter#run.
5
+ #
6
+ # Thus a typical test will look like:
7
+ #
8
+ # @example
9
+ # require 'spec_helper'
10
+ #
11
+ # RSpec.describe MyApp::Linter::MyLinter do
12
+ # include_context 'linter'
13
+ #
14
+ # context 'when source contains "foo"' do
15
+ # let(:src) { <<-SRC }
16
+ # This is some code
17
+ # with the word "foo" in it.
18
+ # SRC
19
+ #
20
+ # it { should report_lint line: 2 }
21
+ # end
22
+ # end
23
+ shared_context 'linter' do
24
+ let(:config) do
25
+ LINT_TRAP_APPLICATION_CLASS.base_configuration.for_linter(described_class)
26
+ end
27
+
28
+ subject do
29
+ linter = described_class.new(config)
30
+ document = LINT_TRAP_APPLICATION_CLASS.document_class
31
+ .new(normalize_indent(src), config)
32
+ linter.run(document)
33
+ linter
34
+ end
35
+ end
@@ -0,0 +1,123 @@
1
+ module LintTrappings
2
+ # Miscellaneus collection of helper functions.
3
+ module Utils
4
+ module_function
5
+
6
+ # Returns whether a glob pattern (or any of a list of patterns) matches the
7
+ # specified file.
8
+ #
9
+ # This is defined here so our file globbing options are consistent
10
+ # everywhere we perform globbing.
11
+ #
12
+ # @param glob [String, Array]
13
+ # @param file [String]
14
+ # @return [Boolean]
15
+ def any_glob_matches?(globs_or_glob, file)
16
+ Array(globs_or_glob).any? do |glob|
17
+ ::File.fnmatch?(glob, file,
18
+ ::File::FNM_PATHNAME | # Wildcards don't match path separators
19
+ ::File::FNM_DOTMATCH) # `*` wildcard matches dotfiles
20
+ end
21
+ end
22
+
23
+ # Find all consecutive items satisfying the given block of a minimum size,
24
+ # yielding each group of consecutive items to the provided block.
25
+ #
26
+ # @param items [Array]
27
+ # @param satisfies [Proc] function that takes an item and returns true/false
28
+ # @param min_consecutive [Fixnum] minimum number of consecutive items before
29
+ # yielding the group
30
+ # @yield Passes list of consecutive items all matching the criteria defined
31
+ # by the `satisfies` {Proc} to the provided block
32
+ # @yieldparam group [Array] List of consecutive items
33
+ # @yieldreturn [Boolean] block should return whether item matches criteria
34
+ # for inclusion
35
+ def for_consecutive_items(items, satisfies, min_consecutive = 2)
36
+ current_index = -1
37
+
38
+ while (current_index += 1) < items.count
39
+ next unless satisfies[items[current_index]]
40
+
41
+ count = count_consecutive(items, current_index, &satisfies)
42
+ next unless count >= min_consecutive
43
+
44
+ # Yield the chunk of consecutive items
45
+ yield items[current_index...(current_index + count)]
46
+
47
+ current_index += count # Skip this patch of consecutive items to find more
48
+ end
49
+ end
50
+
51
+ # Count the number of consecutive items satisfying the given {Proc}.
52
+ #
53
+ # @param items [Array]
54
+ # @param offset [Fixnum] index to start searching from
55
+ # @yield [item] Passes item to the provided block.
56
+ # @yieldparam item [Object] Item to evaluate as matching criteria for
57
+ # inclusion
58
+ # @yieldreturn [Boolean] whether to include the item
59
+ # @return [Integer]
60
+ def count_consecutive(items, offset = 0, &block)
61
+ count = 1
62
+ count += 1 while (offset + count < items.count) && block.call(items[offset + count])
63
+ count
64
+ end
65
+
66
+ # Convert a class name or CamelCase string into snake_case.
67
+ #
68
+ # @see stackoverflow.com/questions/1509915/converting-camel-case-to-underscore-case-in-ruby
69
+ #
70
+ # @param str [String]
71
+ #
72
+ # @return [String]
73
+ def snake_case(str)
74
+ str.gsub(/::/, '/')
75
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
76
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
77
+ .tr('-', '_')
78
+ .downcase
79
+ end
80
+
81
+ # Converts a string containing underscores/hyphens/spaces into CamelCase.
82
+ #
83
+ # @param str [String]
84
+ #
85
+ # @return [String]
86
+ def camel_case(str)
87
+ str.split(/_|-| /).map { |part| part.sub(/^\w/, &:upcase) }.join
88
+ end
89
+
90
+ # Returns the plural of the word if necessary based on the given count.
91
+ #
92
+ # @param word [String]
93
+ # @param count [Integer]
94
+ #
95
+ # @return [String]
96
+ def pluralize(word, count)
97
+ count == 1 ? word : "#{word}s"
98
+ end
99
+
100
+ # Strips off excess leading indentation from each line so we can use Heredocs
101
+ # for writing code without having the leading indentation count.
102
+ def normalize_indent(code)
103
+ leading_indent = code[/^(\s*)/, 1]
104
+ code.lstrip.gsub(/\n#{leading_indent}/, "\n")
105
+ end
106
+
107
+ # Calls a block of code with a modified set of environment variables,
108
+ # restoring them once the code has executed.
109
+ #
110
+ # @param env [Hash] environment variables to set
111
+ def with_environment(env)
112
+ old_env = {}
113
+ env.each do |var, value|
114
+ old_env[var] = ENV[var.to_s]
115
+ ENV[var.to_s] = value
116
+ end
117
+
118
+ yield
119
+ ensure
120
+ old_env.each { |var, value| ENV[var.to_s] = value }
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,4 @@
1
+ # Defines the gem version.
2
+ module LintTrappings
3
+ VERSION = '0.1.0'
4
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lint_trappings
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shane da Silva
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-02-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parallel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: terminal-table
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.4'
41
+ description: Framework for writing static analysis tools (a.k.a. linters)
42
+ email:
43
+ - shane@dasilva.io
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - LICENSE.md
49
+ - lib/lint_trappings.rb
50
+ - lib/lint_trappings/application.rb
51
+ - lib/lint_trappings/arguments_parser.rb
52
+ - lib/lint_trappings/cli.rb
53
+ - lib/lint_trappings/command/base.rb
54
+ - lib/lint_trappings/command/display_documentation.rb
55
+ - lib/lint_trappings/command/display_formatters.rb
56
+ - lib/lint_trappings/command/display_help.rb
57
+ - lib/lint_trappings/command/display_linters.rb
58
+ - lib/lint_trappings/command/display_version.rb
59
+ - lib/lint_trappings/command/scan.rb
60
+ - lib/lint_trappings/configuration.rb
61
+ - lib/lint_trappings/configuration_loader.rb
62
+ - lib/lint_trappings/configuration_resolver.rb
63
+ - lib/lint_trappings/document.rb
64
+ - lib/lint_trappings/errors.rb
65
+ - lib/lint_trappings/executable.rb
66
+ - lib/lint_trappings/file_finder.rb
67
+ - lib/lint_trappings/formatter/base.rb
68
+ - lib/lint_trappings/formatter/checkstyle.rb
69
+ - lib/lint_trappings/formatter/default.rb
70
+ - lib/lint_trappings/formatter/json.rb
71
+ - lib/lint_trappings/formatter_forwarder.rb
72
+ - lib/lint_trappings/formatter_loader.rb
73
+ - lib/lint_trappings/lint.rb
74
+ - lib/lint_trappings/linter.rb
75
+ - lib/lint_trappings/linter_configuration_validator.rb
76
+ - lib/lint_trappings/linter_loader.rb
77
+ - lib/lint_trappings/linter_plugin.rb
78
+ - lib/lint_trappings/linter_selector.rb
79
+ - lib/lint_trappings/location.rb
80
+ - lib/lint_trappings/output.rb
81
+ - lib/lint_trappings/preprocessor.rb
82
+ - lib/lint_trappings/rake_task.rb
83
+ - lib/lint_trappings/report.rb
84
+ - lib/lint_trappings/runner.rb
85
+ - lib/lint_trappings/spec.rb
86
+ - lib/lint_trappings/spec/directory_helpers.rb
87
+ - lib/lint_trappings/spec/indentation_helpers.rb
88
+ - lib/lint_trappings/spec/matchers/report_lint_matcher.rb
89
+ - lib/lint_trappings/spec/shared_contexts/linter_shared_context.rb
90
+ - lib/lint_trappings/utils.rb
91
+ - lib/lint_trappings/version.rb
92
+ homepage: https://github.com/sds/lint-trappings
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '2'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.4.5.1
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: Linter framework
116
+ test_files: []
117
+ has_rdoc: