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,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,8 @@
1
+ module LintTrappings::Command
2
+ # Outputs help documentation.
3
+ class DisplayHelp < Base
4
+ def run
5
+ output.puts options[:help_message]
6
+ end
7
+ end
8
+ 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