slim_lint_standard 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +21 -0
  3. data/bin/slim-lint-standard +7 -0
  4. data/config/default.yml +109 -0
  5. data/lib/slim_lint/atom.rb +129 -0
  6. data/lib/slim_lint/capture_map.rb +19 -0
  7. data/lib/slim_lint/cli.rb +167 -0
  8. data/lib/slim_lint/configuration.rb +111 -0
  9. data/lib/slim_lint/configuration_loader.rb +86 -0
  10. data/lib/slim_lint/constants.rb +10 -0
  11. data/lib/slim_lint/document.rb +78 -0
  12. data/lib/slim_lint/engine.rb +41 -0
  13. data/lib/slim_lint/exceptions.rb +20 -0
  14. data/lib/slim_lint/file_finder.rb +88 -0
  15. data/lib/slim_lint/filter.rb +126 -0
  16. data/lib/slim_lint/filters/attribute_processor.rb +46 -0
  17. data/lib/slim_lint/filters/auto_indenter.rb +39 -0
  18. data/lib/slim_lint/filters/control_processor.rb +46 -0
  19. data/lib/slim_lint/filters/do_inserter.rb +39 -0
  20. data/lib/slim_lint/filters/end_inserter.rb +74 -0
  21. data/lib/slim_lint/filters/interpolation.rb +73 -0
  22. data/lib/slim_lint/filters/multi_flattener.rb +32 -0
  23. data/lib/slim_lint/filters/splat_processor.rb +20 -0
  24. data/lib/slim_lint/filters/static_merger.rb +47 -0
  25. data/lib/slim_lint/lint.rb +70 -0
  26. data/lib/slim_lint/linter/avoid_multiline_expressions.rb +41 -0
  27. data/lib/slim_lint/linter/comment_control_statement.rb +26 -0
  28. data/lib/slim_lint/linter/consecutive_control_statements.rb +26 -0
  29. data/lib/slim_lint/linter/control_statement_spacing.rb +32 -0
  30. data/lib/slim_lint/linter/dynamic_output_spacing.rb +77 -0
  31. data/lib/slim_lint/linter/embedded_engines.rb +18 -0
  32. data/lib/slim_lint/linter/empty_control_statement.rb +15 -0
  33. data/lib/slim_lint/linter/empty_lines.rb +24 -0
  34. data/lib/slim_lint/linter/file_length.rb +18 -0
  35. data/lib/slim_lint/linter/line_length.rb +18 -0
  36. data/lib/slim_lint/linter/redundant_div.rb +21 -0
  37. data/lib/slim_lint/linter/rubocop.rb +131 -0
  38. data/lib/slim_lint/linter/standard.rb +69 -0
  39. data/lib/slim_lint/linter/tab.rb +20 -0
  40. data/lib/slim_lint/linter/tag_case.rb +15 -0
  41. data/lib/slim_lint/linter/trailing_blank_lines.rb +19 -0
  42. data/lib/slim_lint/linter/trailing_whitespace.rb +17 -0
  43. data/lib/slim_lint/linter.rb +93 -0
  44. data/lib/slim_lint/linter_registry.rb +37 -0
  45. data/lib/slim_lint/linter_selector.rb +87 -0
  46. data/lib/slim_lint/logger.rb +103 -0
  47. data/lib/slim_lint/matcher/anything.rb +11 -0
  48. data/lib/slim_lint/matcher/base.rb +21 -0
  49. data/lib/slim_lint/matcher/capture.rb +32 -0
  50. data/lib/slim_lint/matcher/nothing.rb +13 -0
  51. data/lib/slim_lint/options.rb +110 -0
  52. data/lib/slim_lint/parser.rb +584 -0
  53. data/lib/slim_lint/rake_task.rb +125 -0
  54. data/lib/slim_lint/report.rb +25 -0
  55. data/lib/slim_lint/reporter/checkstyle_reporter.rb +42 -0
  56. data/lib/slim_lint/reporter/default_reporter.rb +40 -0
  57. data/lib/slim_lint/reporter/emacs_reporter.rb +40 -0
  58. data/lib/slim_lint/reporter/json_reporter.rb +50 -0
  59. data/lib/slim_lint/reporter.rb +44 -0
  60. data/lib/slim_lint/ruby_extract_engine.rb +30 -0
  61. data/lib/slim_lint/ruby_extractor.rb +175 -0
  62. data/lib/slim_lint/ruby_parser.rb +32 -0
  63. data/lib/slim_lint/runner.rb +82 -0
  64. data/lib/slim_lint/sexp.rb +134 -0
  65. data/lib/slim_lint/sexp_visitor.rb +150 -0
  66. data/lib/slim_lint/source_location.rb +45 -0
  67. data/lib/slim_lint/utils.rb +84 -0
  68. data/lib/slim_lint/version.rb +6 -0
  69. data/lib/slim_lint.rb +55 -0
  70. metadata +218 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6dcf68f7cde5910a4f87dc7f7c1409976006ffde7a554601cb2ff0fad4240dad
4
+ data.tar.gz: c1b6f4332831e572aa1f3adbb010a254ad752c222531849b2891cd740013a997
5
+ SHA512:
6
+ metadata.gz: ef7ee3e8871a0eb7218a7994aefe7922411435c75f112ea85998cc9bfb3be4228ebb0bdf6ebc2ba8de308cad2c03975cbbbf2d65b6f1425998ee358e25ba5252
7
+ data.tar.gz: acaf35418fea782e1e8f1fe1612c0a19668d90c6b3befa00afb638e1998a1b2baead8d32770a94ca2ee0e4c7e0d2077c123912bd684110decff3c58130e545a2
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ `slim-lint-standard` is released under the MIT license.
2
+
3
+ > Copyright (c) 2015 Shane da Silva. http://shane.io
4
+ >
5
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ > of this software and associated documentation files (the "Software"), to deal
7
+ > in the Software without restriction, including without limitation the rights
8
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ > copies of the Software, and to permit persons to whom the Software is
10
+ > furnished to do so, subject to the following conditions:
11
+ >
12
+ > The above copyright notice and this permission notice shall be included in
13
+ > all copies or substantial portions of the Software.
14
+ >
15
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ > SOFTWARE.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'slim_lint/cli'
5
+
6
+ logger = SlimLint::Logger.new(STDOUT)
7
+ exit SlimLint::CLI.new(logger).run(ARGV)
@@ -0,0 +1,109 @@
1
+ # Default application configuration that all configurations inherit from.
2
+ #
3
+ # This is an opinionated list of which hooks are valuable to run and what their
4
+ # out of the box settings should be.
5
+
6
+ # Whether to ignore frontmatter at the beginning of Slim documents for
7
+ # frameworks such as Jekyll/Middleman
8
+ skip_frontmatter: false
9
+
10
+ linters:
11
+ AvoidMultilineExpressions:
12
+ enabled: true
13
+
14
+ CommentControlStatement:
15
+ enabled: true
16
+
17
+ ConsecutiveControlStatements:
18
+ enabled: true
19
+ max_consecutive: 2
20
+
21
+ ControlStatementSpacing:
22
+ enabled: true
23
+ space_after: always
24
+
25
+ DynamicOutputSpacing:
26
+ enabled: true
27
+ space_before: always
28
+ space_after: always
29
+
30
+ EmbeddedEngines:
31
+ enabled: false
32
+ forbidden_engines: []
33
+
34
+ EmptyControlStatement:
35
+ enabled: true
36
+
37
+ EmptyLines:
38
+ enabled: true
39
+
40
+ FileLength:
41
+ enabled: false
42
+ max: 300
43
+
44
+ LineLength:
45
+ enabled: false
46
+ max: 80
47
+
48
+ RedundantDiv:
49
+ enabled: true
50
+
51
+ RuboCop:
52
+ enabled: false
53
+ # These cops are incredibly noisy since the Ruby we extract from Slim
54
+ # templates isn't well-formatted, so we ignore them.
55
+ # WARNING: If you define this list in your own .slim-lint.yml file, you'll
56
+ # be overriding the list defined here.
57
+ # TODO: Narrow this list to essentials.
58
+ ignored_cops:
59
+ - Layout/ArgumentAlignment
60
+ - Layout/ArrayAlignment
61
+ - Layout/BlockAlignment
62
+ - Layout/ClosingParenthesisIndentation
63
+ - Layout/EmptyLineAfterGuardClause
64
+ - Layout/EndAlignment
65
+ - Layout/FirstArgumentIndentation
66
+ - Layout/FirstArrayElementIndentation
67
+ - Layout/FirstHashElementIndentation
68
+ - Layout/FirstParameterIndentation
69
+ - Layout/HashAlignment
70
+ - Layout/IndentationConsistency
71
+ - Layout/IndentationWidth
72
+ - Layout/InitialIndentation
73
+ - Layout/LineEndStringConcatenationIndentation
74
+ - Layout/LineLength
75
+ - Layout/MultilineArrayBraceLayout
76
+ - Layout/MultilineAssignmentLayout
77
+ - Layout/MultilineHashBraceLayout
78
+ - Layout/MultilineMethodCallBraceLayout
79
+ - Layout/MultilineMethodCallIndentation
80
+ - Layout/MultilineMethodDefinitionBraceLayout
81
+ - Layout/MultilineOperationIndentation
82
+ - Layout/ParameterAlignment
83
+ - Layout/TrailingEmptyLines
84
+ - Layout/TrailingWhitespace
85
+ - Lint/Void
86
+ - Metrics/BlockLength
87
+ - Metrics/BlockNesting
88
+ - Naming/FileName
89
+ - Style/FrozenStringLiteralComment
90
+ - Style/IdenticalConditionalBranches
91
+ - Style/IfUnlessModifier
92
+ - Style/Next
93
+ - Style/WhileUntilDo
94
+ - Style/WhileUntilModifier
95
+
96
+ Standard:
97
+ enabled: true
98
+
99
+ Tab:
100
+ enabled: true
101
+
102
+ TagCase:
103
+ enabled: true
104
+
105
+ TrailingBlankLines:
106
+ enabled: true
107
+
108
+ TrailingWhitespace:
109
+ enabled: true
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Represents an atomic, childless, literal value within an S-expression.
5
+ #
6
+ # This creates a light wrapper around literal values of S-expressions so we
7
+ # can make an {Atom} quack like a {Sexp} without being an {Sexp}.
8
+ class Atom
9
+ # Stores the line number of the code in the original document that this Atom
10
+ # came from.
11
+ attr_accessor :value, :start, :finish
12
+
13
+ # Creates an atom from the specified value.
14
+ #
15
+ # @param value [Object]
16
+ def initialize(value, pos:)
17
+ @value = value
18
+
19
+ @start = pos
20
+
21
+ if value.is_a?(String)
22
+ lines = value.count("\n")
23
+ chars = 0
24
+ chars = value.lines.last.size unless value.empty?
25
+ @finish = [pos[0] + lines, chars + (lines == 0 ? pos[1] : 0)]
26
+ end
27
+ end
28
+
29
+ def line
30
+ start[0] if start
31
+ end
32
+
33
+ def location
34
+ SourceLocation.new(
35
+ start_line: start[0],
36
+ start_column: start[1],
37
+ last_line: (finish || start)[0],
38
+ last_column: (finish || start)[1]
39
+ )
40
+ end
41
+
42
+ # Returns whether this atom is equivalent to another object.
43
+ #
44
+ # This defines a helper which unwraps the inner value of the atom to compare
45
+ # against a literal value, saving us having to do it ourselves everywhere
46
+ # else.
47
+ #
48
+ # @param other [Object]
49
+ # @return [Boolean]
50
+ def ==(other)
51
+ @value == (other.is_a?(Atom) ? other.instance_variable_get(:@value) : other)
52
+ end
53
+
54
+ # Returns whether this atom matches the given Sexp pattern.
55
+ #
56
+ # This exists solely to make an {Atom} quack like a {Sexp}, so we don't have
57
+ # to manually check the type when doing comparisons elsewhere.
58
+ #
59
+ # @param [Array, Object]
60
+ # @return [Boolean]
61
+ def match?(pattern)
62
+ # Delegate matching logic if we're comparing against a matcher
63
+ if pattern.is_a?(SlimLint::Matcher::Base)
64
+ return pattern.match?(@value)
65
+ end
66
+
67
+ @value == pattern
68
+ end
69
+
70
+ # Displays the string representation the value this {Atom} wraps.
71
+ #
72
+ # @return [String]
73
+ def to_s
74
+ @value.to_s
75
+ end
76
+
77
+ def to_array
78
+ @value
79
+ end
80
+
81
+ # Displays a string representation of this {Atom} suitable for debugging.
82
+ #
83
+ # @return [String]
84
+ def inspect
85
+ range = +""
86
+ range << start.join(":") if start
87
+ range << " => " if start && finish
88
+ range << finish.join(":") if finish
89
+
90
+ "A(#{range}) #{@value.inspect}"
91
+ end
92
+
93
+ # Redirect methods to the value this {Atom} wraps.
94
+ #
95
+ # Again, this is for convenience so we don't need to manually unwrap the
96
+ # value ourselves. It's pretty magical, but results in much DRYer code.
97
+ #
98
+ # @param method_sym [Symbol] method that was called
99
+ # @param args [Array]
100
+ # @yield block that was passed to the method
101
+ def method_missing(method_sym, *args, &block)
102
+ if @value.respond_to?(method_sym)
103
+ @value.send(method_sym, *args, &block)
104
+ else
105
+ super
106
+ end
107
+ end
108
+
109
+ # @param method_name [String,Symbol] method name
110
+ # @param args [Array]
111
+ def respond_to_missing?(method_name, *args)
112
+ @value.__send__(:respond_to_missing?, method_name, *args) || super
113
+ end
114
+
115
+ # Return whether this {Atom} or the value it wraps responds to the given
116
+ # message.
117
+ #
118
+ # @param method_sym [Symbol]
119
+ # @param include_private [Boolean]
120
+ # @return [Boolean]
121
+ def respond_to?(method_sym, include_private = false)
122
+ if super
123
+ true
124
+ else
125
+ @value.respond_to?(method_sym, include_private)
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Holds the list of captures, providing a convenient interface for accessing
5
+ # the values and unwrapping them on your behalf.
6
+ class CaptureMap < Hash
7
+ # Returns the captured value with the specified name.
8
+ #
9
+ # @param capture_name [Symbol]
10
+ # @return [Object]
11
+ def [](capture_name)
12
+ if key?(capture_name)
13
+ super.value
14
+ else
15
+ raise ArgumentError, "Capture #{capture_name.inspect} does not exist!"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "slim_lint"
4
+ require "slim_lint/options"
5
+
6
+ module SlimLint
7
+ # Command line application interface.
8
+ class CLI
9
+ # Exit codes
10
+ # @see https://man.openbsd.org/sysexits.3
11
+ EX_OK = 0
12
+ EX_USAGE = 64
13
+ EX_DATAERR = 65
14
+ EX_NOINPUT = 67
15
+ EX_SOFTWARE = 70
16
+ EX_CONFIG = 78
17
+
18
+ # Create a CLI that outputs to the specified logger.
19
+ #
20
+ # @param logger [SlimLint::Logger]
21
+ def initialize(logger)
22
+ @log = logger
23
+ end
24
+
25
+ # Parses the given command-line arguments and executes appropriate logic
26
+ # based on those arguments.
27
+ #
28
+ # @param args [Array<String>] command line arguments
29
+ # @return [Integer] exit status code
30
+ def run(args)
31
+ options = SlimLint::Options.new.parse(args)
32
+ act_on_options(options)
33
+ rescue => e
34
+ handle_exception(e)
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :log
40
+
41
+ # Given the provided options, execute the appropriate command.
42
+ #
43
+ # @return [Integer] exit status code
44
+ def act_on_options(options)
45
+ log.color_enabled = options.fetch(:color, log.tty?)
46
+
47
+ if options[:help]
48
+ print_help(options)
49
+ EX_OK
50
+ elsif options[:version] || options[:verbose_version]
51
+ print_version(options)
52
+ EX_OK
53
+ elsif options[:show_linters]
54
+ print_available_linters
55
+ EX_OK
56
+ elsif options[:show_reporters]
57
+ print_available_reporters
58
+ EX_OK
59
+ else
60
+ scan_for_lints(options)
61
+ end
62
+ end
63
+
64
+ # Outputs a message and returns an appropriate error code for the specified
65
+ # exception.
66
+ def handle_exception(exception)
67
+ case exception
68
+ when SlimLint::Exceptions::ConfigurationError
69
+ log.error exception.message
70
+ EX_CONFIG
71
+ when SlimLint::Exceptions::InvalidCLIOption
72
+ log.error exception.message
73
+ log.log "Run `#{APP_NAME}` --help for usage documentation"
74
+ EX_USAGE
75
+ when SlimLint::Exceptions::InvalidFilePath
76
+ log.error exception.message
77
+ EX_NOINPUT
78
+ when SlimLint::Exceptions::NoLintersError
79
+ log.error exception.message
80
+ EX_NOINPUT
81
+ else
82
+ print_unexpected_exception(exception)
83
+ EX_SOFTWARE
84
+ end
85
+ end
86
+
87
+ # Scans the files specified by the given options for lints.
88
+ #
89
+ # @return [Integer] exit status code
90
+ def scan_for_lints(options)
91
+ report = Runner.new.run(options)
92
+ print_report(report, options)
93
+ report.failed? ? EX_DATAERR : EX_OK
94
+ end
95
+
96
+ # Outputs a report of the linter run using the specified reporter.
97
+ def print_report(report, options)
98
+ reporter = options.fetch(:reporter,
99
+ SlimLint::Reporter::DefaultReporter).new(log)
100
+ reporter.display_report(report)
101
+ end
102
+
103
+ # Outputs a list of all currently available linters.
104
+ def print_available_linters
105
+ log.info "Available linters:"
106
+
107
+ linter_names = SlimLint::LinterRegistry.linters.map do |linter|
108
+ linter.name.split("::").last
109
+ end
110
+
111
+ linter_names.sort.each do |linter_name|
112
+ log.log " - #{linter_name}"
113
+ end
114
+ end
115
+
116
+ # Outputs a list of currently available reporters.
117
+ def print_available_reporters
118
+ log.info "Available reporters:"
119
+
120
+ reporter_names = SlimLint::Reporter.descendants.map do |reporter|
121
+ reporter.name.split("::").last.sub(/Reporter$/, "").downcase
122
+ end
123
+
124
+ reporter_names.sort.each do |reporter_name|
125
+ log.log " - #{reporter_name}"
126
+ end
127
+ end
128
+
129
+ # Outputs help documentation.
130
+ def print_help(options)
131
+ log.log options[:help]
132
+ end
133
+
134
+ # Outputs the application name and version.
135
+ def print_version(options)
136
+ log.log "#{SlimLint::APP_NAME} #{SlimLint::VERSION}"
137
+
138
+ if options[:verbose_version]
139
+ log.log "slim #{Gem.loaded_specs["slim"].version}"
140
+ log.log "rubocop #{Gem.loaded_specs["rubocop"].version}"
141
+ log.log RUBY_DESCRIPTION
142
+ end
143
+ end
144
+
145
+ # Outputs the backtrace of an exception with instructions on how to report
146
+ # the issue.
147
+ def print_unexpected_exception(exception)
148
+ log.bold_error exception.message
149
+ log.error exception.backtrace.join("\n")
150
+ log.warning "Report this bug at ", false
151
+ log.info SlimLint::BUG_REPORT_URL
152
+ log.newline
153
+ log.success "To help fix this issue, please include:"
154
+ log.log "- The above stack trace"
155
+ log.log "- slim-lint-standard version: ", false
156
+ log.info SlimLint::VERSION
157
+ log.log "- RuboCop version: ", false
158
+ log.info Gem.loaded_specs["rubocop"].version
159
+ if Gem.loaded_specs["standard"]
160
+ log.log "- Standard version: ", false
161
+ log.info Gem.loaded_specs["standard"].version
162
+ end
163
+ log.log "- Ruby version: ", false
164
+ log.info RUBY_VERSION
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Stores runtime configuration for the application.
5
+ #
6
+ # The purpose of this class is to validate and ensure all configurations
7
+ # satisfy some basic pre-conditions so other parts of the application don't
8
+ # have to check the configuration for errors. It should have no knowledge of
9
+ # how these configuration values are ultimately used.
10
+ class Configuration
11
+ # Internal hash storing the configuration.
12
+ attr_reader :hash
13
+
14
+ # Creates a configuration from the given options hash.
15
+ #
16
+ # @param options [Hash]
17
+ def initialize(options)
18
+ @hash = options
19
+ validate
20
+ end
21
+
22
+ # Access the configuration as if it were a hash.
23
+ #
24
+ # @param key [String]
25
+ # @return [Array,Hash,Number,String]
26
+ def [](key)
27
+ @hash[key]
28
+ end
29
+
30
+ # Compares this configuration with another.
31
+ #
32
+ # @param other [SlimLint::Configuration]
33
+ # @return [true,false] whether the given configuration is equivalent
34
+ def ==(other)
35
+ super || @hash == other.hash
36
+ end
37
+
38
+ # Returns a non-modifiable configuration for the specified linter.
39
+ #
40
+ # @param linter [SlimLint::Linter,Class]
41
+ def for_linter(linter)
42
+ linter_name =
43
+ case linter
44
+ when SlimLint::Linter
45
+ linter.name
46
+ when Class
47
+ linter.name.split("::").last
48
+ when String
49
+ linter.split("::").last
50
+ end
51
+
52
+ @hash["linters"].fetch(linter_name, {}).dup.freeze
53
+ end
54
+
55
+ # Merges the given configuration with this one, returning a new
56
+ # {Configuration}. The provided configuration will either add to or replace
57
+ # any options defined in this configuration.
58
+ #
59
+ # @param config [SlimLint::Configuration]
60
+ def merge(config)
61
+ self.class.new(smart_merge(@hash, config.hash))
62
+ end
63
+
64
+ private
65
+
66
+ # Merge two hashes such that nested hashes are merged rather than replaced.
67
+ #
68
+ # @param parent [Hash]
69
+ # @param child [Hash]
70
+ # @return [Hash]
71
+ def smart_merge(parent, child)
72
+ parent.merge(child) do |_key, old, new|
73
+ case old
74
+ when Hash
75
+ smart_merge(old, new)
76
+ else
77
+ new
78
+ end
79
+ end
80
+ end
81
+
82
+ # Validates the configuration for any invalid options, normalizing it where
83
+ # possible.
84
+ def validate
85
+ ensure_exclude_option_array_exists
86
+ ensure_linter_section_exists
87
+ ensure_linter_include_exclude_arrays_exist
88
+ end
89
+
90
+ # Ensures the `exclude` global option is an array.
91
+ def ensure_exclude_option_array_exists
92
+ @hash["exclude"] = Array(@hash["exclude"])
93
+ end
94
+
95
+ # Ensures the `linters` configuration section exists.
96
+ def ensure_linter_section_exists
97
+ @hash["linters"] ||= {}
98
+ end
99
+
100
+ # Ensure `include` and `exclude` options for linters are arrays
101
+ # (since users can specify a single string glob pattern for convenience)
102
+ def ensure_linter_include_exclude_arrays_exist
103
+ @hash["linters"].each_key do |linter_name|
104
+ %w[include exclude].each do |option|
105
+ linter_config = @hash["linters"][linter_name]
106
+ linter_config[option] = Array(linter_config[option])
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "yaml"
5
+
6
+ module SlimLint
7
+ # Manages configuration file loading.
8
+ class ConfigurationLoader
9
+ DEFAULT_CONFIG_PATH = File.join(SlimLint::HOME, "config", "default.yml").freeze
10
+ CONFIG_FILE_NAME = ".slim-lint.yml"
11
+
12
+ class << self
13
+ # Load configuration file given the current working directory the
14
+ # application is running within.
15
+ def load_applicable_config
16
+ directory = File.expand_path(Dir.pwd)
17
+ config_file = possible_config_files(directory).find(&:file?)
18
+
19
+ if config_file
20
+ load_file(config_file.to_path)
21
+ else
22
+ default_configuration
23
+ end
24
+ end
25
+
26
+ # Loads the built-in default configuration.
27
+ def default_configuration
28
+ @default_configuration ||= load_from_file(DEFAULT_CONFIG_PATH)
29
+ end
30
+
31
+ # Loads a configuration, ensuring it extends the default configuration.
32
+ #
33
+ # @param file [String]
34
+ # @return [SlimLint::Configuration]
35
+ def load_file(file)
36
+ config = load_from_file(file)
37
+
38
+ default_configuration.merge(config)
39
+ rescue Psych::SyntaxError, Errno::ENOENT => e
40
+ raise SlimLint::Exceptions::ConfigurationError,
41
+ "Unable to load configuration from '#{file}': #{e}",
42
+ e.backtrace
43
+ end
44
+
45
+ # Creates a configuration from the specified hash, ensuring it extends the
46
+ # default configuration.
47
+ #
48
+ # @param hash [Hash]
49
+ # @return [SlimLint::Configuration]
50
+ def load_hash(hash)
51
+ config = SlimLint::Configuration.new(hash)
52
+
53
+ default_configuration.merge(config)
54
+ end
55
+
56
+ private
57
+
58
+ # Parses and loads a configuration from the given file.
59
+ #
60
+ # @param file [String]
61
+ # @return [SlimLint::Configuration]
62
+ def load_from_file(file)
63
+ hash =
64
+ if (yaml = YAML.load_file(file))
65
+ yaml.to_hash
66
+ else
67
+ {}
68
+ end
69
+
70
+ SlimLint::Configuration.new(hash)
71
+ end
72
+
73
+ # Returns a list of possible configuration files given the context of the
74
+ # specified directory.
75
+ #
76
+ # @param directory [String]
77
+ # @return [Array<Pathname>]
78
+ def possible_config_files(directory)
79
+ files = Pathname.new(directory)
80
+ .enum_for(:ascend)
81
+ .map { |path| path + CONFIG_FILE_NAME }
82
+ files << Pathname.new(CONFIG_FILE_NAME)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Global application constants.
4
+ module SlimLint
5
+ HOME = File.expand_path(File.join(File.dirname(__FILE__), "..", "..")).freeze
6
+ APP_NAME = "slim-lint-standard"
7
+
8
+ REPO_URL = "https://github.com/pvande/slim-lint-standard"
9
+ BUG_REPORT_URL = "#{REPO_URL}/issues"
10
+ end