├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .mailmap ├── .overcommit.yml ├── .projections.json ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .simplecov ├── CHANGELOG.md ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── bin └── scss-lint ├── config └── default.yml ├── data ├── prefixed-identifiers │ ├── base.txt │ └── bourbon.txt ├── properties.txt ├── property-sort-orders │ ├── concentric.txt │ ├── recess.txt │ └── smacss.txt └── pseudo-elements.txt ├── lib ├── scss_lint.rb └── scss_lint │ ├── cli.rb │ ├── config.rb │ ├── constants.rb │ ├── control_comment_processor.rb │ ├── engine.rb │ ├── exceptions.rb │ ├── file_finder.rb │ ├── lint.rb │ ├── linter.rb │ ├── linter │ ├── README.md │ ├── bang_format.rb │ ├── bem_depth.rb │ ├── border_zero.rb │ ├── chained_classes.rb │ ├── color_keyword.rb │ ├── color_variable.rb │ ├── comment.rb │ ├── compass.rb │ ├── compass │ │ ├── README.md │ │ └── property_with_mixin.rb │ ├── debug_statement.rb │ ├── declaration_order.rb │ ├── disable_linter_reason.rb │ ├── duplicate_property.rb │ ├── else_placement.rb │ ├── empty_line_between_blocks.rb │ ├── empty_rule.rb │ ├── encoding.rb │ ├── extend_directive.rb │ ├── final_newline.rb │ ├── hex_length.rb │ ├── hex_notation.rb │ ├── hex_validation.rb │ ├── id_selector.rb │ ├── import_path.rb │ ├── important_rule.rb │ ├── indentation.rb │ ├── leading_zero.rb │ ├── length_variable.rb │ ├── mergeable_selector.rb │ ├── name_format.rb │ ├── nesting_depth.rb │ ├── placeholder_in_extend.rb │ ├── private_naming_convention.rb │ ├── property_count.rb │ ├── property_sort_order.rb │ ├── property_spelling.rb │ ├── property_units.rb │ ├── pseudo_element.rb │ ├── qualifying_element.rb │ ├── selector_depth.rb │ ├── selector_format.rb │ ├── shorthand.rb │ ├── single_line_per_property.rb │ ├── single_line_per_selector.rb │ ├── space_after_comma.rb │ ├── space_after_comment.rb │ ├── space_after_property_colon.rb │ ├── space_after_property_name.rb │ ├── space_after_variable_colon.rb │ ├── space_after_variable_name.rb │ ├── space_around_operator.rb │ ├── space_before_brace.rb │ ├── space_between_parens.rb │ ├── string_quotes.rb │ ├── syntax.rb │ ├── trailing_semicolon.rb │ ├── trailing_whitespace.rb │ ├── trailing_zero.rb │ ├── transition_all.rb │ ├── unnecessary_mantissa.rb │ ├── unnecessary_parent_reference.rb │ ├── url_format.rb │ ├── url_quotes.rb │ ├── variable_for_property.rb │ ├── vendor_prefix.rb │ └── zero_unit.rb │ ├── linter_registry.rb │ ├── location.rb │ ├── logger.rb │ ├── options.rb │ ├── plugins.rb │ ├── plugins │ ├── linter_dir.rb │ └── linter_gem.rb │ ├── rake_task.rb │ ├── reporter.rb │ ├── reporter │ ├── clean_files_reporter.rb │ ├── config_reporter.rb │ ├── default_reporter.rb │ ├── files_reporter.rb │ ├── json_reporter.rb │ ├── stats_reporter.rb │ └── tap_reporter.rb │ ├── runner.rb │ ├── sass │ ├── script.rb │ └── tree.rb │ ├── selector_visitor.rb │ ├── utils.rb │ └── version.rb ├── logo └── horizontal.png ├── scss_lint.gemspec └── spec ├── scss_lint ├── cli_spec.rb ├── config_spec.rb ├── engine_spec.rb ├── file_finder_spec.rb ├── fixtures │ └── plugins │ │ └── linter_plugin.rb ├── linter │ ├── bang_format_spec.rb │ ├── bem_depth_spec.rb │ ├── border_zero_spec.rb │ ├── chained_classes_spec.rb │ ├── color_keyword_spec.rb │ ├── color_variable_spec.rb │ ├── comment_spec.rb │ ├── compass │ │ └── property_with_mixin_spec.rb │ ├── debug_statement_spec.rb │ ├── declaration_order_spec.rb │ ├── disable_linter_reason_spec.rb │ ├── duplicate_property_spec.rb │ ├── else_placement_spec.rb │ ├── empty_line_between_blocks_spec.rb │ ├── empty_rule_spec.rb │ ├── extend_directive_spec.rb │ ├── final_newline_spec.rb │ ├── hex_length_spec.rb │ ├── hex_notation_spec.rb │ ├── hex_validation_spec.rb │ ├── id_selector_spec.rb │ ├── import_path_spec.rb │ ├── important_rule_spec.rb │ ├── indentation_spec.rb │ ├── leading_zero_spec.rb │ ├── length_variable_spec.rb │ ├── mergeable_selector_spec.rb │ ├── name_format_spec.rb │ ├── nesting_depth_spec.rb │ ├── placeholder_in_extend_spec.rb │ ├── private_naming_convention_spec.rb │ ├── property_count_spec.rb │ ├── property_sort_order_spec.rb │ ├── property_spelling_spec.rb │ ├── property_units_spec.rb │ ├── pseudo_element_spec.rb │ ├── qualifying_element_spec.rb │ ├── selector_depth_spec.rb │ ├── selector_format_spec.rb │ ├── shorthand_spec.rb │ ├── single_line_per_property_spec.rb │ ├── single_line_per_selector_spec.rb │ ├── space_after_comma_spec.rb │ ├── space_after_comment_spec.rb │ ├── space_after_property_colon_spec.rb │ ├── space_after_property_name_spec.rb │ ├── space_after_variable_colon_spec.rb │ ├── space_after_variable_name_spec.rb │ ├── space_around_operator_spec.rb │ ├── space_before_brace_spec.rb │ ├── space_between_parens_spec.rb │ ├── string_quotes_spec.rb │ ├── trailing_semicolon_spec.rb │ ├── trailing_whitespace_spec.rb │ ├── trailing_zero_spec.rb │ ├── transition_all_spec.rb │ ├── unnecessary_mantissa_spec.rb │ ├── unnecessary_parent_reference_spec.rb │ ├── url_format_spec.rb │ ├── url_quotes_spec.rb │ ├── variable_for_property_spec.rb │ ├── vendor_prefix_spec.rb │ └── zero_unit_spec.rb ├── linter_registry_spec.rb ├── linter_spec.rb ├── location_spec.rb ├── logger_spec.rb ├── options_spec.rb ├── plugins │ ├── linter_dir_spec.rb │ └── linter_gem_spec.rb ├── plugins_spec.rb ├── preprocess_spec.rb ├── rake_task_spec.rb ├── report_lint_spec.rb ├── reporter │ ├── clean_files_reporter_spec.rb │ ├── config_reporter_spec.rb │ ├── default_reporter_spec.rb │ ├── files_reporter_spec.rb │ ├── json_reporter_spec.rb │ ├── stats_reporter_spec.rb │ └── tap_reporter_spec.rb ├── reporter_spec.rb ├── runner_spec.rb └── selector_visitor_spec.rb ├── spec_helper.rb └── support ├── isolated_environment.rb └── matchers └── report_lint.rb /.editorconfig: -------------------------------------------------------------------------------- 1 | # Defines the coding style for different editors and IDEs. 2 | # http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | types: [synchronize, opened, reopened, ready_for_review] 8 | 9 | jobs: 10 | ci: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | ruby-version: ['3.0', '3.1', '3.2', '3.3', jruby] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up Ruby ${{ matrix.ruby-version }} 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: ${{ matrix.ruby-version }} 23 | bundler-cache: true 24 | - name: Run tests 25 | run: bundle exec rspec 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | Gemfile.lock 3 | /coverage 4 | /pkg 5 | 6 | # YARD 7 | /doc 8 | /.yardoc 9 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | David Clark unknown 2 | Joe Lencioni Joe Lencioni 3 | Shane da Silva Shane da Silva 4 | Shane da Silva Shane da Silva 5 | -------------------------------------------------------------------------------- /.overcommit.yml: -------------------------------------------------------------------------------- 1 | # Run Overcommit within a Bundler context using this repo's Gemfile 2 | gemfile: Gemfile 3 | 4 | PreCommit: 5 | BundleCheck: 6 | enabled: true 7 | 8 | ExecutePermissions: 9 | enabled: true 10 | exclude: 11 | - 'bin/scss-lint' 12 | 13 | HardTabs: 14 | enabled: true 15 | 16 | RuboCop: 17 | enabled: true 18 | 19 | TrailingWhitespace: 20 | enabled: true 21 | 22 | YamlSyntax: 23 | enabled: true 24 | -------------------------------------------------------------------------------- /.projections.json: -------------------------------------------------------------------------------- 1 | { 2 | "lib/scss_lint/*.rb": { 3 | "alternate": "spec/scss_lint/{}_spec.rb", 4 | "type": "source", 5 | "dispatch": "bundle exec rspec spec/scss_lint/{}_spec.rb" 6 | }, 7 | "spec/scss_lint/*_spec.rb": { 8 | "alternate": "lib/scss_lint/{}.rb", 9 | "type": "test", 10 | "dispatch": "bundle exec rspec {file}" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2024-01-16 23:49:35 UTC using RuboCop version 1.60.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 26 10 | # Configuration parameters: AllowedMethods. 11 | # AllowedMethods: enums 12 | Lint/ConstantDefinitionInBlock: 13 | Exclude: 14 | - 'spec/scss_lint/cli_spec.rb' 15 | - 'spec/scss_lint/config_spec.rb' 16 | - 'spec/scss_lint/linter_registry_spec.rb' 17 | - 'spec/scss_lint/linter_spec.rb' 18 | - 'spec/scss_lint/reporter/stats_reporter_spec.rb' 19 | - 'spec/scss_lint/reporter_spec.rb' 20 | - 'spec/scss_lint/runner_spec.rb' 21 | - 'spec/scss_lint/selector_visitor_spec.rb' 22 | 23 | # Offense count: 1 24 | Lint/FormatParameterMismatch: 25 | Exclude: 26 | - 'lib/scss_lint/reporter/stats_reporter.rb' 27 | 28 | # Offense count: 3 29 | # Configuration parameters: AllowedParentClasses. 30 | Lint/MissingSuper: 31 | Exclude: 32 | - 'lib/scss_lint/linter.rb' 33 | - 'lib/scss_lint/rake_task.rb' 34 | 35 | # Offense count: 3 36 | Lint/MixedRegexpCaptureTypes: 37 | Exclude: 38 | - 'lib/scss_lint/control_comment_processor.rb' 39 | - 'lib/scss_lint/linter/disable_linter_reason.rb' 40 | - 'lib/scss_lint/linter/property_sort_order.rb' 41 | 42 | # Offense count: 1 43 | Style/ClassVars: 44 | Exclude: 45 | - 'lib/scss_lint/selector_visitor.rb' 46 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | SimpleCov.start do 2 | add_filter '/bin/' 3 | add_filter '/spec/' 4 | end 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'rspec', '~> 3.8' 6 | 7 | # Run all pre-commit checks via Overcommit during CI runs 8 | gem 'overcommit', '0.62.0' 9 | 10 | # Needed to test Rake integration in specs 11 | gem 'rake' 12 | 13 | # Pin tool versions (which are executed by Overcommit) for Travis builds 14 | gem 'rubocop', '1.60.0' 15 | 16 | gem 'coveralls', require: false 17 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Shane da Silva 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | -------------------------------------------------------------------------------- /bin/scss-lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'scss_lint' 4 | require 'scss_lint/cli' 5 | 6 | logger = SCSSLint::Logger.new($stdout) 7 | exit SCSSLint::CLI.new(logger).run(ARGV) 8 | -------------------------------------------------------------------------------- /data/prefixed-identifiers/base.txt: -------------------------------------------------------------------------------- 1 | # Based on Autoprefixer --info 2 | 3 | # At-Rules 4 | keyframes 5 | 6 | # Selectors 7 | selection 8 | placeholder 9 | fullscreen 10 | 11 | # Properties 12 | transition 13 | transition-property 14 | border-radius 15 | border-top-left-radius 16 | border-top-right-radius 17 | border-bottom-right-radius 18 | border-bottom-left-radius 19 | box-shadow 20 | animation 21 | animation-name 22 | animation-duration 23 | animation-delay 24 | animation-direction 25 | animation-fill-mode 26 | animation-iteration-count 27 | animation-play-state 28 | animation-timing-function 29 | transition-duration 30 | transition-delay 31 | transition-timing-function 32 | transform 33 | transform-origin 34 | perspective 35 | perspective-origin 36 | transform-style 37 | backface-visibility 38 | border-image 39 | box-sizing 40 | filter 41 | columns 42 | column-width 43 | column-gap 44 | column-rule 45 | column-rule-color 46 | column-rule-width 47 | column-count 48 | column-rule-style 49 | column-span 50 | column-fill 51 | break-before 52 | break-after 53 | break-inside 54 | user-select 55 | flex 56 | flex-grow 57 | flex-shrink 58 | flex-basis 59 | flex-direction 60 | flex-wrap 61 | flex-flow 62 | justify-content 63 | order 64 | align-items 65 | align-self 66 | align-content 67 | background-clip 68 | background-origin 69 | background-size 70 | font-feature-settings 71 | font-variant-ligatures 72 | font-language-override 73 | font-kerning 74 | hyphens 75 | tab-size 76 | touch-action 77 | text-decoration-style 78 | text-decoration-line 79 | text-decoration-color 80 | text-size-adjust 81 | clip-path 82 | mask 83 | mask-clip 84 | mask-composite 85 | mask-image 86 | mask-origin 87 | mask-position 88 | mask-repeat 89 | mask-size 90 | 91 | # Values 92 | linear-gradient 93 | repeating-linear-gradient 94 | radial-gradient 95 | repeating-radial-gradient 96 | flex 97 | inline-flex 98 | calc 99 | max-content 100 | min-content 101 | fit-content 102 | fill-available 103 | zoom-in 104 | zoom-out 105 | grab 106 | grabbing 107 | sticky 108 | -------------------------------------------------------------------------------- /data/prefixed-identifiers/bourbon.txt: -------------------------------------------------------------------------------- 1 | # Identifiers covered by Bourbon mixins 2 | 3 | # At-Rules 4 | keyframes 5 | 6 | # Selectors 7 | placeholder 8 | 9 | # Properties 10 | animation 11 | animation-delay 12 | animation-direction 13 | animation-duration 14 | animation-fill-mode 15 | animation-iteration-count 16 | animation-name 17 | animation-play-state 18 | animation-timing-function 19 | appearance 20 | backface-visibility 21 | background 22 | border-image 23 | border-top-left-radius 24 | border-top-right-radius 25 | border-bottom-right-radius 26 | border-bottom-left-radius 27 | box-sizing 28 | calc 29 | columns 30 | column-width 31 | column-gap 32 | column-rule 33 | column-rule-color 34 | column-rule-width 35 | column-count 36 | column-rule-style 37 | column-span 38 | column-fill 39 | filter 40 | flex 41 | flex-grow 42 | flex-shrink 43 | flex-basis 44 | flex-direction 45 | flex-wrap 46 | flex-flow 47 | justify-content 48 | order 49 | align-items 50 | align-self 51 | align-content 52 | font-feature-settings 53 | hyphens 54 | perspective 55 | perspective-origin 56 | transform 57 | transform-origin 58 | transform-style 59 | transition 60 | transition-property 61 | transition-duration 62 | transition-delay 63 | transition-timing-function 64 | user-select 65 | 66 | 67 | # Values 68 | crisp-edges 69 | optimize-contrast 70 | linear-gradient 71 | radial-gradient 72 | -------------------------------------------------------------------------------- /data/property-sort-orders/concentric.txt: -------------------------------------------------------------------------------- 1 | # Concentric CSS 2 | # http://rhodesmill.org/brandon/2011/concentric-css/ 3 | 4 | display 5 | position 6 | top 7 | right 8 | bottom 9 | left 10 | 11 | flex 12 | flex-basis 13 | flex-direction 14 | flex-flow 15 | flex-grow 16 | flex-shrink 17 | flex-wrap 18 | align-content 19 | align-items 20 | align-self 21 | justify-content 22 | order 23 | 24 | columns 25 | column-gap 26 | column-fill 27 | column-rule 28 | column-span 29 | column-count 30 | column-width 31 | 32 | float 33 | clear 34 | 35 | transform 36 | transform-origin 37 | transition 38 | 39 | animation 40 | animation-name 41 | animation-duration 42 | animation-timing-function 43 | animation-delay 44 | animation-iteration-count 45 | animation-direction 46 | animation-fill-mode 47 | animation-play-state 48 | 49 | visibility 50 | opacity 51 | z-index 52 | 53 | margin 54 | margin-top 55 | margin-right 56 | margin-bottom 57 | margin-left 58 | 59 | outline 60 | outline-offset 61 | outline-width 62 | outline-style 63 | outline-color 64 | 65 | border 66 | border-top 67 | border-right 68 | border-bottom 69 | border-left 70 | border-width 71 | border-top-width 72 | border-right-width 73 | border-bottom-width 74 | border-left-width 75 | 76 | border-style 77 | border-top-style 78 | border-right-style 79 | border-bottom-style 80 | border-left-style 81 | 82 | border-radius 83 | border-top-left-radius 84 | border-top-right-radius 85 | border-bottom-left-radius 86 | border-bottom-right-radius 87 | 88 | border-color 89 | border-top-color 90 | border-right-color 91 | border-bottom-color 92 | border-left-color 93 | 94 | box-shadow 95 | 96 | background 97 | background-attachment 98 | background-clip 99 | background-color 100 | background-image 101 | background-repeat 102 | background-position 103 | background-size 104 | cursor 105 | 106 | padding 107 | padding-top 108 | padding-right 109 | padding-bottom 110 | padding-left 111 | 112 | width 113 | min-width 114 | max-width 115 | 116 | height 117 | min-height 118 | max-height 119 | 120 | overflow 121 | 122 | list-style 123 | caption-side 124 | 125 | table-layout 126 | border-collapse 127 | border-spacing 128 | empty-cells 129 | 130 | vertical-align 131 | 132 | text-align 133 | text-indent 134 | text-transform 135 | text-decoration 136 | text-rendering 137 | text-shadow 138 | text-overflow 139 | 140 | line-height 141 | word-break 142 | word-wrap 143 | word-spacing 144 | letter-spacing 145 | white-space 146 | color 147 | 148 | font 149 | font-family 150 | font-size 151 | font-weight 152 | font-smoothing 153 | font-style 154 | 155 | content 156 | quotes 157 | -------------------------------------------------------------------------------- /data/property-sort-orders/recess.txt: -------------------------------------------------------------------------------- 1 | # RECESS Property Order 2 | # https://github.com/twitter/recess 3 | 4 | position 5 | top 6 | right 7 | bottom 8 | left 9 | z-index 10 | display 11 | align-content 12 | align-items 13 | align-self 14 | flex 15 | flex-basis 16 | flex-direction 17 | flex-flow 18 | flex-grow 19 | flex-shrink 20 | flex-wrap 21 | justify-content 22 | order 23 | float 24 | width 25 | height 26 | max-width 27 | max-height 28 | min-width 29 | min-height 30 | padding 31 | padding-top 32 | padding-right 33 | padding-bottom 34 | padding-left 35 | margin 36 | margin-top 37 | margin-right 38 | margin-bottom 39 | margin-left 40 | margin-collapse 41 | margin-top-collapse 42 | margin-right-collapse 43 | margin-bottom-collapse 44 | margin-left-collapse 45 | overflow 46 | overflow-x 47 | overflow-y 48 | clip 49 | clear 50 | font 51 | font-family 52 | font-size 53 | font-smoothing 54 | osx-font-smoothing 55 | font-style 56 | font-weight 57 | hyphens 58 | src 59 | line-height 60 | letter-spacing 61 | word-spacing 62 | color 63 | text-align 64 | text-decoration 65 | text-indent 66 | text-overflow 67 | text-rendering 68 | text-size-adjust 69 | text-shadow 70 | text-transform 71 | word-break 72 | word-wrap 73 | white-space 74 | vertical-align 75 | list-style 76 | list-style-type 77 | list-style-position 78 | list-style-image 79 | pointer-events 80 | cursor 81 | background 82 | background-attachment 83 | background-clip 84 | background-color 85 | background-image 86 | background-position 87 | background-repeat 88 | background-size 89 | border 90 | border-collapse 91 | border-top 92 | border-right 93 | border-bottom 94 | border-left 95 | border-color 96 | border-image 97 | border-top-color 98 | border-right-color 99 | border-bottom-color 100 | border-left-color 101 | border-spacing 102 | border-style 103 | border-top-style 104 | border-right-style 105 | border-bottom-style 106 | border-left-style 107 | border-width 108 | border-top-width 109 | border-right-width 110 | border-bottom-width 111 | border-left-width 112 | border-radius 113 | border-top-right-radius 114 | border-bottom-right-radius 115 | border-bottom-left-radius 116 | border-top-left-radius 117 | border-radius-topright 118 | border-radius-bottomright 119 | border-radius-bottomleft 120 | border-radius-topleft 121 | content 122 | quotes 123 | outline 124 | outline-offset 125 | outline-width 126 | outline-style 127 | outline-color 128 | opacity 129 | filter 130 | visibility 131 | size 132 | zoom 133 | transform 134 | box-align 135 | box-flex 136 | box-orient 137 | box-pack 138 | box-shadow 139 | box-sizing 140 | table-layout 141 | animation 142 | animation-delay 143 | animation-duration 144 | animation-iteration-count 145 | animation-name 146 | animation-play-state 147 | animation-timing-function 148 | animation-fill-mode 149 | transition 150 | transition-delay 151 | transition-duration 152 | transition-property 153 | transition-timing-function 154 | backface-visibility 155 | resize 156 | appearance 157 | user-select 158 | interpolation-mode 159 | direction 160 | marks 161 | page 162 | set-link-source 163 | unicode-bidi 164 | speak 165 | -------------------------------------------------------------------------------- /lib/scss_lint.rb: -------------------------------------------------------------------------------- 1 | require 'scss_lint/constants' 2 | require 'scss_lint/exceptions' 3 | require 'scss_lint/config' 4 | require 'scss_lint/engine' 5 | require 'scss_lint/location' 6 | require 'scss_lint/lint' 7 | require 'scss_lint/linter_registry' 8 | require 'scss_lint/logger' 9 | require 'scss_lint/file_finder' 10 | require 'scss_lint/runner' 11 | require 'scss_lint/selector_visitor' 12 | require 'scss_lint/control_comment_processor' 13 | require 'scss_lint/version' 14 | require 'scss_lint/utils' 15 | require 'scss_lint/plugins' 16 | 17 | # Load Sass classes and then monkey patch them 18 | require 'sass' 19 | require File.expand_path('scss_lint/sass/script', File.dirname(__FILE__)) 20 | require File.expand_path('scss_lint/sass/tree', File.dirname(__FILE__)) 21 | 22 | # Load all linters in sorted order, since ordering matters and some systems 23 | # return the files in an order which loads a child class before the parent. 24 | require 'scss_lint/linter' 25 | Dir[File.expand_path('scss_lint/linter/**/*.rb', File.dirname(__FILE__))].sort.each do |file| 26 | require file 27 | end 28 | 29 | # Load all reporters 30 | require 'scss_lint/reporter' 31 | Dir[File.expand_path('scss_lint/reporter/**/*.rb', File.dirname(__FILE__))].sort.each do |file| 32 | require file 33 | end 34 | -------------------------------------------------------------------------------- /lib/scss_lint/constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Global application constants. 4 | module SCSSLint 5 | SCSS_LINT_HOME = File.realpath(File.join(File.dirname(__FILE__), '..', '..')).freeze 6 | SCSS_LINT_DATA = File.join(SCSS_LINT_HOME, 'data').freeze 7 | 8 | REPO_URL = 'https://github.com/sds/scss-lint'.freeze 9 | BUG_REPORT_URL = "#{REPO_URL}/issues".freeze 10 | end 11 | -------------------------------------------------------------------------------- /lib/scss_lint/exceptions.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint::Exceptions 2 | # Raised when an invalid flag is given via the command line. 3 | class InvalidCLIOption < StandardError; end 4 | 5 | # Raised when the configuration file is invalid for some reason. 6 | class InvalidConfiguration < StandardError; end 7 | 8 | # Raised when an unexpected error occurs in a linter 9 | class LinterError < StandardError; end 10 | 11 | # Raised when no files were specified or specified glob patterns did not match 12 | # any files. 13 | class NoFilesError < StandardError; end 14 | 15 | # Raised when a required library (specified via command line) does not exist. 16 | class RequiredLibraryMissingError < StandardError; end 17 | 18 | # Raised when a linter gem plugin is required but not installed. 19 | class PluginGemLoadError < StandardError; end 20 | 21 | # Raised when the preprocessor tool exits with a non-zero code. 22 | class PreprocessorError < StandardError; end 23 | end 24 | -------------------------------------------------------------------------------- /lib/scss_lint/file_finder.rb: -------------------------------------------------------------------------------- 1 | require 'find' 2 | 3 | module SCSSLint 4 | # Finds all SCSS files that should be linted given a set of paths, globs, and 5 | # configuration. 6 | class FileFinder 7 | # List of extensions of files to include when only a directory is specified 8 | # as a path. 9 | VALID_EXTENSIONS = %w[.css .scss].freeze 10 | 11 | # Create a {FileFinder}. 12 | # 13 | # @param config [SCSSLint::Config] 14 | def initialize(config) 15 | @config = config 16 | end 17 | 18 | # Find all files that match given the specified options. 19 | # 20 | # @param patterns [Array] a list of file paths and glob patterns 21 | def find(patterns) 22 | if patterns.empty? 23 | raise SCSSLint::Exceptions::NoFilesError, 24 | 'No files, paths, or patterns were specified' 25 | end 26 | 27 | matched_files = extract_files_from(patterns) 28 | if matched_files.empty? 29 | raise SCSSLint::Exceptions::NoFilesError, 30 | "No SCSS files matched by the patterns: #{patterns.join(' ')}" 31 | end 32 | 33 | matched_files.reject { |file| @config.excluded_file?(file) } 34 | end 35 | 36 | private 37 | 38 | # @param list [Array] 39 | def extract_files_from(list) 40 | files = [] 41 | 42 | list.each do |file| 43 | if File.directory?(file) 44 | Find.find(file) do |f| 45 | files << f if scssish_file?(f) 46 | end 47 | else 48 | files << file # Otherwise include file as-is 49 | end 50 | end 51 | 52 | files.uniq.sort 53 | end 54 | 55 | # @param file [String] 56 | # @return [true,false] 57 | def scssish_file?(file) 58 | return false unless FileTest.file?(file) 59 | 60 | VALID_EXTENSIONS.include?(File.extname(file)) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/scss_lint/lint.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Stores information about a single problem that was detected by a [Linter]. 3 | class Lint 4 | attr_reader :linter, :filename, :location, :description, :severity 5 | 6 | # @param linter [SCSSLint::Linter] 7 | # @param filename [String] 8 | # @param location [SCSSLint::Location] 9 | # @param description [String] 10 | # @param severity [Symbol] 11 | def initialize(linter, filename, location, description, severity = :warning) 12 | @linter = linter 13 | @filename = filename 14 | @location = location 15 | @description = description 16 | @severity = severity 17 | end 18 | 19 | # @return [Boolean] 20 | def error? 21 | severity == :error 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/bang_format.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks spacing of ! declarations, like !important and !default 3 | class Linter::BangFormat < Linter 4 | include LinterRegistry 5 | 6 | STOPPING_CHARACTERS = ['!', "'", '"', nil].freeze 7 | 8 | def visit_extend(node) 9 | check_bang(node) 10 | end 11 | 12 | def visit_prop(node) 13 | check_bang(node) 14 | end 15 | 16 | def visit_variable(node) 17 | check_bang(node) 18 | end 19 | 20 | private 21 | 22 | def check_bang(node) 23 | range = if node.respond_to?(:value_source_range) 24 | node.value_source_range 25 | else 26 | node.source_range 27 | end 28 | return unless source_from_range(range).include?('!') 29 | return unless check_spacing(range) 30 | 31 | before_qualifier = config['space_before_bang'] ? '' : 'not ' 32 | after_qualifier = config['space_after_bang'] ? '' : 'not ' 33 | 34 | add_lint(node, "! should #{before_qualifier}be preceded by a space, " \ 35 | "and should #{after_qualifier}be followed by a space") 36 | end 37 | 38 | # Start from the back and move towards the front so that any !important or 39 | # !default !'s will be found *before* quotation marks. Then we can 40 | # stop at quotation marks to protect against linting !'s within strings 41 | # (e.g. `content`) 42 | def find_bang_offset(range) 43 | offset = 0 44 | offset -= 1 until STOPPING_CHARACTERS.include?(character_at(range.end_pos, offset)) 45 | offset 46 | end 47 | 48 | def is_before_wrong?(range, offset) 49 | before_expected = config['space_before_bang'] ? / / : /[^ ]/ 50 | before_actual = character_at(range.end_pos, offset - 1) 51 | (before_actual =~ before_expected).nil? 52 | end 53 | 54 | def is_after_wrong?(range, offset) 55 | after_expected = config['space_after_bang'] ? / / : /[^ ]/ 56 | after_actual = character_at(range.end_pos, offset + 1) 57 | (after_actual =~ after_expected).nil? 58 | end 59 | 60 | def check_spacing(range) 61 | offset = find_bang_offset(range) 62 | 63 | return if character_at(range.end_pos, offset) != '!' 64 | 65 | is_before_wrong?(range, offset) || is_after_wrong?(range, offset) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/bem_depth.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for BEM selectors with more elements than a specified maximum number. 3 | class Linter::BemDepth < Linter 4 | include LinterRegistry 5 | 6 | def visit_root(_node) 7 | @max_elements = config['max_elements'] 8 | yield # Continue linting children 9 | end 10 | 11 | def visit_class(klass) 12 | check_depth(klass, 'selectors') 13 | end 14 | 15 | def visit_placeholder(placeholder) 16 | check_depth(placeholder, 'placeholders') 17 | end 18 | 19 | private 20 | 21 | def check_depth(node, plural_type) 22 | selector = node.name 23 | parts = selector.split('__') 24 | num_elements = (parts[1..-1] || []).length 25 | return if num_elements <= @max_elements 26 | 27 | found_elements = pluralize(@max_elements, 'element') 28 | add_lint(node, "BEM #{plural_type} should have no more than #{found_elements}, " \ 29 | "but `#{selector}` has #{num_elements}") 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/border_zero.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Enforce a particular value for empty borders. 3 | class Linter::BorderZero < Linter 4 | include LinterRegistry 5 | 6 | CONVENTION_TO_PREFERENCE = { 7 | 'zero' => %w[0 none], 8 | 'none' => %w[none 0], 9 | }.freeze 10 | 11 | BORDER_PROPERTIES = %w[ 12 | border 13 | border-top 14 | border-right 15 | border-bottom 16 | border-left 17 | ].freeze 18 | 19 | def visit_root(_node) 20 | @preference = CONVENTION_TO_PREFERENCE[config['convention'].to_s] 21 | unless @preference 22 | raise "Invalid `convention` specified: #{config['convention']}." \ 23 | "Must be one of [#{CONVENTION_TO_PREFERENCE.keys.join(', ')}]" 24 | end 25 | yield # Continue linting children 26 | end 27 | 28 | def visit_prop(node) 29 | return unless BORDER_PROPERTIES.include?(node.name.first.to_s) 30 | check_border(node, node.name.first.to_s, node.value.first.to_sass.strip) 31 | end 32 | 33 | private 34 | 35 | def check_border(node, border_property, border_value) 36 | return unless %w[0 none].include?(border_value) 37 | return if @preference[0] == border_value 38 | 39 | add_lint(node, "`#{border_property}: #{@preference[0]}` is preferred over " \ 40 | "`#{border_property}: #{@preference[1]}`") 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/chained_classes.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for uses of chained classes (e.g. .foo.bar). 3 | class Linter::ChainedClasses < Linter 4 | include LinterRegistry 5 | 6 | def visit_sequence(sequence) 7 | line_offset = 0 8 | sequence.members.each do |member| 9 | line_offset += 1 if member.to_s =~ /\n/ 10 | next unless chained_class?(member) 11 | add_lint(member.line + line_offset, 12 | 'Prefer using a distinct class over chained classes ' \ 13 | '(e.g. .new-class over .foo.bar') 14 | end 15 | end 16 | 17 | private 18 | 19 | def chained_class?(simple_sequence) 20 | return unless simple_sequence.is_a?(Sass::Selector::SimpleSequence) 21 | simple_sequence.members.count { |member| member.is_a?(Sass::Selector::Class) } >= 2 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/color_keyword.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for uses of a color keyword instead of the preferred hexadecimal 3 | # form. 4 | class Linter::ColorKeyword < Linter 5 | include LinterRegistry 6 | 7 | FUNCTIONS_ALLOWING_COLOR_KEYWORD_ARGS = %w[ 8 | map-get 9 | map-has-key 10 | map-remove 11 | ].to_set 12 | 13 | def visit_script_color(node) 14 | word = source_from_range(node.source_range)[/([a-z]+)/i, 1] 15 | add_color_lint(node, word) if color_keyword?(word) 16 | end 17 | 18 | def visit_script_string(node) 19 | return unless node.type == :identifier 20 | 21 | remove_quoted_strings(node.value).scan(/(^|\s)([a-z]+)(?=\s|$)/i) do |_, word| 22 | add_color_lint(node, word) if color_keyword?(word) 23 | end 24 | end 25 | 26 | private 27 | 28 | def add_color_lint(node, original) 29 | return if in_map?(node) || in_allowed_function_call?(node) 30 | 31 | hex_form = Sass::Script::Value::Color.new(color_keyword_to_code(original)).tap do |color| 32 | color.options = {} # `inspect` requires options to be set 33 | end.inspect 34 | 35 | add_lint(node, 36 | "Color `#{original}` should be written in hexadecimal form " \ 37 | "as `#{hex_form}`") 38 | end 39 | 40 | def in_map?(node) 41 | node_ancestor(node, 2).is_a?(Sass::Script::Tree::MapLiteral) 42 | end 43 | 44 | def in_allowed_function_call?(node) 45 | (funcall = node_ancestor(node, 2)).is_a?(Sass::Script::Tree::Funcall) && 46 | FUNCTIONS_ALLOWING_COLOR_KEYWORD_ARGS.include?(funcall.name) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/color_variable.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Ensures color literals are used only in variable declarations. 3 | class Linter::ColorVariable < Linter 4 | include LinterRegistry 5 | 6 | COLOR_FUNCTIONS = %w[rgb rgba hsl hsla].freeze 7 | 8 | def visit_script_color(node) 9 | return if in_variable_declaration?(node) || 10 | in_map_declaration?(node) || 11 | in_rgba_function_call?(node) 12 | 13 | # Source range sometimes includes closing parenthesis, so extract it 14 | color = source_from_range(node.source_range)[/(#?[a-z0-9]+)/i, 1] 15 | 16 | record_lint(node, color) if color?(color) 17 | end 18 | 19 | def visit_script_string(node) 20 | return if literal_string?(node) 21 | remove_quoted_strings(node.value) # rubocop:disable Style/HashEachMethods 22 | .scan(/(^|\s)(#[a-f0-9]+|[a-z]+)(?=\s|$)/i) 23 | .select { |_, word| color?(word) } 24 | .each { |_, color| record_lint(node, color) } 25 | end 26 | 27 | def visit_script_funcall(node) 28 | if literal_color_function?(node) 29 | record_lint node, node.to_sass 30 | else 31 | yield 32 | end 33 | end 34 | 35 | private 36 | 37 | def record_lint(node, color) 38 | add_lint node, "Color literals like `#{color}` should only be used in " \ 39 | 'variable declarations; they should be referred to via ' \ 40 | 'variable everywhere else.' 41 | end 42 | 43 | def literal_string?(script_string) 44 | return unless script_string.respond_to?(:source_range) && 45 | source_range = script_string.source_range 46 | 47 | # If original source starts with a quote character, it's a string, not a 48 | # color 49 | %w[' "].include?(source_from_range(source_range)[0]) 50 | end 51 | 52 | def in_variable_declaration?(node) 53 | parent = node.node_parent 54 | parent.is_a?(Sass::Script::Tree::Literal) && 55 | (parent.node_parent.is_a?(Sass::Tree::VariableNode) || 56 | parent.node_parent.node_parent.is_a?(Sass::Tree::VariableNode)) 57 | end 58 | 59 | def function_in_variable_declaration?(node) 60 | node.node_parent.is_a?(Sass::Tree::VariableNode) || 61 | node.node_parent.node_parent.is_a?(Sass::Tree::VariableNode) 62 | end 63 | 64 | def in_rgba_function_call?(node) 65 | grandparent = node_ancestor(node, 2) 66 | 67 | grandparent.is_a?(Sass::Script::Tree::Funcall) && 68 | grandparent.name == 'rgba' 69 | end 70 | 71 | def in_map_declaration?(node) 72 | node_ancestor(node, 2).is_a?(Sass::Script::Tree::MapLiteral) 73 | end 74 | 75 | def all_arguments_are_literals?(node) 76 | node.args.all? do |arg| 77 | arg.is_a?(Sass::Script::Tree::Literal) 78 | end 79 | end 80 | 81 | def color_function?(node) 82 | COLOR_FUNCTIONS.include?(node.name) 83 | end 84 | 85 | def literal_color_function?(node) 86 | color_function?(node) && 87 | all_arguments_are_literals?(node) && 88 | !function_in_variable_declaration?(node) 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/comment.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for uses of renderable comments (/* ... */) 3 | class Linter::Comment < Linter 4 | include LinterRegistry 5 | 6 | def visit_comment(node) 7 | add_lint(node, message) unless valid_comment?(node) 8 | end 9 | 10 | private 11 | 12 | def valid_comment?(node) 13 | allowed_type = 14 | if config.fetch('style', 'silent') == 'silent' 15 | node.invisible? 16 | else 17 | !node.invisible? 18 | end 19 | return true if allowed_type 20 | 21 | # Otherwise check if comment contains content that excludes it (i.e. a 22 | # copyright notice for loud comments) 23 | allowed?(node) 24 | end 25 | 26 | # @param node [CommentNode] 27 | # @return [Boolean] 28 | def allowed?(node) 29 | return false unless config['allowed'] 30 | re = Regexp.new(config['allowed']) 31 | 32 | node.value.join.match(re) 33 | end 34 | 35 | def message 36 | if config.fetch('style', 'silent') == 'silent' 37 | 'Use `//` comments everywhere' 38 | else 39 | 'Use `/* */` comments everywhere' 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/compass.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Superclass for linters that apply to codebases using the Compass framework. 3 | # 4 | # Any shared code/constants amongst Compass linters should be stored here. 5 | class Linter::Compass < Linter 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/compass/README.md: -------------------------------------------------------------------------------- 1 | # Compass Linters 2 | 3 | These linters are designed specifically for codebases which utilize the 4 | [Compass](http://compass-style.org/) framework. They are disabled by default, 5 | but can be enabled by adding the following to your configuration: 6 | 7 | ```yaml 8 | linters: 9 | Compass::*: 10 | enabled: true 11 | ``` 12 | 13 | You can of course enable/disable specific linters by referring to them by full 14 | name, e.g. `Compass::PropertyWithMixin`. 15 | 16 | ## Compass::PropertyWithMixin 17 | 18 | Prefer Compass mixins for properties when they exist. 19 | 20 | **Bad: property possibly not fully supported in all browsers** 21 | ```scss 22 | border-radius: 5px; 23 | ``` 24 | 25 | **Good: using Compass mixin ensures all vendor-prefixed extensions are rendered** 26 | ```scss 27 | @include border-radius(5px); 28 | ``` 29 | 30 | These mixins include the necessary vendor-prefixed properties to increase the 31 | number of browsers the CSS supports. 32 | 33 | In the event you don't want to be warned about certain properties, you can opt 34 | to ignore them by listing them in the `ignore` option, e.g. in your 35 | `.scss-lint.yml`: 36 | 37 | ```yaml 38 | linters: 39 | Compass::PropertyWithMixin: 40 | ignore: 41 | - 'inline-block' 42 | ``` 43 | 44 | Configuration Option | Description 45 | ----------------------|-------------------------------------------------------- 46 | `ignore` | Array of Compass mixins that should not be preferred. 47 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/compass/property_with_mixin.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for uses of properties where a Compass mixin would be preferred. 3 | class Linter::Compass::PropertyWithMixin < Linter::Compass 4 | include LinterRegistry 5 | 6 | def visit_prop(node) 7 | check_for_properties_with_mixins(node) 8 | check_for_inline_block(node) 9 | end 10 | 11 | private 12 | 13 | # Set of properties where the Compass mixin version is preferred 14 | PROPERTIES_WITH_MIXINS = %w[ 15 | background-clip 16 | background-origin 17 | border-radius 18 | box-shadow 19 | box-sizing 20 | opacity 21 | text-shadow 22 | transform 23 | ].to_set 24 | 25 | def check_for_properties_with_mixins(node) 26 | prop_name = node.name.join 27 | return unless PROPERTIES_WITH_MIXINS.include?(prop_name) && 28 | !ignore_compass_mixin?(prop_name) 29 | 30 | add_lint node, "Use the Compass `#{prop_name}` mixin instead of the property" 31 | end 32 | 33 | def check_for_inline_block(node) 34 | prop_name = node.name.join 35 | return unless prop_name == 'display' && 36 | node.value.first.to_sass == 'inline-block' && 37 | !ignore_compass_mixin?('inline-block') 38 | 39 | add_lint node, 40 | 'Use the Compass `inline-block` mixin instead of `display: inline-block`' 41 | end 42 | 43 | def ignore_compass_mixin?(prop_name) 44 | config.fetch('ignore', []).include?(prop_name) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/debug_statement.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for leftover `@debug` statements. 3 | class Linter::DebugStatement < Linter 4 | include LinterRegistry 5 | 6 | def visit_debug(node) 7 | add_lint(node, 'Remove `@debug` line') 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/declaration_order.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SCSSLint 4 | # Checks the order of nested items within a rule set. 5 | class Linter::DeclarationOrder < Linter 6 | include LinterRegistry 7 | 8 | def check_order(node) 9 | check_node(node) 10 | yield # Continue linting children 11 | end 12 | 13 | alias visit_rule check_order 14 | alias visit_mixin check_order 15 | alias visit_media check_order 16 | 17 | private 18 | 19 | MESSAGE = 20 | 'Rule sets should be ordered as follows: '\ 21 | '`@extends`, `@includes` without `@content`, ' \ 22 | 'properties, `@includes` with `@content`, ' \ 23 | 'nested rule sets'.freeze 24 | 25 | MIXIN_WITH_CONTENT = 'mixin_with_content'.freeze 26 | 27 | DECLARATION_ORDER = [ 28 | Sass::Tree::ExtendNode, 29 | Sass::Tree::MixinNode, 30 | Sass::Tree::PropNode, 31 | MIXIN_WITH_CONTENT, 32 | Sass::Tree::RuleNode, 33 | ].freeze 34 | 35 | def important_node?(node) 36 | DECLARATION_ORDER.include?(node.class) 37 | end 38 | 39 | def check_node(node) 40 | children = node.children.each_with_index 41 | .select { |n, _| important_node?(n) } 42 | .map { |n, i| [n, node_declaration_type(n), i] } 43 | 44 | sorted_children = children.sort do |(_, a_type, i), (_, b_type, j)| 45 | [DECLARATION_ORDER.index(a_type), i] <=> [DECLARATION_ORDER.index(b_type), j] 46 | end 47 | 48 | check_children_order(sorted_children, children) 49 | end 50 | 51 | # Find the child that is out of place 52 | def check_children_order(sorted_children, children) 53 | sorted_children.each_with_index do |sorted_item, index| 54 | next if sorted_item == children[index] 55 | 56 | add_lint(sorted_item.first.line, 57 | "Expected item on line #{sorted_item.first.line} to appear " \ 58 | "before line #{children[index].first.line}. #{MESSAGE}") 59 | break 60 | end 61 | end 62 | 63 | def node_declaration_type(node) 64 | # If the node has no children, return the class. 65 | return node.class unless node.has_children 66 | 67 | # If the node is a mixin with children, indicate that; 68 | # otherwise, just return the class. 69 | return node.class unless node.is_a?(Sass::Tree::MixinNode) 70 | MIXIN_WITH_CONTENT 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/disable_linter_reason.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for "reason" comments above linter-disabling comments. 3 | class Linter::DisableLinterReason < Linter 4 | include LinterRegistry 5 | 6 | def visit_comment(node) 7 | # No lint if the first line of the comment is not a command (because then 8 | # either this comment has no commands, or the first line serves as a the 9 | # reason for a command on a later line). 10 | if comment_lines(node).first.match?(COMMAND_REGEX) 11 | visit_command_comment(node) 12 | else 13 | @previous_comment = node 14 | end 15 | end 16 | 17 | def visit_command_comment(node) 18 | if @previous_comment.nil? 19 | report_lint(node) 20 | return 21 | end 22 | 23 | # Not a "disable linter reason" if the last line of the previous comment is a command. 24 | if comment_lines(@previous_comment).last.match?(COMMAND_REGEX) 25 | report_lint(node) 26 | return 27 | end 28 | 29 | # No lint if the last line of the previous comment is on the previous line. 30 | if @previous_comment.source_range.end_pos.line == node.source_range.end_pos.line - 1 31 | return 32 | end 33 | 34 | # The "reason" comment doesn't have to be on the previous line, as long as it is exactly 35 | # the previous node. 36 | if previous_node(node) == @previous_comment 37 | return 38 | end 39 | 40 | report_lint(node) 41 | end 42 | 43 | private 44 | 45 | COMMAND_REGEX = %r{ 46 | (/|\*)\s* # Comment start marker 47 | scss-lint: 48 | (?disable)\s+ 49 | (?.*?) 50 | \s*(?:\*/|\n) # Comment end marker or end of line 51 | }x.freeze 52 | 53 | def comment_lines(node) 54 | node.value.join.split("\n") 55 | end 56 | 57 | def report_lint(node) 58 | add_lint(node, 59 | 'scss-lint:disable control comments should be preceded by a ' \ 60 | 'comment explaining why the linters need to be disabled.') 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/duplicate_property.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for a property declared twice in a rule set. 3 | class Linter::DuplicateProperty < Linter 4 | include LinterRegistry 5 | 6 | def visit_root(_node) 7 | @ignore_consecutive = config['ignore_consecutive'] 8 | yield 9 | end 10 | 11 | def check_properties(node) 12 | static_properties(node).each_with_object({}) do |prop, prop_names| 13 | prop_key = property_key(prop) 14 | 15 | if existing_prop = prop_names[prop_key] 16 | if existing_prop.line < prop.line - 1 || !ignore_consecutive_of?(prop) 17 | add_lint(prop, "Property `#{existing_prop.name.join}` already "\ 18 | "defined on line #{existing_prop.line}") 19 | else 20 | prop_names[prop_key] = prop 21 | end 22 | else 23 | prop_names[prop_key] = prop 24 | end 25 | end 26 | 27 | yield # Continue linting children 28 | end 29 | 30 | alias visit_rule check_properties 31 | alias visit_mixindef check_properties 32 | alias visit_media check_properties 33 | 34 | private 35 | 36 | def static_properties(node) 37 | node.children 38 | .select { |child| child.is_a?(Sass::Tree::PropNode) } 39 | .reject { |prop| prop.name.any? { |item| item.is_a?(Sass::Script::Node) } } 40 | end 41 | 42 | # Returns a key identifying the bucket this property and value correspond to 43 | # for purposes of uniqueness. 44 | def property_key(prop) 45 | prop_key = prop.name.join 46 | prop_value = value_as_string(prop.value.first) 47 | 48 | # Differentiate between values for different vendor prefixes 49 | prop_value.to_s.scan(/^(-[^-]+-.+)/) do |vendor_keyword| 50 | prop_key << vendor_keyword.first 51 | end 52 | 53 | prop_key 54 | end 55 | 56 | def value_as_string(value) 57 | case value 58 | when Sass::Script::Funcall 59 | value.name 60 | when Sass::Script::String 61 | nil 62 | when Sass::Script::Tree::Literal 63 | value.value 64 | when Sass::Script::Tree::ListLiteral 65 | value.elements.map { |e| value_as_string(e) }.join(' ') 66 | else 67 | value.to_s 68 | end 69 | end 70 | 71 | def ignore_consecutive_of?(prop) 72 | case @ignore_consecutive 73 | when true 74 | true 75 | when false 76 | false 77 | when nil 78 | false 79 | when Array 80 | @ignore_consecutive.include?(prop.name.join) 81 | else 82 | raise SCSSLint::Exceptions::LinterError, 83 | "#{@ignore_consecutive.inspect} is not a valid value for ignore_consecutive." 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/else_placement.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks where `@else` and `@else if` directives are placed with respect to 3 | # the previous curly brace. 4 | class Linter::ElsePlacement < Linter 5 | include LinterRegistry 6 | 7 | def visit_if(node) 8 | visit_else(node, node.else) if node.else 9 | yield # Lint nested @if statements 10 | visit(node.else) if node.else 11 | end 12 | 13 | def visit_else(if_node, else_node) 14 | # Check each @else branch if there are multiple `@else if`s 15 | visit_else(else_node, else_node.else) if else_node.else 16 | 17 | # Skip @else statements on the same line as the previous @if, since we 18 | # don't care about placement in that case 19 | return if if_node.line == else_node.line 20 | 21 | spaces = 0 22 | while (char = character_at(else_node.source_range.start_pos, - (spaces + 1))) 23 | if char == '}' 24 | curly_on_same_line = true 25 | break 26 | end 27 | spaces += 1 28 | end 29 | 30 | check_placement(else_node, curly_on_same_line) 31 | end 32 | 33 | private 34 | 35 | def check_placement(else_node, curly_on_same_line) 36 | if same_line_preferred? 37 | unless curly_on_same_line 38 | add_lint(else_node, 39 | '`@else` should be placed on same line as previous curly brace') 40 | end 41 | elsif curly_on_same_line 42 | add_lint(else_node, '`@else` should be placed on its own line') 43 | end 44 | end 45 | 46 | def same_line_preferred? 47 | config['style'] == 'same_line' 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/empty_rule.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for rules with no content. 3 | class Linter::EmptyRule < Linter 4 | include LinterRegistry 5 | 6 | def visit_rule(node) 7 | add_lint(node, 'Empty rule') if node.children.empty? 8 | yield # Continue linting children 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/encoding.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | class Linter::Encoding < Linter 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/extend_directive.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks that `@extend` is never used. 3 | class Linter::ExtendDirective < Linter 4 | include LinterRegistry 5 | 6 | def visit_extend(node) 7 | add_lint(node, 'Do not use the `@extend` directive (`@include` a `@mixin` ' \ 8 | 'instead)') 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/final_newline.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for final newlines at the end of a file. 3 | class Linter::FinalNewline < Linter 4 | include LinterRegistry 5 | 6 | def visit_root(_node) 7 | return if engine.lines.empty? 8 | 9 | ends_with_newline = engine.lines[-1][-1] == "\n" 10 | 11 | if config['present'] 12 | unless ends_with_newline 13 | add_lint(engine.lines.count, 'Files should end with a trailing newline') 14 | end 15 | elsif ends_with_newline 16 | add_lint(engine.lines.count, 'Files should not end with a trailing newline') 17 | end 18 | 19 | yield 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/hex_length.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks that hexadecimal colors are written in the desired number of 3 | # characters. 4 | class Linter::HexLength < Linter 5 | include LinterRegistry 6 | 7 | HEX_REGEX = /(#(\h{3}|\h{6}))(?!\h)/.freeze 8 | 9 | def visit_script_color(node) 10 | return unless hex = source_from_range(node.source_range)[HEX_REGEX, 1] 11 | check_hex(hex, node) 12 | end 13 | 14 | def visit_script_string(node) 15 | return unless node.type == :identifier 16 | 17 | node.value.scan(HEX_REGEX) do |match| 18 | check_hex(match.first, node) 19 | end 20 | end 21 | 22 | private 23 | 24 | def check_hex(hex, node) 25 | return if expected(hex) == hex 26 | 27 | add_lint(node, "Color `#{hex}` should be written as `#{expected(hex)}`") 28 | end 29 | 30 | def expected(hex) 31 | return short_hex_form(hex) if can_be_shorter?(hex) && short_style? 32 | return long_hex_form(hex) if hex.length == 4 && !short_style? 33 | 34 | hex 35 | end 36 | 37 | def can_be_shorter?(hex) 38 | hex.length == 7 && 39 | hex[1] == hex[2] && 40 | hex[3] == hex[4] && 41 | hex[5] == hex[6] 42 | end 43 | 44 | def short_hex_form(hex) 45 | [hex[0..1], hex[3], hex[5]].join 46 | end 47 | 48 | def long_hex_form(hex) 49 | [hex[0..1], hex[1], hex[2], hex[2], hex[3], hex[3]].join 50 | end 51 | 52 | def short_style? 53 | config['style'] == 'short' 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/hex_notation.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks if hexadecimal colors are written lowercase / uppercase. 3 | class Linter::HexNotation < Linter 4 | include LinterRegistry 5 | 6 | HEX_REGEX = /(#(\h{3}|\h{6}))(?!\h)/.freeze 7 | 8 | def visit_script_color(node) 9 | return unless hex = source_from_range(node.source_range)[HEX_REGEX, 1] 10 | check_hex(hex, node) 11 | end 12 | 13 | def visit_script_string(node) 14 | return unless node.type == :identifier 15 | 16 | node.value.scan(HEX_REGEX) do |match| 17 | check_hex(match.first, node) 18 | end 19 | end 20 | 21 | private 22 | 23 | def check_hex(hex, node) 24 | return if expected(hex) == hex 25 | 26 | add_lint(node, "Color `#{hex}` should be written as `#{expected(hex)}`") 27 | end 28 | 29 | def expected(color) 30 | return color.downcase if lowercase_style? 31 | color.upcase 32 | end 33 | 34 | def lowercase_style? 35 | config['style'] == 'lowercase' 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/hex_validation.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for invalid hexadecimal colors. 3 | class Linter::HexValidation < Linter 4 | include LinterRegistry 5 | 6 | def visit_script_string(node) 7 | return unless node.type == :identifier 8 | 9 | node.value.scan(/(?:\W|^)(#\h+)(?:\W|$)/) do |match| 10 | check_hex(match.first, node) 11 | end 12 | end 13 | 14 | private 15 | 16 | HEX_REGEX = /(#(\h{3}|\h{6}|\h{8}))(?!\h)/.freeze 17 | 18 | def check_hex(hex, node) 19 | return if HEX_REGEX.match?(hex) 20 | add_lint(node, "Colors must have either three or six digits: `#{hex}`") 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/id_selector.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for the use of an ID selector. 3 | class Linter::IdSelector < Linter 4 | include LinterRegistry 5 | 6 | def visit_id(id) 7 | add_lint(id, 'Avoid using id selectors') 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/import_path.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks formatting of the basenames of @imported partials 3 | class Linter::ImportPath < Linter 4 | include LinterRegistry 5 | 6 | def visit_import(node) 7 | # Ignore CSS imports 8 | return if File.extname(node.imported_filename) == '.css' 9 | basename = File.basename(node.imported_filename) 10 | return if underscore_ok?(basename) && extension_ok?(basename) 11 | add_lint(node, compose_message(node.imported_filename)) 12 | end 13 | 14 | private 15 | 16 | # Checks if the presence or absence of a leading underscore 17 | # on a string is ok, given config option. 18 | # 19 | # @param str [String] the string to check 20 | # @return [Boolean] 21 | def underscore_ok?(str) 22 | underscore_exists = str.start_with?('_') 23 | config['leading_underscore'] ? underscore_exists : !underscore_exists 24 | end 25 | 26 | # Checks if the presence or absence of an `scss` filename 27 | # extension on a string is ok, given config option. 28 | # 29 | # @param str [String] the string to check 30 | # @return [Boolean] 31 | def extension_ok?(str) 32 | extension_exists = str.end_with?('.scss') 33 | config['filename_extension'] ? extension_exists : !extension_exists 34 | end 35 | 36 | # Composes a helpful lint message based on the original filename 37 | # and the config options. 38 | # 39 | # @param orig_filename [String] the original filename 40 | # @return [String] the helpful lint message 41 | def compose_message(orig_filename) 42 | orig_basename = File.basename(orig_filename) 43 | fixed_basename = orig_basename 44 | 45 | if config['leading_underscore'] 46 | fixed_basename = "_#{fixed_basename}" unless fixed_basename.start_with?('_') 47 | else 48 | fixed_basename = fixed_basename.sub(/^_/, '') 49 | end 50 | 51 | if config['filename_extension'] 52 | fixed_basename += '.scss' unless fixed_basename.end_with?('.scss') 53 | else 54 | fixed_basename = fixed_basename.sub(/\.scss$/, '') 55 | end 56 | 57 | fixed_filename = orig_filename.sub(/(.*)#{Regexp.quote(orig_basename)}/, 58 | "\\1#{fixed_basename}") 59 | "Imported partial `#{orig_filename}` should be written as `#{fixed_filename}`" 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/important_rule.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Reports the use of !important in properties. 3 | class Linter::ImportantRule < Linter 4 | include LinterRegistry 5 | 6 | def visit_prop(node) 7 | return unless source_from_range(node.source_range).include?('!important') 8 | 9 | add_lint(node, '!important should not be used') 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/leading_zero.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for unnecessary leading zeros in numeric values with decimal points. 3 | class Linter::LeadingZero < Linter 4 | include LinterRegistry 5 | 6 | def visit_script_string(node) 7 | return unless node.type == :identifier 8 | 9 | non_string_values = remove_quoted_strings(node.value).split 10 | non_string_values.each do |value| 11 | next unless number = value[NUMBER_WITH_LEADING_ZERO_REGEX, 1] 12 | check_for_leading_zeros(node, number) 13 | end 14 | end 15 | 16 | def visit_script_number(node) 17 | return unless number = 18 | source_from_range(node.source_range)[NUMBER_WITH_LEADING_ZERO_REGEX, 1] 19 | 20 | check_for_leading_zeros(node, number) 21 | end 22 | 23 | private 24 | 25 | NUMBER_WITH_LEADING_ZERO_REGEX = /^-?(0?\.\d+)/.freeze 26 | 27 | CONVENTIONS = { 28 | 'exclude_zero' => { 29 | explanation: '`%s` should be written without a leading zero as `%s`', 30 | validator: ->(original) { original =~ /^\.\d+$/ }, 31 | converter: ->(original) { original[1..-1] }, 32 | }, 33 | 'include_zero' => { 34 | explanation: '`%s` should be written with a leading zero as `%s`', 35 | validator: ->(original) { original =~ /^0\.\d+$/ }, 36 | converter: ->(original) { "0#{original}" } 37 | }, 38 | }.freeze 39 | 40 | def check_for_leading_zeros(node, original_number) 41 | style = config.fetch('style', 'exclude_zero') 42 | convention = CONVENTIONS[style] 43 | return if convention[:validator].call(original_number) 44 | 45 | corrected = convention[:converter].call(original_number) 46 | add_lint(node, convention[:explanation] % [original_number, corrected]) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/mergeable_selector.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for rule sets that can be merged with other rule sets. 3 | class Linter::MergeableSelector < Linter 4 | include LinterRegistry 5 | 6 | def check_node(node) 7 | node.children.each_with_object([]) do |child_node, seen_nodes| 8 | next unless child_node.is_a?(Sass::Tree::RuleNode) 9 | 10 | next if whitelist_contains(child_node) 11 | 12 | mergeable_node = find_mergeable_node(child_node, seen_nodes) 13 | seen_nodes << child_node 14 | next unless mergeable_node 15 | 16 | rule_text = node_rule(child_node).gsub(/(\r?\n)+/, ' ') 17 | 18 | add_lint child_node.line, 19 | "Merge rule `#{rule_text}` with rule " \ 20 | "on line #{mergeable_node.line}" 21 | end 22 | 23 | yield # Continue linting children 24 | end 25 | 26 | alias visit_root check_node 27 | alias visit_rule check_node 28 | 29 | private 30 | 31 | def find_mergeable_node(node, seen_nodes) 32 | return if multiple_parent_references?(node) 33 | 34 | seen_nodes.find do |seen_node| 35 | equal?(node, seen_node) || 36 | (config['force_nesting'] && nested?(node, seen_node)) 37 | end 38 | end 39 | 40 | def multiple_parent_references?(rule_node) 41 | return unless rules = rule_node.parsed_rules 42 | 43 | # Iterate over each sequence counting all parent references 44 | total_parent_references = rules.members.inject(0) do |sum, seq| 45 | sum + seq.members.inject(0) do |ssum, simple_seq| 46 | next ssum unless simple_seq.respond_to?(:members) 47 | ssum + simple_seq.members.count do |member| 48 | member.is_a?(Sass::Selector::Parent) 49 | end 50 | end 51 | end 52 | 53 | total_parent_references > 1 54 | end 55 | 56 | def equal?(node1, node2) 57 | node_rule(node1) == node_rule(node2) 58 | end 59 | 60 | def nested?(node1, node2) 61 | return false unless single_rule?(node1) && single_rule?(node2) 62 | 63 | rule1 = node_rule(node1) 64 | rule2 = node_rule(node2) 65 | subrule?(rule1, rule2) || subrule?(rule2, rule1) 66 | end 67 | 68 | def node_rule(node) 69 | node.rule.join 70 | end 71 | 72 | def single_rule?(node) 73 | return unless node.parsed_rules 74 | node.parsed_rules.members.count == 1 75 | end 76 | 77 | def subrule?(rule1, rule2) 78 | rule1.to_s.start_with?("#{rule2} ", "#{rule2}.") 79 | end 80 | 81 | def whitelist_contains(node) 82 | if @whitelist.nil? 83 | @whitelist = config['whitelist'] || [] 84 | @whitelist = [@whitelist] if @whitelist.is_a? String 85 | end 86 | 87 | @whitelist.include?(node_rule(node)) 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/nesting_depth.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for rule sets nested deeper than a specified maximum depth. 3 | class Linter::NestingDepth < Linter 4 | include LinterRegistry 5 | 6 | IGNORED_SELECTORS = [Sass::Selector::Parent, Sass::Selector::Pseudo].freeze 7 | 8 | def visit_root(_node) 9 | @max_depth = config['max_depth'] 10 | @depth = 1 11 | yield # Continue linting children 12 | end 13 | 14 | def visit_rule(node) 15 | return yield if ignore_selectors?(node) 16 | 17 | if @depth > @max_depth 18 | add_lint node, "Nesting should be no greater than #{@max_depth}, " \ 19 | "but was #{@depth}" 20 | else 21 | # Only continue if we didn't exceed the max depth already (this makes 22 | # the lint less noisy) 23 | @depth += 1 24 | yield # Continue linting children 25 | @depth -= 1 26 | end 27 | end 28 | 29 | private 30 | 31 | def ignore_selectors?(node) 32 | return unless config['ignore_parent_selectors'] 33 | return unless node.parsed_rules 34 | 35 | simple_selectors(node.parsed_rules).all? do |selector| 36 | IGNORED_SELECTORS.include?(selector.class) 37 | end 38 | end 39 | 40 | def simple_selectors(node) 41 | node.members.flat_map(&:members).reject do |simple_sequence| 42 | simple_sequence.is_a?(String) 43 | end.flat_map(&:members) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/placeholder_in_extend.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks that `@extend` is always used with a placeholder selector. 3 | class Linter::PlaceholderInExtend < Linter 4 | include LinterRegistry 5 | 6 | def visit_extend(node) 7 | # Ignore if it cannot be statically determined that this selector is a 8 | # placeholder since its prefix is dynamically generated 9 | return if node.selector.first.is_a?(Sass::Script::Tree::Node) 10 | 11 | # The array returned by the parser is a bit awkward in that it splits on 12 | # every word boundary (so %placeholder becomes ['%', 'placeholder']). 13 | selector = node.selector.join 14 | 15 | if selector.include?(',') 16 | add_lint(node, 'Avoid comma sequences in `@extend` directives; ' \ 17 | 'prefer single placeholder selectors (e.g. `%some-placeholder`)') 18 | elsif !selector.start_with?('%') 19 | add_lint(node, 'Prefer using placeholder selectors (e.g. ' \ 20 | '%some-placeholder) with @extend') 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/property_count.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks that the number of properties in a rule set is under a defined limit. 3 | class Linter::PropertyCount < Linter 4 | include LinterRegistry 5 | 6 | def visit_root(_node) 7 | @property_count = {} # Lookup table of counts for rule sets 8 | @max = config['max_properties'] 9 | yield # Continue linting children 10 | end 11 | 12 | def visit_rule(node) 13 | count = property_count(node) 14 | 15 | if count > @max 16 | add_lint node, 17 | "Rule set contains (#{count}/#{@max}) properties" \ 18 | "#{' (including properties in nested rule sets)' if config['include_nested']}" 19 | 20 | # Don't lint nested rule sets as we already have them in the count 21 | return if config['include_nested'] 22 | end 23 | 24 | yield # Lint nested rule sets 25 | end 26 | 27 | private 28 | 29 | def property_count(rule_node) 30 | @property_count[rule_node] ||= 31 | begin 32 | count = rule_node.children.count { |node| node.is_a?(Sass::Tree::PropNode) } 33 | 34 | if config['include_nested'] 35 | count += rule_node.children.inject(0) do |sum, node| 36 | node.is_a?(Sass::Tree::RuleNode) ? sum + property_count(node) : sum 37 | end 38 | end 39 | 40 | count 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/property_spelling.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for misspelled properties. 3 | class Linter::PropertySpelling < Linter 4 | include LinterRegistry 5 | 6 | KNOWN_PROPERTIES = File.open(File.join(SCSS_LINT_DATA, 'properties.txt')) 7 | .read 8 | .split 9 | .to_set 10 | 11 | def visit_root(_node) 12 | @extra_properties = Array(config['extra_properties']).to_set 13 | @disabled_properties = Array(config['disabled_properties']).to_set 14 | 15 | yield # Continue linting children 16 | end 17 | 18 | def visit_prop(node) 19 | # Ignore properties with interpolation 20 | return if node.name.count > 1 || !node.name.first.is_a?(String) 21 | 22 | nested_properties = node.children.select { |child| child.is_a?(Sass::Tree::PropNode) } 23 | if nested_properties.any? 24 | # Treat nested properties specially, as they are a concatenation of the 25 | # parent with child property 26 | nested_properties.each do |nested_prop| 27 | check_property(nested_prop, node.name.join) 28 | end 29 | else 30 | check_property(node) 31 | end 32 | end 33 | 34 | private 35 | 36 | def check_property(node, prefix = nil) 37 | return if contains_interpolation?(node) 38 | 39 | name = prefix ? "#{prefix}-" : '' 40 | name += node.name.join 41 | 42 | # Ignore vendor-prefixed properties 43 | return if name.start_with?('-') 44 | return if known_property?(name) && !@disabled_properties.include?(name) 45 | 46 | if @disabled_properties.include?(name) 47 | add_lint(node, "Property #{name} is prohibited") 48 | else 49 | add_lint(node, "Unknown property #{name}") 50 | end 51 | end 52 | 53 | def known_property?(name) 54 | KNOWN_PROPERTIES.include?(name) || @extra_properties.include?(name) 55 | end 56 | 57 | def contains_interpolation?(node) 58 | node.name.count > 1 || !node.name.first.is_a?(String) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/property_units.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Check for allowed units 3 | class Linter::PropertyUnits < Linter 4 | include LinterRegistry 5 | 6 | NUMBER_WITH_UNITS_REGEX = / 7 | (?: 8 | (["']).+?\1 # [0: quote mark] quoted string, e.g. "hi there" 9 | | # or 10 | (?:^|\s) # beginning of value or whitespace 11 | (?: 12 | \d+ # any number of digits, e.g. 123 13 | | # or 14 | \d*\.?\d+ # any number of digits with decimal, e.g. 1.23 or .123 15 | ) 16 | ([a-z%]+) # [1: units] letters or percent sign, e.g. px or % 17 | ) 18 | /ix.freeze 19 | 20 | def visit_root(_node) 21 | @globally_allowed_units = config['global'].to_set 22 | @allowed_units_for_property = config['properties'] 23 | 24 | yield # Continue linting children 25 | end 26 | 27 | def visit_prop(node) 28 | property = node.name.join 29 | 30 | # Handle nested properties by ensuring the full name is extracted 31 | if @nested_under 32 | property = "#{@nested_under}-#{property}" 33 | end 34 | 35 | if node.value.first.respond_to?(:value) 36 | node.value.first.value.to_s.scan(NUMBER_WITH_UNITS_REGEX).each do |matches| 37 | is_quoted_value = !matches[0].nil? 38 | next if is_quoted_value 39 | units = matches[1] 40 | check_units(node, property, units) 41 | end 42 | end 43 | 44 | @nested_under = property 45 | yield # Continue linting nested properties 46 | @nested_under = nil 47 | end 48 | 49 | private 50 | 51 | # Checks if a property value's units are allowed. 52 | # 53 | # @param node [Sass::Tree::Node] 54 | # @param property [String] 55 | # @param units [String] 56 | def check_units(node, property, units) 57 | allowed_units = allowed_units_for_property(property) 58 | return if allowed_units.include?(units) 59 | 60 | add_lint(node, 61 | "#{units} units not allowed on `#{property}`; must be one of " \ 62 | "(#{allowed_units.to_a.sort.join(', ')})") 63 | end 64 | 65 | # Return the list of allowed units for a property. 66 | # 67 | # @param property [String] 68 | # @return Array 69 | def allowed_units_for_property(property) 70 | if @allowed_units_for_property.key?(property) 71 | @allowed_units_for_property[property] 72 | else 73 | @globally_allowed_units 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/pseudo_element.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for the use of double colons with pseudo elements. 3 | class Linter::PseudoElement < Linter 4 | include LinterRegistry 5 | 6 | # https://msdn.microsoft.com/en-us/library/windows/apps/hh767361.aspx 7 | # https://developer.mozilla.org/en-US/docs/Web/CSS/Mozilla_Extensions 8 | # http://tjvantoll.com/2013/04/15/list-of-pseudo-elements-to-style-form-controls/ 9 | PSEUDO_ELEMENTS = File.open(File.join(SCSS_LINT_DATA, 'pseudo-elements.txt')) 10 | .read 11 | .split 12 | .to_set 13 | 14 | def visit_pseudo(pseudo) 15 | if PSEUDO_ELEMENTS.include?(pseudo.name) 16 | return if pseudo.syntactic_type == :element 17 | add_lint(pseudo, 'Begin pseudo elements with double colons: `::`') 18 | else 19 | return if pseudo.syntactic_type != :element 20 | add_lint(pseudo, 'Begin pseudo classes with a single colon: `:`') 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/qualifying_element.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for element selectors qualifying id, classes, or attribute selectors. 3 | class Linter::QualifyingElement < Linter 4 | include LinterRegistry 5 | 6 | def visit_simple_sequence(seq) 7 | return unless seq_contains_sel_class?(seq, Sass::Selector::Element) 8 | check_id(seq) unless config['allow_element_with_id'] 9 | check_class(seq) unless config['allow_element_with_class'] 10 | check_attribute(seq) unless config['allow_element_with_attribute'] 11 | end 12 | 13 | private 14 | 15 | # Checks if a simple sequence contains a 16 | # simple selector of a certain class. 17 | # 18 | # @param seq [Sass::Selector::SimpleSequence] 19 | # @param selector_class [Sass::Selector::Simple] 20 | # @returns [Boolean] 21 | def seq_contains_sel_class?(seq, selector_class) 22 | seq.members.any? do |simple| 23 | simple.is_a?(selector_class) 24 | end 25 | end 26 | 27 | def check_id(seq) 28 | return unless seq_contains_sel_class?(seq, Sass::Selector::Id) 29 | add_lint(seq.line, 'Avoid qualifying id selectors with an element.') 30 | end 31 | 32 | def check_class(seq) 33 | return unless seq_contains_sel_class?(seq, Sass::Selector::Class) 34 | add_lint(seq.line, 'Avoid qualifying class selectors with an element.') 35 | end 36 | 37 | def check_attribute(seq) 38 | return unless seq_contains_sel_class?(seq, Sass::Selector::Attribute) 39 | add_lint(seq.line, 'Avoid qualifying attribute selectors with an element.') 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/selector_depth.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for selectors with large depths of applicability. 3 | class Linter::SelectorDepth < Linter 4 | include LinterRegistry 5 | 6 | def visit_root(_node) 7 | @max_depth = config['max_depth'] 8 | @depth = 0 9 | yield # Continue 10 | end 11 | 12 | def visit_rule(node) 13 | old_depth = @depth 14 | @depth = max_sequence_depth(node.parsed_rules, @depth) 15 | 16 | if @depth > @max_depth 17 | add_lint(node.parsed_rules || node, 18 | 'Selector should have depth of applicability no greater ' \ 19 | "than #{@max_depth}, but was #{@depth}") 20 | end 21 | 22 | yield # Continue linting children 23 | @depth = old_depth 24 | end 25 | 26 | private 27 | 28 | # Find the maximum depth of all sequences in a comma sequence. 29 | def max_sequence_depth(comma_sequence, current_depth) 30 | # Sequence contains interpolation; assume a depth of 1 31 | return current_depth + 1 unless comma_sequence 32 | 33 | comma_sequence.members.map { |sequence| sequence_depth(sequence, current_depth) }.max 34 | end 35 | 36 | def sequence_depth(sequence, current_depth) 37 | separators, simple_sequences = sequence.members.partition do |item| 38 | item.is_a?(String) 39 | end 40 | 41 | parent_selectors = simple_sequences.count do |item| 42 | next if item.is_a?(Array) # @keyframe percentages end up as Arrays 43 | item.rest.any? { |i| i.is_a?(Sass::Selector::Parent) } 44 | end 45 | 46 | # Take the number of simple sequences and subtract one for each sibling 47 | # combinator, as these "combine" simple sequences such that they do not 48 | # increase depth. 49 | depth = simple_sequences.size - 50 | separators.count { |item| %w[~ +].include?(item) } 51 | 52 | depth += 53 | if parent_selectors > 0 54 | # If parent selectors are present, add the current depth for each 55 | # additional parent selector. 56 | parent_selectors * (current_depth - 1) 57 | else 58 | # Otherwise this just descends from the containing selector 59 | current_depth 60 | end 61 | 62 | depth 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/single_line_per_property.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks that all properties in a rule set are on their own distinct lines. 3 | class Linter::SingleLinePerProperty < Linter 4 | include LinterRegistry 5 | 6 | def visit_rule(node) 7 | yield # Continue linting children 8 | 9 | single_line = single_line_rule_set?(node) 10 | return if single_line && config['allow_single_line_rule_sets'] 11 | 12 | properties = node.children.select { |child| child.is_a?(Sass::Tree::PropNode) } 13 | return unless properties.any? 14 | 15 | # Special case: if single line rule sets aren't allowed, we want to report 16 | # when the first property isn't on a separate line from the selector 17 | if single_line && !config['allow_single_line_rule_sets'] 18 | add_lint(properties.first, 19 | "Property '#{properties.first.name.join}' should be placed " \ 20 | 'on separate line from selector') 21 | end 22 | 23 | check_adjacent_properties(properties) 24 | end 25 | 26 | private 27 | 28 | # Return whether this rule set occupies a single line. 29 | # 30 | # Note that this allows: 31 | # a, 32 | # b, 33 | # i { margin: 0; padding: 0; } 34 | # 35 | # and: 36 | # 37 | # p { margin: 0; padding: 0; } 38 | # 39 | # In other words, the line of the opening curly brace is the line that the 40 | # rule set is considered to occupy. 41 | def single_line_rule_set?(rule) 42 | rule.children.all? { |child| child.line == rule.source_range.end_pos.line } 43 | end 44 | 45 | def first_property_not_on_own_line?(rule, properties) 46 | properties.any? && properties.first.line == rule.line 47 | end 48 | 49 | # Compare each property against the next property to see if they are on 50 | # the same line. 51 | # 52 | # @param properties [Array] 53 | def check_adjacent_properties(properties) 54 | properties[0..-2].zip(properties[1..-1]).each do |first, second| 55 | next unless first.line == second.line 56 | 57 | add_lint(second, "Property '#{second.name.join}' should be placed on own line") 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/single_line_per_selector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SCSSLint 4 | # Checks that selector sequences are split over multiple lines by comma. 5 | class Linter::SingleLinePerSelector < Linter 6 | include LinterRegistry 7 | 8 | MESSAGE = 'Each selector in a comma sequence should be on its own single line'.freeze 9 | 10 | def visit_comma_sequence(node) 11 | return unless node.members.count > 1 12 | 13 | check_comma_on_own_line(node) 14 | 15 | line_offset = 0 16 | node.members[1..-1].each do |sequence| 17 | line_offset += 1 if sequence_start_of_line?(sequence) 18 | check_multiline_sequence(node, sequence, line_offset) 19 | check_sequence_commas(node, sequence, line_offset) 20 | end 21 | end 22 | 23 | def visit_sequence(node) 24 | # Only execute if this is first or only sequence in a comma sequence. If 25 | # it is the only sequence, then it won't be in a comma sequence, which is 26 | # why we define a separate visit_* method specifically for this case. 27 | return if node.members.first == "\n" 28 | 29 | check_multiline_sequence(node, node, 0) 30 | end 31 | 32 | private 33 | 34 | def sequence_start_of_line?(sequence) 35 | sequence.members[0] == "\n" 36 | end 37 | 38 | def check_comma_on_own_line(node) 39 | return unless node.members[0].members[1] == "\n" 40 | add_lint(node, MESSAGE) 41 | end 42 | 43 | # Checks if an individual sequence is split over multiple lines 44 | def check_multiline_sequence(node, sequence, index) 45 | return unless sequence.members.size > 1 46 | return unless sequence.members[2..-1].any? { |member| member == "\n" } 47 | 48 | add_lint(node.line + index, MESSAGE) 49 | end 50 | 51 | def check_sequence_commas(node, sequence, index) 52 | if !sequence_start_of_line?(sequence) 53 | # Next sequence doesn't reside on its own line 54 | add_lint(node.line + index, MESSAGE) 55 | elsif sequence.members[1] == "\n" 56 | # Comma is on its own line 57 | add_lint(node.line + index, MESSAGE) 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/space_after_comment.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for a space after comment literals 3 | class Linter::SpaceAfterComment < Linter 4 | include LinterRegistry 5 | 6 | def visit_comment(node) 7 | source = source_from_range(node.source_range).strip 8 | check_method = "check_#{node.type}_comment" 9 | send(check_method, node, source) 10 | end 11 | 12 | private 13 | 14 | def check_silent_comment(node, source) 15 | source.split("\n").each_with_index do |line, index| 16 | next if config['allow_empty_comments'] && line.strip.length <= 2 17 | whitespace = whitespace_after_comment(line.lstrip, 2) 18 | check_for_space(node.line + index, whitespace) 19 | end 20 | end 21 | 22 | def check_normal_comment(node, source) 23 | whitespace = whitespace_after_comment(source, 2) 24 | check_for_space(node, whitespace) 25 | end 26 | 27 | def check_loud_comment(node, source) 28 | whitespace = whitespace_after_comment(source, 3) 29 | check_for_space(node, whitespace) 30 | end 31 | 32 | def check_for_no_spaces(node_or_line, whitespace) 33 | return if whitespace == 0 34 | add_lint(node_or_line, 'Comment literal should not be followed by any spaces') 35 | end 36 | 37 | def check_for_one_space(node_or_line, whitespace) 38 | return if whitespace == 1 39 | add_lint(node_or_line, 'Comment literal should be followed by one space') 40 | end 41 | 42 | def check_for_at_least_one_space(node_or_line, whitespace) 43 | return if whitespace >= 1 44 | add_lint(node_or_line, 'Comment literal should be followed by at least one space') 45 | end 46 | 47 | def check_for_space(node_or_line, spaces) 48 | case config['style'] 49 | when 'one_space' 50 | check_for_one_space(node_or_line, spaces) 51 | when 'no_space' 52 | check_for_no_spaces(node_or_line, spaces) 53 | when 'at_least_one_space' 54 | check_for_at_least_one_space(node_or_line, spaces) 55 | end 56 | end 57 | 58 | def whitespace_after_comment(source, offset) 59 | whitespace = 0 60 | 61 | offset += 1 if source[offset] == '/' # Allow for triple-slash comments 62 | offset += 1 if source[offset] == '/' # Allow for quadruple-slash comments 63 | 64 | while [' ', "\t"].include? source[offset] 65 | whitespace += 1 66 | offset += 1 67 | end 68 | 69 | whitespace 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/space_after_property_name.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for spaces following the name of a property and before the colon 3 | # separating the property's name from its value. 4 | class Linter::SpaceAfterPropertyName < Linter 5 | include LinterRegistry 6 | 7 | def visit_prop(node) 8 | offset = property_name_colon_offset(node) 9 | return unless character_at(node.name_source_range.start_pos, offset - 1) == ' ' 10 | add_lint node, 'Property name should be immediately followed by a colon' 11 | end 12 | 13 | private 14 | 15 | # Deals with a weird Sass bug where the name_source_range of a PropNode does 16 | # not start at the beginning of the property name. 17 | def property_name_colon_offset(node) 18 | offset_to(node.name_source_range.start_pos, ':') 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/space_after_variable_colon.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for spaces following the colon that separates a variable's name from 3 | # its value. 4 | class Linter::SpaceAfterVariableColon < Linter 5 | include LinterRegistry 6 | 7 | def visit_variable(node) 8 | whitespace = whitespace_after_colon(node) 9 | 10 | case config['style'] 11 | when 'no_space' 12 | check_for_no_spaces(node, whitespace) 13 | when 'one_space' 14 | check_for_one_space(node, whitespace) 15 | when 'at_least_one_space' 16 | check_for_at_least_one_space(node, whitespace) 17 | when 'one_space_or_newline' 18 | check_for_one_space_or_newline(node, whitespace) 19 | end 20 | end 21 | 22 | private 23 | 24 | def check_for_no_spaces(node, whitespace) 25 | return if whitespace == [] 26 | add_lint(node, 'Colon after variable should not be followed by any spaces') 27 | end 28 | 29 | def check_for_one_space(node, whitespace) 30 | return if whitespace == [' '] 31 | add_lint(node, 'Colon after variable should be followed by one space') 32 | end 33 | 34 | def check_for_at_least_one_space(node, whitespace) 35 | return if whitespace.uniq == [' '] 36 | add_lint(node, 'Colon after variable should be followed by at least one space') 37 | end 38 | 39 | def check_for_one_space_or_newline(node, whitespace) 40 | return if [[' '], ["\n"]].include?(whitespace) 41 | return if whitespace[0] == "\n" && whitespace[1..-1].uniq == [' '] 42 | add_lint(node, 'Colon after variable should be followed by one space or a newline') 43 | end 44 | 45 | def whitespace_after_colon(node) 46 | whitespace = [] 47 | offset = 0 48 | start_pos = node.source_range.start_pos 49 | 50 | # Find the colon after the variable name 51 | offset = offset_to(start_pos, ':', offset) + 1 52 | 53 | # Count spaces after the colon 54 | while [' ', "\t", "\n"].include?(character_at(start_pos, offset)) 55 | whitespace << character_at(start_pos, offset) 56 | offset += 1 57 | end 58 | 59 | whitespace 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/space_after_variable_name.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for spaces following the name of a variable and before the colon 3 | # separating the variables's name from its value. 4 | class Linter::SpaceAfterVariableName < Linter 5 | include LinterRegistry 6 | 7 | def visit_variable(node) 8 | return unless spaces_before_colon?(node) 9 | add_lint(node, 'Variable names should be followed immediately by a colon') 10 | end 11 | 12 | private 13 | 14 | def spaces_before_colon?(node) 15 | source_from_range(node.source_range) =~ /\A[^:]+\s+:/ 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/space_before_brace.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for the presence of a single space before an opening brace. 3 | class Linter::SpaceBeforeBrace < Linter 4 | include LinterRegistry 5 | 6 | def check_node(node) 7 | source = source_from_range(node.source_range).strip 8 | 9 | # Only lint `@include`s which have curly braces 10 | if source[-1] == '{' 11 | check_for_space(node, source) 12 | end 13 | 14 | yield 15 | end 16 | 17 | def visit_if(node, &block) 18 | check_node(node, &block) 19 | check_node(node.else, &block) if node.else 20 | end 21 | 22 | alias visit_each check_node 23 | alias visit_for check_node 24 | alias visit_function check_node 25 | alias visit_mixindef check_node 26 | alias visit_mixin check_node 27 | alias visit_rule check_node 28 | alias visit_while check_node 29 | 30 | private 31 | 32 | def check_for_space(node, string) 33 | line = node.source_range.end_pos.line 34 | 35 | if config['allow_single_line_padding'] && node_on_single_line?(node) 36 | return unless string[-2] != ' ' 37 | add_lint(line, 'Opening curly brace in a single line rule set '\ 38 | '`{` should be preceded by at least one space') 39 | else 40 | return unless chars_before_incorrect(string) 41 | style_message = config['style'] == 'new_line' ? 'a new line' : 'one space' 42 | add_lint(line, 'Opening curly brace `{` should be ' \ 43 | "preceded by #{style_message}") 44 | end 45 | end 46 | 47 | # Check if the characters before the end of the string 48 | # are not what they should be 49 | def chars_before_incorrect(string) 50 | if config['style'] != 'new_line' 51 | return !single_space_before(string) 52 | end 53 | !newline_before_nonwhitespace(string) 54 | end 55 | 56 | # Check if there is one space and only one 57 | # space before the end of the string 58 | def single_space_before(string) 59 | return false if string[-2] != ' ' 60 | return false if string[-3] == ' ' 61 | true 62 | end 63 | 64 | # Check if, starting from the end of a string 65 | # and moving backwards, towards the beginning, 66 | # we find a new line before any non-whitespace characters 67 | def newline_before_nonwhitespace(string) 68 | offset = -2 69 | while /\S/.match(string[offset]).nil? 70 | return true if string[offset] == "\n" 71 | offset -= 1 72 | end 73 | false 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/string_quotes.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks the type of quotes used in string literals. 3 | class Linter::StringQuotes < Linter 4 | include LinterRegistry 5 | 6 | def visit_script_stringinterpolation(node) 7 | # We can't statically determine what the resultant string looks like when 8 | # string interpolation is used, e.g. "one #{$var} three" could be a very 9 | # different string depending on $var = `'" + "'` or $var = `two`. 10 | # 11 | # Thus we manually skip the substrings in the string interpolation and 12 | # visit the expressions in the interpolation itself. 13 | node.children 14 | .reject { |child| child.is_a?(Sass::Script::Tree::Literal) } 15 | .each { |child| visit(child) } 16 | end 17 | 18 | def visit_script_string(node) 19 | check_quotes(node, source_from_range(node.source_range)) 20 | end 21 | 22 | def visit_import(node) 23 | # `@import` source range conveniently includes only the quoted string 24 | check_quotes(node, source_from_range(node.source_range)) 25 | end 26 | 27 | private 28 | 29 | def check_quotes(node, source) 30 | source = source.strip 31 | string = extract_string_without_quotes(source) 32 | return unless string 33 | 34 | case source[0] 35 | when '"' 36 | check_double_quotes(node, string) 37 | when "'" 38 | check_single_quotes(node, string) 39 | end 40 | end 41 | 42 | STRING_WITHOUT_QUOTES_REGEX = %r{ 43 | \A 44 | ["'](.*)["'] # Extract text between quotes 45 | \s*\)?\s*;?\s* # Sometimes the Sass parser includes a trailing ) or ; 46 | (//.*)? # Exclude any trailing comments that might have snuck in 47 | \z 48 | }x.freeze 49 | 50 | def extract_string_without_quotes(source) 51 | return unless match = STRING_WITHOUT_QUOTES_REGEX.match(source) 52 | match[1] 53 | end 54 | 55 | def check_double_quotes(node, string) 56 | if config['style'] == 'single_quotes' 57 | add_lint(node, 'Prefer single quoted strings') if string !~ /'/ 58 | elsif string =~ /(? "0" 35 | 36 | add_lint(node, 37 | "`#{original_number}` should be written without a trailing " \ 38 | "zero as `#{fixed_number}`") 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/transition_all.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for explicitly transitioned properties instead of transition all. 3 | class Linter::TransitionAll < Linter 4 | include LinterRegistry 5 | 6 | TRANSITION_PROPERTIES = %w[ 7 | transition 8 | transition-property 9 | ].freeze 10 | 11 | def visit_prop(node) 12 | property = node.name.first.to_s 13 | return unless TRANSITION_PROPERTIES.include?(property) 14 | 15 | check_transition(node, property, node.value.first.to_sass) 16 | end 17 | 18 | private 19 | 20 | def check_transition(node, property, value) 21 | return unless offset = value =~ /\ball\b/ 22 | 23 | pos = node.value_source_range.start_pos.after(value[0, offset]) 24 | 25 | add_lint(Location.new(pos.line, pos.offset, 3), 26 | "#{property} should contain explicit properties " \ 27 | 'instead of using the keyword all') 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/unnecessary_mantissa.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SCSSLint 4 | # Checks for the unnecessary inclusion of a zero-value mantissa in numbers. 5 | # (e.g. `4.0` could be written as just `4`) 6 | class Linter::UnnecessaryMantissa < Linter 7 | include LinterRegistry 8 | 9 | def visit_script_string(node) 10 | return unless node.type == :identifier 11 | return if node.value.match?(/^'|"/) 12 | return if url_literal?(node) 13 | 14 | node.value.scan(REAL_NUMBER_REGEX) do |number, integer, mantissa, units| 15 | if unnecessary_mantissa?(mantissa) 16 | add_lint(node, MESSAGE_FORMAT % [number, integer, units]) 17 | end 18 | end 19 | end 20 | 21 | def visit_script_number(node) 22 | return unless match = REAL_NUMBER_REGEX.match(source_from_range(node.source_range)) 23 | return unless unnecessary_mantissa?(match[:mantissa]) 24 | 25 | add_lint(node, MESSAGE_FORMAT % [match[:number], match[:integer], 26 | match[:units]]) 27 | end 28 | 29 | private 30 | 31 | REAL_NUMBER_REGEX = / 32 | \b(? 33 | (?\d*) 34 | \. 35 | (?\d+) 36 | (?\w*) 37 | )\b 38 | /ix.freeze 39 | 40 | MESSAGE_FORMAT = '`%s` should be written without the mantissa as `%s%s`'.freeze 41 | 42 | def unnecessary_mantissa?(mantissa) 43 | mantissa !~ /[^0]/ 44 | end 45 | 46 | def url_literal?(node) 47 | node.value.start_with?('url(') 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/unnecessary_parent_reference.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SCSSLint 4 | # Checks for unnecessary uses of the parent reference (&) in nested selectors. 5 | class Linter::UnnecessaryParentReference < Linter 6 | include LinterRegistry 7 | 8 | MESSAGE = 'Unnecessary parent selector (&)'.freeze 9 | 10 | def visit_comma_sequence(comma_sequence) 11 | @multiple_sequences = comma_sequence.members.size > 1 12 | end 13 | 14 | def visit_sequence(sequence) 15 | return unless sequence_starts_with_parent?(sequence.members.first) 16 | 17 | # Allow concatentation, e.g. 18 | # element { 19 | # &.foo {} 20 | # } 21 | return if sequence.members.first.members.size > 1 22 | 23 | # Allow sequences that contain multiple parent references, e.g. 24 | # element { 25 | # & + & { ... } 26 | # } 27 | return if sequence.members[1..-1].any? { |ss| sequence_contains_parent_reference?(ss) } 28 | 29 | # Special case: allow an isolated parent to appear if it is part of a 30 | # comma sequence of more than one sequence, as this could be used to DRY 31 | # up code. 32 | return if @multiple_sequences && isolated_parent?(sequence) 33 | 34 | add_lint(sequence.members.first.line, MESSAGE) 35 | end 36 | 37 | private 38 | 39 | def isolated_parent?(sequence) 40 | sequence.members.size == 1 && 41 | sequence_starts_with_parent?(sequence.members.first) 42 | end 43 | 44 | def sequence_starts_with_parent?(simple_sequence) 45 | return unless simple_sequence.is_a?(Sass::Selector::SimpleSequence) 46 | first = simple_sequence.members.first 47 | first.is_a?(Sass::Selector::Parent) && 48 | first.suffix.nil? # Ignore concatenated selectors, like `&-something` 49 | end 50 | 51 | def sequence_contains_parent_reference?(simple_sequence) 52 | return unless simple_sequence.is_a?(Sass::Selector::SimpleSequence) 53 | simple_sequence.members.any? { |s| s.is_a?(Sass::Selector::Parent) } 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/url_format.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | 3 | module SCSSLint 4 | # Checks the format of URLs for unnecessary protocols or domains. 5 | class Linter::UrlFormat < Linter 6 | include LinterRegistry 7 | 8 | def visit_script_funcall(node) 9 | return unless node.name == 'url' 10 | 11 | if url_string?(node.args[0]) 12 | url = node.args[0].value.value.to_s 13 | check_url(url, node) 14 | end 15 | 16 | yield 17 | end 18 | 19 | def visit_prop(node) 20 | if url_literal?(node.value.first) 21 | url = node.value.first.to_sass.sub(/^url\((.*)\)$/, '\\1') 22 | check_url(url, node) 23 | end 24 | 25 | yield 26 | end 27 | 28 | private 29 | 30 | def url_literal?(prop_value) 31 | return unless prop_value.is_a?(Sass::Script::Tree::Literal) 32 | return unless prop_value.value.is_a?(Sass::Script::Value::String) 33 | return unless prop_value.value.type == :identifier 34 | 35 | prop_value.to_sass.start_with?('url(') 36 | end 37 | 38 | def url_string?(arg) 39 | return unless arg.is_a?(Sass::Script::Tree::Literal) 40 | return unless arg.value.is_a?(Sass::Script::Value::String) 41 | 42 | arg.value.type == :string 43 | end 44 | 45 | def check_url(url, node) 46 | return if url.start_with?('data:') || url.include?('${') 47 | uri = URI(url) 48 | 49 | if uri.scheme || uri.host 50 | add_lint(node, "URL `#{url}` should not contain protocol or domain") 51 | end 52 | rescue URI::Error => e 53 | add_lint(node, "Invalid URL `#{url}`: #{e}") 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/url_quotes.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for quotes in URLs. 3 | class Linter::UrlQuotes < Linter 4 | include LinterRegistry 5 | 6 | def visit_prop(node) 7 | case node.value.first 8 | when Sass::Script::Tree::Literal 9 | check(node, node.value.first.value.to_s) 10 | when Sass::Script::Tree::ListLiteral 11 | node.value.first 12 | .children 13 | .select { |child| child.is_a?(Sass::Script::Tree::Literal) } 14 | .each { |child| check(node, child.value.to_s) } 15 | end 16 | 17 | yield 18 | end 19 | 20 | private 21 | 22 | def check(node, string) 23 | return unless string.match?(/^\s*url\(\s*[^"']/) 24 | return if string.match?(/^\s*url\(\s*data:/) # Ignore data URIs 25 | 26 | add_lint(node, 'URLs should be enclosed in quotes') 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/variable_for_property.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Reports the use of literals for properties where variables are prefered. 3 | class Linter::VariableForProperty < Linter 4 | include LinterRegistry 5 | 6 | IGNORED_VALUES = %w[currentColor inherit initial transparent].freeze 7 | 8 | def visit_root(_node) 9 | @properties = Set.new(config['properties']) 10 | yield if @properties.any? 11 | end 12 | 13 | def visit_prop(node) 14 | property_name = node.name.join 15 | return unless @properties.include?(property_name) 16 | return if ignored_value?(node.value.first) 17 | return if node.children.first.is_a?(Sass::Script::Tree::Variable) 18 | return if variable_property_with_important?(node.value.first) 19 | 20 | add_lint(node, "Property #{property_name} should use " \ 21 | 'a variable rather than a literal value') 22 | end 23 | 24 | private 25 | 26 | def variable_property_with_important?(value) 27 | value.is_a?(Sass::Script::Tree::ListLiteral) && 28 | value.children.length == 2 && 29 | value.children.first.is_a?(Sass::Script::Tree::Variable) && 30 | value.children.last.value.value == '!important' 31 | end 32 | 33 | def ignored_value?(value) 34 | value.respond_to?(:value) && 35 | IGNORED_VALUES.include?(value.value.to_s) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/vendor_prefix.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Checks for vendor prefixes. 3 | class Linter::VendorPrefix < Linter 4 | include LinterRegistry 5 | 6 | def visit_root(_node) 7 | @identifiers = Set.new(extract_identifiers_from_config) 8 | @identifiers.merge(Set.new(config['additional_identifiers'])) 9 | @exclusions = Set.new(config['excluded_identifiers']) 10 | yield 11 | end 12 | 13 | def check_node(node) 14 | name = node.name.is_a?(Array) ? node.name.join : node.name 15 | # Ignore '@' from @keyframes node name 16 | check_identifier(node, name.sub(/^@/, '')) 17 | 18 | # Check for values 19 | return unless node.respond_to?(:value) && node.value.first.respond_to?(:source_range) 20 | check_identifier(node, source_from_range(node.value.first.source_range)) 21 | end 22 | 23 | alias visit_prop check_node 24 | alias visit_pseudo check_node 25 | alias visit_directive check_node 26 | 27 | private 28 | 29 | def check_identifier(node, identifier) 30 | return unless identifier.match?(/^[_-]/) 31 | 32 | # Strip vendor prefix to check against identifiers. 33 | # (Also strip closing parentheticals from values like linear-gradient.) 34 | stripped_identifier = identifier.gsub(/(^[_-][a-zA-Z0-9_]+-|\(.*\)|;)/, '').strip 35 | return if @exclusions.include?(stripped_identifier) 36 | return unless @identifiers.include?(stripped_identifier) 37 | 38 | add_lint(node, 'Avoid vendor prefixes.') 39 | end 40 | 41 | def extract_identifiers_from_config 42 | case config['identifier_list'] 43 | when nil 44 | nil 45 | when Array 46 | config['identifier_list'] 47 | when String 48 | begin 49 | file = File.open(File.join(SCSS_LINT_DATA, 50 | 'prefixed-identifiers', 51 | "#{config['identifier_list']}.txt")) 52 | file.read.split("\n").reject { |line| line =~ /^(#|\s*$)/ } 53 | rescue Errno::ENOENT 54 | raise SCSSLint::Exceptions::LinterError, 55 | "Identifier list '#{config['identifier_list']}' does not exist" 56 | end 57 | else 58 | raise SCSSLint::Exceptions::LinterError, 59 | 'Invalid identifier list specified -- must be the name of a '\ 60 | 'preset or an array of strings' 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/scss_lint/linter/zero_unit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SCSSLint 4 | # Checks for unnecessary units on zero values. 5 | class Linter::ZeroUnit < Linter 6 | include LinterRegistry 7 | 8 | def visit_script_string(node) 9 | return unless node.type == :identifier 10 | return if node.value.start_with?('calc(') 11 | 12 | node.value.scan(ZERO_UNIT_REGEX) do |match| 13 | next unless zero_with_length_units?(match.first) 14 | add_lint(node, MESSAGE_FORMAT % match.first) 15 | end 16 | end 17 | 18 | def visit_script_number(node) 19 | length = source_from_range(node.source_range)[ZERO_UNIT_REGEX, 1] 20 | return unless zero_with_length_units?(length) 21 | 22 | add_lint(node, MESSAGE_FORMAT % length) 23 | end 24 | 25 | def visit_script_funcall(node) 26 | # Don't report errors for values within `calc` expressions, since they 27 | # require units in order to work 28 | yield unless node.name == 'calc' 29 | end 30 | 31 | private 32 | 33 | ZERO_UNIT_REGEX = / 34 | \b 35 | (?(other) 30 | %i[line column length].each do |attr| 31 | result = send(attr) <=> other.send(attr) 32 | return result unless result == 0 33 | end 34 | 35 | 0 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/scss_lint/plugins.rb: -------------------------------------------------------------------------------- 1 | require_relative 'plugins/linter_gem' 2 | require_relative 'plugins/linter_dir' 3 | 4 | module SCSSLint 5 | # Loads external linter plugins. 6 | class Plugins 7 | def initialize(config) 8 | @config = config 9 | end 10 | 11 | def load 12 | all.map(&:load) 13 | end 14 | 15 | private 16 | 17 | def all 18 | [plugin_gems, plugin_directories].flatten 19 | end 20 | 21 | def plugin_gems 22 | Array(@config['plugin_gems']).map do |gem_name| 23 | LinterGem.new(gem_name) 24 | end 25 | end 26 | 27 | def plugin_directories 28 | Array(@config['plugin_directories']).map do |directory| 29 | LinterDir.new(File.join(File.dirname(@config.file), directory)) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/scss_lint/plugins/linter_dir.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | class Plugins 3 | # Load ruby files from linter plugin directories. 4 | class LinterDir 5 | attr_reader :config 6 | 7 | def initialize(dir) 8 | @dir = dir 9 | end 10 | 11 | def load 12 | ruby_files.each { |file| require file } 13 | @config = plugin_config 14 | self 15 | end 16 | 17 | private 18 | 19 | def ruby_files 20 | Dir.glob(File.expand_path(File.join(@dir, '**', '*.rb'))) 21 | end 22 | 23 | # Returns the {SCSSLint::Config} for this directory. 24 | # 25 | # This is intended to be merged with the configuration that loaded this 26 | # plugin. 27 | # 28 | # @return [SCSSLint::Config] 29 | def plugin_config 30 | file = plugin_config_file 31 | 32 | if File.exist?(file) 33 | Config.load(file, merge_with_default: false) 34 | else 35 | Config.new({}) 36 | end 37 | end 38 | 39 | # Path of the configuration file to attempt to load for this directory. 40 | # 41 | # @return [String] 42 | def plugin_config_file 43 | File.join(@dir, Config::FILE_NAME) 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/scss_lint/plugins/linter_gem.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | class Plugins 3 | # Load linter plugin gems 4 | class LinterGem 5 | attr_reader :config 6 | 7 | def initialize(name) 8 | @name = name 9 | end 10 | 11 | def load 12 | require @name 13 | @config = plugin_config 14 | self 15 | rescue LoadError 16 | raise SCSSLint::Exceptions::PluginGemLoadError, 17 | "Unable to load linter plugin gem '#{@name}'. Try running " \ 18 | "`gem install #{@name}`, or adding it to your Gemfile and " \ 19 | 'running `bundle install`. See the `plugin_gems` section of ' \ 20 | 'your .scss-lint.yml file to add/remove gem plugins.' 21 | end 22 | 23 | private 24 | 25 | # Returns the {SCSSLint::Config} for this plugin. 26 | # 27 | # This is intended to be merged with the configuration that loaded this 28 | # plugin. 29 | # 30 | # @return [SCSSLint::Config] 31 | def plugin_config 32 | file = plugin_config_file 33 | 34 | if File.exist?(file) 35 | Config.load(file, merge_with_default: false) 36 | else 37 | Config.new({}) 38 | end 39 | end 40 | 41 | # Path of the configuration file to attempt to load for this plugin. 42 | # 43 | # @return [String] 44 | def plugin_config_file 45 | gem_specification = Gem::Specification.find_by_name(@name) 46 | 47 | File.join(gem_specification.gem_dir, Config::FILE_NAME) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/scss_lint/reporter.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Responsible for displaying lints to the user in some format. 3 | class Reporter 4 | attr_reader :lints, :files, :log 5 | 6 | def self.descendants 7 | ObjectSpace.each_object(Class).select { |klass| klass < self } 8 | end 9 | 10 | # @param lints [List] a list of Lints sorted by file and line number 11 | # @param files [List] a list of the files that were linted 12 | # @param logger [SCSSLint::Logger] 13 | def initialize(lints, files, logger) 14 | @lints = lints 15 | @files = files 16 | @log = logger 17 | end 18 | 19 | def report_lints 20 | raise NotImplementedError, 'You must implement report_lints' 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/scss_lint/reporter/clean_files_reporter.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Reports a single line for each clean file (having zero lints). 3 | class Reporter::CleanFilesReporter < Reporter 4 | def report_lints 5 | dirty_files = lints.map(&:filename).uniq 6 | clean_files = files.map { |e| e['path'] } - dirty_files 7 | "#{clean_files.sort.join("\n")}\n" if clean_files.any? 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/scss_lint/reporter/config_reporter.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Returns a YAML configuration where all linters are disabled which 3 | # caused a lint. 4 | class Reporter::ConfigReporter < Reporter 5 | def report_lints 6 | { 'linters' => disabled_linters }.to_yaml unless lints.empty? 7 | end 8 | 9 | private 10 | 11 | def disabled_linters 12 | linters.each_with_object({}) do |linter, m| 13 | m[linter] = { 'enabled' => false } 14 | end 15 | end 16 | 17 | def linters 18 | lints.map { |lint| linter_name(lint.linter) }.compact.uniq.sort 19 | end 20 | 21 | def linter_name(linter) 22 | linter.class.to_s.split('::').last 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/scss_lint/reporter/default_reporter.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Reports a single line per lint. 3 | class Reporter::DefaultReporter < Reporter 4 | def report_lints 5 | return unless lints.any? 6 | 7 | "#{lints.map { |lint| "#{location(lint)} #{type(lint)} #{message(lint)}" }.join("\n")}\n" 8 | end 9 | 10 | private 11 | 12 | def location(lint) 13 | [ 14 | log.cyan(lint.filename), 15 | log.magenta(lint.location.line.to_s), 16 | log.magenta(lint.location.column.to_s), 17 | ].join(':') 18 | end 19 | 20 | def type(lint) 21 | lint.error? ? log.red('[E]') : log.yellow('[W]') 22 | end 23 | 24 | def message(lint) 25 | linter_name = log.green("#{lint.linter.name}: ") 26 | "#{linter_name}#{lint.description}" 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/scss_lint/reporter/files_reporter.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Reports a single line per file. 3 | class Reporter::FilesReporter < Reporter 4 | def report_lints 5 | "#{lints.map(&:filename).uniq.join("\n")}\n" if lints.any? 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/scss_lint/reporter/json_reporter.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module SCSSLint 4 | # Reports lints in a JSON format. 5 | class Reporter::JSONReporter < Reporter 6 | def report_lints 7 | output = {} 8 | lints.group_by(&:filename).each do |filename, file_lints| 9 | output[filename] = file_lints.map do |lint| 10 | issue_hash(lint) 11 | end 12 | end 13 | JSON.pretty_generate(output) 14 | end 15 | 16 | private 17 | 18 | def issue_hash(lint) 19 | { 20 | 'line' => lint.location.line, 21 | 'column' => lint.location.column, 22 | 'length' => lint.location.length, 23 | 'severity' => lint.severity, 24 | 'reason' => lint.description, 25 | }.tap do |hash| 26 | hash['linter'] = lint.linter.name 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/scss_lint/reporter/stats_reporter.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Reports a single line per lint. 3 | class Reporter::StatsReporter < Reporter 4 | def report_lints # rubocop:disable Metrics/AbcSize 5 | return unless lints.any? 6 | 7 | stats = organize_stats 8 | total_lints = lints.length 9 | linter_name_length = 10 | stats.inject('') { |memo, stat| memo.length > stat[0].length ? memo : stat[0] }.length 11 | total_files = lints.group_by(&:filename).size 12 | 13 | # Math.log10(1) is 0; avoid this by using at least 1. 14 | lint_count_length = [1, Math.log10(total_lints).ceil].max 15 | file_count_length = [1, Math.log10(total_files).ceil].max 16 | 17 | str = '' 18 | stats.each do |linter_name, lint_count, file_count| 19 | str << "%#{lint_count_length}d %-#{linter_name_length}s" % [lint_count, linter_name] 20 | str << " (across %#{file_count_length}d files)\n" % [file_count] 21 | end 22 | str << "#{'-' * lint_count_length} #{'-' * linter_name_length}" 23 | str << " #{'-' * (file_count_length + 15)}\n" 24 | str << "%#{lint_count_length}d #{'total'.ljust(linter_name_length)}" % total_lints 25 | str << " (across %#{file_count_length}d files)\n" % total_files 26 | str 27 | end 28 | 29 | def organize_stats 30 | lints 31 | .group_by(&:linter) 32 | .sort_by { |_, lints_by_linter| -lints_by_linter.size } 33 | .inject([]) do |ary, (linter, lints_by_linter)| 34 | file_count = lints_by_linter.group_by(&:filename).size 35 | ary << [linter.name, lints_by_linter.size, file_count] 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/scss_lint/runner.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Finds and aggregates all lints found by running the registered linters 3 | # against a set of SCSS files. 4 | class Runner 5 | attr_reader :lints, :files 6 | 7 | # @param config [Config] 8 | def initialize(config) 9 | @config = config 10 | @lints = [] 11 | @linters = LinterRegistry.linters.select { |linter| @config.linter_enabled?(linter) } 12 | @linters.map!(&:new) 13 | end 14 | 15 | # @param files [Array] list of file object/path hashes 16 | def run(files) 17 | @files = files 18 | @files.each do |file| 19 | find_lints(file) 20 | end 21 | end 22 | 23 | private 24 | 25 | # @param file [Hash] 26 | # @option file [String] File object 27 | # @option path [String] path to File (determines which Linter config to apply) 28 | def find_lints(file) # rubocop:disable Metrics/AbcSize 29 | options = file.merge(preprocess_command: @config.options['preprocess_command'], 30 | preprocess_files: @config.options['preprocess_files']) 31 | engine = Engine.new(options) 32 | 33 | @linters.each do |linter| 34 | begin 35 | run_linter(linter, engine, file[:path]) 36 | rescue StandardError => e 37 | raise SCSSLint::Exceptions::LinterError, 38 | "#{linter.class} raised unexpected error linting file #{file[:path]}: " \ 39 | "'#{e.message}'", 40 | e.backtrace 41 | end 42 | end 43 | rescue Sass::SyntaxError => e 44 | @lints << Lint.new(Linter::Syntax.new, e.sass_filename, Location.new(e.sass_line), 45 | "Syntax Error: #{e}", :error) 46 | rescue FileEncodingError => e 47 | @lints << Lint.new(Linter::Encoding.new, file[:path], Location.new, e.to_s, :error) 48 | end 49 | 50 | # For stubbing in tests. 51 | def run_linter(linter, engine, file_path) 52 | return if @config.excluded_file_for_linter?(file_path, linter) 53 | @lints += linter.run(engine, @config.linter_options(linter)) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/scss_lint/sass/script.rb: -------------------------------------------------------------------------------- 1 | # Ignore documentation lints as these aren't original implementations. 2 | # rubocop:disable Style/Documentation 3 | 4 | module Sass::Script 5 | # Since the Sass library is already loaded at this point. 6 | # Define the `node_name` and `visit_method` class methods for each Sass Script 7 | # parse tree node type so that our custom visitor can seamless traverse the 8 | # tree. 9 | # Define the `invalid_child_method_name` and `invalid_parent_method_name` 10 | # class methods to make errors understandable. 11 | # 12 | # This would be easier if we could just define an `inherited` callback, but 13 | # that won't work since the Sass library will have already been loaded before 14 | # this code gets loaded, so the `inherited` callback won't be fired. 15 | # 16 | # Thus we are left to manually define the methods for each type explicitly. 17 | { 18 | 'Value' => %w[ArgList Bool Color List Map Null Number String], 19 | 'Tree' => %w[Funcall Interpolation ListLiteral Literal MapLiteral 20 | Operation Selector StringInterpolation UnaryOperation Variable], 21 | }.each do |namespace, types| 22 | types.each do |type| 23 | node_name = type.downcase 24 | 25 | eval <<-DECL, binding, __FILE__, __LINE__ + 1 26 | class #{namespace}::#{type} 27 | def self.node_name 28 | :script_#{node_name} 29 | end 30 | 31 | def self.visit_method 32 | :visit_script_#{node_name} 33 | end 34 | 35 | def self.invalid_child_method_name 36 | :"invalid_#{node_name}_child?" 37 | end 38 | 39 | def self.invalid_parent_method_name 40 | :"invalid_#{node_name}_parent?" 41 | end 42 | end 43 | DECL 44 | end 45 | end 46 | 47 | class Value::Base 48 | attr_accessor :node_parent 49 | 50 | def children 51 | [] 52 | end 53 | 54 | def line 55 | @line || (node_parent && node_parent.line) 56 | end 57 | 58 | def source_range 59 | @source_range || (node_parent && node_parent.source_range) 60 | end 61 | end 62 | 63 | # Contains extensions of Sass::Script::Tree::Nodes to add support for 64 | # accessing various parts of the parse tree not provided out-of-the-box. 65 | module Tree 66 | class Node 67 | attr_accessor :node_parent 68 | end 69 | 70 | class Literal 71 | # Literals wrap their underlying values. For sake of convenience, consider 72 | # the wrapped value a child of the Literal. 73 | def children 74 | [value] 75 | end 76 | end 77 | end 78 | end 79 | 80 | # rubocop:enable Style/Documentation 81 | -------------------------------------------------------------------------------- /lib/scss_lint/selector_visitor.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | # Provides functionality for conveniently visiting a Selector sequence. 3 | module SelectorVisitor 4 | def visit_selector(node) 5 | visit_selector_node(node) 6 | end 7 | 8 | private 9 | 10 | def visit_selector_node(node) 11 | method = "visit_#{selector_node_name(node)}" 12 | send(method, node) if respond_to?(method, true) 13 | 14 | visit_members(node) if node.is_a?(Sass::Selector::AbstractSequence) 15 | end 16 | 17 | def visit_members(sequence) 18 | sequence.members 19 | .reject { |member| member.is_a?(String) } # Skip newlines in multi-line comma seqs 20 | .each do |member| 21 | visit_selector(member) 22 | end 23 | end 24 | 25 | # The class name of a node, in snake_case form, e.g. 26 | # `Sass::Selector::SimpleSequence` -> `simple_sequence`. 27 | # 28 | # The name is memoized as a class variable on the node itself. 29 | def selector_node_name(node) 30 | if node.class.class_variable_defined?(:@@snake_case_name) 31 | return node.class.class_variable_get(:@@snake_case_name) 32 | end 33 | 34 | rindex = node.class.name.rindex('::') 35 | name = node.class.name[(rindex + 2)..-1] 36 | name.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2') 37 | name.gsub!(/([a-z\d])([A-Z])/, '\1_\2') 38 | name.downcase! 39 | node.class.class_variable_set(:@@snake_case_name, name) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/scss_lint/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Defines the gem version. 4 | module SCSSLint 5 | VERSION = '0.60.0'.freeze 6 | end 7 | -------------------------------------------------------------------------------- /logo/horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sds/scss-lint/9099df2a76c24a36fd53a258505ce4ce10dbf3aa/logo/horizontal.png -------------------------------------------------------------------------------- /scss_lint.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.expand_path('lib', __dir__) 2 | require 'scss_lint/constants' 3 | require 'scss_lint/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'scss_lint' 7 | s.version = SCSSLint::VERSION 8 | s.license = 'MIT' 9 | s.summary = 'SCSS lint tool' 10 | s.description = 'Configurable tool for writing clean and consistent SCSS' 11 | s.authors = ['Shane da Silva'] 12 | s.email = ['shane@dasilva.io'] 13 | s.homepage = SCSSLint::REPO_URL 14 | 15 | s.require_paths = ['lib'] 16 | 17 | s.executables = ['scss-lint'] 18 | 19 | s.files = Dir['config/**/*.yml'] + 20 | Dir['data/**/*'] + 21 | Dir['lib/**/*.rb'] + 22 | ['MIT-LICENSE'] 23 | 24 | s.test_files = Dir['spec/**/*'] 25 | 26 | s.required_ruby_version = '>= 2.4' 27 | 28 | s.add_dependency 'sass', '~> 3.5', '>= 3.5.5' 29 | end 30 | -------------------------------------------------------------------------------- /spec/scss_lint/engine_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Engine do 4 | let(:engine) { described_class.new(code: scss) } 5 | 6 | context 'when a @media directive is present' do 7 | let(:scss) { <<-SCSS } 8 | @media only screen { 9 | } 10 | SCSS 11 | 12 | it 'has a parse tree' do 13 | engine.tree.should_not be_nil 14 | end 15 | end 16 | 17 | context 'when a custom property is present' do 18 | let(:scss) { <<-SCSS } 19 | :root { 20 | --my-font-family: Helvetica; 21 | } 22 | SCSS 23 | 24 | it 'has a parse tree' do 25 | engine.tree.should_not be_nil 26 | end 27 | end 28 | 29 | context 'when the file being linted has an invalid byte sequence' do 30 | let(:scss) { "\xC0\u0001" } 31 | 32 | it 'raises a SyntaxError' do 33 | expect { engine }.to raise_error(SCSSLint::FileEncodingError) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/scss_lint/file_finder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fileutils' 3 | 4 | describe SCSSLint::FileFinder do 5 | let(:config) { SCSSLint::Config.default } 6 | 7 | subject { described_class.new(config) } 8 | 9 | describe '#find' do 10 | include_context 'isolated environment' 11 | 12 | subject { super().find(patterns) } 13 | 14 | context 'when no patterns are given' do 15 | let(:patterns) { [] } 16 | 17 | it 'raises an error' do 18 | expect { subject }.to raise_error SCSSLint::Exceptions::NoFilesError 19 | end 20 | end 21 | 22 | context 'when files without valid extension are given' do 23 | let(:patterns) { ['test.txt'] } 24 | 25 | context 'and those files exist' do 26 | before do 27 | FileUtils.touch('test.txt') 28 | end 29 | 30 | it { should == ['test.txt'] } 31 | end 32 | 33 | context 'and those files do not exist' do 34 | it { should == ['test.txt'] } 35 | end 36 | end 37 | 38 | context 'when directories are given' do 39 | let(:patterns) { ['some-dir'] } 40 | 41 | context 'and those directories exist' do 42 | before do 43 | `mkdir -p some-dir` 44 | end 45 | 46 | context 'and they contain SCSS files' do 47 | before do 48 | FileUtils.touch(File.join('some-dir', 'test.scss')) 49 | end 50 | 51 | it { should == [File.join('some-dir', 'test.scss')] } 52 | 53 | context 'and those SCSS files are excluded by the config' do 54 | before do 55 | config.exclude_file('some-dir/test.scss') 56 | end 57 | 58 | it { should == [] } 59 | end 60 | end 61 | 62 | context 'and they contain CSS files' do 63 | before do 64 | FileUtils.touch(File.join('some-dir', 'test.css')) 65 | end 66 | 67 | it { should == [File.join('some-dir', 'test.css')] } 68 | end 69 | 70 | context 'and they contain more directories with files with recognized extensions' do 71 | before do 72 | `mkdir -p some-dir/more-dir` 73 | FileUtils.mkdir_p(File.join('some-dir', 'more-dir')) 74 | FileUtils.touch(File.join('some-dir', 'more-dir', 'test.scss')) 75 | end 76 | 77 | it { should == [File.join('some-dir', 'more-dir', 'test.scss')] } 78 | 79 | context 'and those SCSS files are excluded by the config' do 80 | before do 81 | config.exclude_file('**/*.scss') 82 | end 83 | 84 | it { should == [] } 85 | end 86 | end 87 | 88 | context 'and they contain no SCSS files' do 89 | before do 90 | FileUtils.touch(File.join('some-dir', 'test.txt')) 91 | end 92 | 93 | it 'raises an error' do 94 | expect { subject }.to raise_error SCSSLint::Exceptions::NoFilesError 95 | end 96 | end 97 | end 98 | 99 | context 'and those directories do not exist' do 100 | it { should == ['some-dir'] } 101 | end 102 | end 103 | 104 | context 'when the same file is specified multiple times' do 105 | let(:patterns) { ['test.scss'] * 3 } 106 | 107 | it { should == ['test.scss'] } 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /spec/scss_lint/fixtures/plugins/linter_plugin.rb: -------------------------------------------------------------------------------- 1 | module SCSSLint 2 | class Linter 3 | class LinterPlugin < Linter 4 | include LinterRegistry 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/chained_classes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::ChainedClasses do 4 | context 'with a single class' do 5 | let(:scss) { <<-SCSS } 6 | .class {} 7 | SCSS 8 | 9 | it { should_not report_lint } 10 | end 11 | 12 | context 'with a single class with a descendant ' do 13 | let(:scss) { <<-SCSS } 14 | .class .descendant {} 15 | SCSS 16 | 17 | it { should_not report_lint } 18 | end 19 | 20 | context 'with a chained class' do 21 | let(:scss) { <<-SCSS } 22 | .chained.class {} 23 | SCSS 24 | 25 | it { should report_lint line: 1 } 26 | end 27 | 28 | context 'with a chained class in a nested rule set' do 29 | let(:scss) { <<-SCSS } 30 | p { 31 | .chained.class {} 32 | } 33 | SCSS 34 | 35 | it { should report_lint line: 2 } 36 | end 37 | 38 | context 'with a chained class in part of a sequence' do 39 | let(:scss) { <<-SCSS } 40 | .some .sequence .with .chained.class .in .it {} 41 | SCSS 42 | 43 | it { should report_lint line: 1 } 44 | end 45 | 46 | context 'with a chained class in a multiline comma sequence' do 47 | let(:scss) { <<-SCSS } 48 | .one, 49 | .two.three {} 50 | SCSS 51 | 52 | it { should report_lint line: 2 } 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/color_keyword_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::ColorKeyword do 4 | context 'when a color is specified as a hex' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | color: #fff; 8 | } 9 | SCSS 10 | 11 | it { should_not report_lint } 12 | end 13 | 14 | context 'when a color is specified as a keyword' do 15 | let(:scss) { <<-SCSS } 16 | p { 17 | color: white; 18 | } 19 | SCSS 20 | 21 | it { should report_lint line: 2 } 22 | end 23 | 24 | context 'when a color keyword exists in a shorthand property' do 25 | let(:scss) { <<-SCSS } 26 | p { 27 | border: 1px solid black; 28 | } 29 | SCSS 30 | 31 | it { should report_lint line: 2 } 32 | end 33 | 34 | context 'when a property contains a color keyword as a string' do 35 | let(:scss) { <<-SCSS } 36 | p { 37 | content: 'white'; 38 | } 39 | SCSS 40 | 41 | it { should_not report_lint } 42 | end 43 | 44 | context 'when a function call contains a color keyword' do 45 | let(:scss) { <<-SCSS } 46 | p { 47 | color: function(red); 48 | } 49 | SCSS 50 | 51 | it { should report_lint line: 2 } 52 | end 53 | 54 | context 'when a mixin include contains a color keyword' do 55 | let(:scss) { <<-SCSS } 56 | p { 57 | @include some-mixin(red); 58 | } 59 | SCSS 60 | 61 | it { should report_lint line: 2 } 62 | end 63 | 64 | context 'when the "transparent" color keyword is used' do 65 | let(:scss) { <<-SCSS } 66 | p { 67 | @include mixin(transparent); 68 | } 69 | SCSS 70 | 71 | it { should_not report_lint } 72 | end 73 | 74 | context 'when color keyword appears in a string identifier' do 75 | let(:scss) { <<-SCSS } 76 | p { 77 | content: content-with-blue-in-name; 78 | } 79 | SCSS 80 | 81 | it { should_not report_lint } 82 | end 83 | 84 | context 'when a color keyword is used in a map declaration as keys' do 85 | let(:scss) { <<-SCSS } 86 | $palette: ( 87 | white: ( 88 | first: #fff, 89 | second: #ccc, 90 | third: #000 91 | ), 92 | 'black': ( 93 | first: #000, 94 | second: #ccc, 95 | third: #fff 96 | ) 97 | ); 98 | SCSS 99 | 100 | it { should_not report_lint } 101 | end 102 | 103 | context 'when color keyword is used in a map function call' do 104 | let(:scss) { <<-SCSS } 105 | p { 106 | color: map-get($my-colors, green); 107 | } 108 | SCSS 109 | 110 | it { should_not report_lint } 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/comment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::Comment do 4 | context 'when no comments exist' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | margin: 0; 8 | } 9 | SCSS 10 | 11 | it { should_not report_lint } 12 | end 13 | 14 | context 'when comment is a single line comment' do 15 | let(:scss) { '// Single line comment' } 16 | 17 | it { should_not report_lint } 18 | end 19 | 20 | context 'when comment is a single line comment at the end of a line' do 21 | let(:scss) { <<-SCSS } 22 | p { 23 | margin: 0; // Comment at end of line 24 | } 25 | SCSS 26 | 27 | it { should_not report_lint } 28 | end 29 | 30 | context 'when comment is a multi-line comment' do 31 | let(:scss) { <<-SCSS } 32 | h1 { 33 | color: #eee; 34 | } 35 | /* 36 | * This is a multi-line comment that should report a lint 37 | */ 38 | p { 39 | color: #DDD; 40 | } 41 | SCSS 42 | 43 | it { should report_lint line: 4 } 44 | end 45 | 46 | context 'when multi-line-style comment is a at the end of a line' do 47 | let(:scss) { <<-SCSS } 48 | h1 { 49 | color: #eee; /* This is a comment */ 50 | } 51 | SCSS 52 | 53 | it { should report_lint line: 2 } 54 | end 55 | 56 | context 'when multi-line comment is allowed by config' do 57 | let(:linter_config) { { 'allowed' => '^[/\\* ]*Copyright' } } 58 | let(:scss) { <<-SCSS } 59 | /* Copyright someone. */ 60 | a { 61 | color: #DDD; 62 | } 63 | SCSS 64 | 65 | it { should_not report_lint } 66 | end 67 | 68 | context 'when multi-line comment is not allowed by config' do 69 | let(:linter_config) { { 'allowed' => '^[/\\* ]*Copyright' } } 70 | let(:scss) { <<-SCSS } 71 | /* Other multiline. */ 72 | p { 73 | color: #DDD; 74 | } 75 | SCSS 76 | 77 | it { should report_lint } 78 | end 79 | 80 | context 'when multi-line comments are preferred' do 81 | let(:linter_config) { { 'style' => 'loud' } } 82 | 83 | context 'and silent comments are present' do 84 | let(:scss) { '// A silent comment' } 85 | 86 | it { should report_lint } 87 | end 88 | 89 | context 'and loud comments are present' do 90 | let(:scss) { '/* A loud comment */' } 91 | 92 | it { should_not report_lint } 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/compass/property_with_mixin_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::Compass::PropertyWithMixin do 4 | context 'when a rule has a property with an equivalent Compass mixin' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | opacity: .5; 8 | } 9 | SCSS 10 | 11 | it { should report_lint line: 2 } 12 | end 13 | 14 | context 'when a rule includes a Compass property mixin' do 15 | let(:scss) { <<-SCSS } 16 | p { 17 | @include opacity(.5); 18 | } 19 | SCSS 20 | 21 | it { should_not report_lint } 22 | end 23 | 24 | context 'when a rule does not have a property with a corresponding Compass mixin' do 25 | let(:scss) { <<-SCSS } 26 | p { 27 | margin: 0; 28 | } 29 | SCSS 30 | 31 | it { should_not report_lint } 32 | end 33 | 34 | context 'when a rule includes display: inline-block' do 35 | let(:scss) { <<-SCSS } 36 | p { 37 | display: inline-block; 38 | } 39 | SCSS 40 | 41 | it { should report_lint line: 2 } 42 | end 43 | 44 | context 'when properties are ignored' do 45 | let(:linter_config) { { 'ignore' => %w[inline-block] } } 46 | 47 | let(:scss) { <<-SCSS } 48 | p { 49 | display: inline-block; 50 | } 51 | SCSS 52 | 53 | it { should_not report_lint } 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/debug_statement_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::DebugStatement do 4 | context 'when no debug statements are present' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | color: #fff; 8 | } 9 | SCSS 10 | 11 | it { should_not report_lint } 12 | end 13 | 14 | context 'when a debug statement is present' do 15 | let(:scss) { <<-SCSS } 16 | @debug 'This is a debug statement'; 17 | SCSS 18 | 19 | it { should report_lint line: 1 } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/disable_linter_reason_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::DisableLinterReason do 4 | context 'when no disabling instructions exist' do 5 | let(:scss) { <<-SCSS } 6 | // Comment. 7 | p { 8 | margin: 0; 9 | } 10 | SCSS 11 | 12 | it { should_not report_lint } 13 | end 14 | 15 | context 'when no reason accompanies a disabling comment' do 16 | let(:scss) { <<-SCSS } 17 | // scss-lint:disable BorderZero 18 | p { 19 | margin: 0; 20 | } 21 | SCSS 22 | 23 | it { should report_lint line: 1 } 24 | end 25 | 26 | context 'when a reason immediately precedes a disabling comment' do 27 | let(:scss) { <<-SCSS } 28 | // We like using `border: none` in our CSS. 29 | // scss-lint:disable BorderZero 30 | p { 31 | margin: 0; 32 | } 33 | SCSS 34 | 35 | it { should_not report_lint } 36 | end 37 | 38 | context 'when a reason precedes a disabling comment, at a distance' do 39 | let(:scss) { <<-SCSS } 40 | // We like using `border: none` in our CSS. 41 | 42 | // scss-lint:disable BorderZero 43 | p { 44 | margin: 0; 45 | } 46 | SCSS 47 | 48 | it { should_not report_lint } 49 | end 50 | 51 | context 'when no reason precedes an enabling comment' do 52 | let(:scss) { <<-SCSS } 53 | // Disable for now 54 | // scss-lint:disable BorderZero 55 | p { 56 | border: none; 57 | } 58 | // scss-lint:enable BorderZero 59 | SCSS 60 | 61 | it { should_not report_lint } 62 | end 63 | 64 | context 'when a reason precedes an inline disabling comment' do 65 | let(:scss) { <<-SCSS } 66 | p { 67 | // Disable for now 68 | border: none; // scss-lint:disable BorderZero 69 | } 70 | SCSS 71 | 72 | it { should_not report_lint } 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/empty_rule_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::EmptyRule do 4 | context 'when rule is empty' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | } 8 | SCSS 9 | 10 | it { should report_lint line: 1 } 11 | end 12 | 13 | context 'when rule contains an empty nested rule' do 14 | let(:scss) { <<-SCSS } 15 | p { 16 | background: #000; 17 | display: none; 18 | margin: 5px; 19 | padding: 10px; 20 | a { 21 | } 22 | } 23 | SCSS 24 | 25 | it { should report_lint line: 6 } 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/extend_directive_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::ExtendDirective do 4 | context 'when extending with a class' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | @extend .error; 8 | } 9 | SCSS 10 | 11 | it { should report_lint line: 2 } 12 | end 13 | 14 | context 'when extending with a type' do 15 | let(:scss) { <<-SCSS } 16 | p { 17 | @extend span; 18 | } 19 | SCSS 20 | 21 | it { should report_lint line: 2 } 22 | end 23 | 24 | context 'when extending with an id' do 25 | let(:scss) { <<-SCSS } 26 | p { 27 | @extend #some-identifer; 28 | } 29 | SCSS 30 | 31 | it { should report_lint line: 2 } 32 | end 33 | 34 | context 'when extending with a placeholder' do 35 | let(:scss) { <<-SCSS } 36 | p { 37 | @extend %placeholder; 38 | } 39 | SCSS 40 | 41 | it { should report_lint line: 2 } 42 | end 43 | 44 | context 'when extending with a selector whose prefix is not a placeholder' do 45 | let(:scss) { <<-SCSS } 46 | p { 47 | @extend .blah-\#{$dynamically_generated_name}; 48 | } 49 | SCSS 50 | 51 | it { should report_lint line: 2 } 52 | end 53 | 54 | context 'when extending with a selector whose prefix is dynamic' do 55 | let(:scss) { <<-SCSS } 56 | p { 57 | @extend \#{$dynamically_generated_placeholder_name}; 58 | } 59 | SCSS 60 | 61 | it { should report_lint line: 2 } 62 | end 63 | 64 | context 'when not using extend' do 65 | let(:scss) { <<-SCSS } 66 | p { 67 | @include mixin; 68 | } 69 | SCSS 70 | 71 | it { should_not report_lint } 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/final_newline_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::FinalNewline do 4 | let(:linter_config) { { 'present' => present } } 5 | 6 | context 'when trailing newline is preferred' do 7 | let(:present) { true } 8 | 9 | context 'when the file is empty' do 10 | let(:scss) { '' } 11 | 12 | it { should_not report_lint } 13 | end 14 | 15 | context 'when the file ends with a newline' do 16 | let(:scss) { "p {}\n" } 17 | 18 | it { should_not report_lint } 19 | end 20 | 21 | context 'when the file does not end with a newline' do 22 | let(:scss) { 'p {}' } 23 | 24 | it { should report_lint } 25 | end 26 | end 27 | 28 | context 'when no trailing newline is preferred' do 29 | let(:present) { false } 30 | 31 | context 'when the file is empty' do 32 | let(:scss) { '' } 33 | 34 | it { should_not report_lint } 35 | end 36 | 37 | context 'when the file ends with a newline' do 38 | let(:scss) { "p {}\n" } 39 | 40 | it { should report_lint } 41 | end 42 | 43 | context 'when the file does not end with a newline' do 44 | let(:scss) { 'p {}' } 45 | 46 | it { should_not report_lint } 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/hex_length_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::HexLength do 4 | let(:linter_config) { { 'style' => style } } 5 | let(:style) { 'short' } 6 | 7 | context 'when rule is empty' do 8 | let(:scss) { <<-SCSS } 9 | p { 10 | } 11 | SCSS 12 | 13 | it { should_not report_lint } 14 | end 15 | 16 | context 'when rule contains properties with valid hex code' do 17 | let(:scss) { <<-SCSS } 18 | p { 19 | color: #1234ab; 20 | } 21 | SCSS 22 | 23 | it { should_not report_lint } 24 | end 25 | 26 | context 'when ID selector starts with a hex code' do 27 | let(:scss) { <<-SCSS } 28 | #aabbcc { 29 | } 30 | SCSS 31 | 32 | it { should_not report_lint } 33 | end 34 | 35 | context 'when color is specified as a color keyword' do 36 | let(:scss) { <<-SCSS } 37 | p { 38 | @include box-shadow(0 0 1px 1px gold); 39 | } 40 | SCSS 41 | 42 | it { should_not report_lint } 43 | end 44 | 45 | context 'when short style is preferred' do 46 | let(:style) { 'short' } 47 | 48 | context 'with short hex code' do 49 | let(:scss) { <<-SCSS } 50 | p { 51 | background: #ccc; 52 | background: #CCC; 53 | @include crazy-color(#fff); 54 | } 55 | SCSS 56 | 57 | it { should_not report_lint } 58 | end 59 | 60 | context 'with long hex code that could be condensed to 3 digits' do 61 | let(:scss) { <<-SCSS } 62 | p { 63 | background: #cccccc; 64 | background: #CCCCCC; 65 | @include crazy-color(#ffffff); 66 | } 67 | SCSS 68 | 69 | it { should report_lint line: 2 } 70 | it { should report_lint line: 3 } 71 | it { should report_lint line: 4 } 72 | end 73 | end 74 | 75 | context 'when long style is preferred' do 76 | let(:style) { 'long' } 77 | 78 | context 'with long hex code that could be condensed to 3 digits' do 79 | let(:scss) { <<-SCSS } 80 | p { 81 | background: #cccccc; 82 | background: #CCCCCC; 83 | @include crazy-color(#ffffff); 84 | } 85 | SCSS 86 | 87 | it { should_not report_lint } 88 | end 89 | 90 | context 'with short hex code' do 91 | let(:scss) { <<-SCSS } 92 | p { 93 | background: #ccc; 94 | background: #CCC; 95 | @include crazy-color(#fff); 96 | } 97 | SCSS 98 | 99 | it { should report_lint line: 2 } 100 | it { should report_lint line: 3 } 101 | it { should report_lint line: 4 } 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/hex_notation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::HexNotation do 4 | let(:linter_config) { { 'style' => style } } 5 | let(:style) { nil } 6 | 7 | context 'when rule is empty' do 8 | let(:scss) { <<-SCSS } 9 | p { 10 | } 11 | SCSS 12 | 13 | it { should_not report_lint } 14 | end 15 | 16 | context 'when rule contains color keyword' do 17 | let(:scss) { <<-SCSS } 18 | p { 19 | border-color: red; 20 | } 21 | SCSS 22 | 23 | it { should_not report_lint } 24 | end 25 | 26 | context 'lowercase style' do 27 | let(:style) { 'lowercase' } 28 | 29 | context 'when rule contains properties with lowercase hex code' do 30 | let(:scss) { <<-SCSS } 31 | p { 32 | background: #ccc; 33 | color: #cccccc; 34 | @include crazy-color(#fff); 35 | } 36 | SCSS 37 | 38 | it { should_not report_lint } 39 | end 40 | 41 | context 'with uppercase hex codes' do 42 | let(:scss) { <<-SCSS } 43 | p { 44 | background: #CCC; 45 | color: #CCCCCC; 46 | @include crazy-color(#FFF); 47 | } 48 | SCSS 49 | 50 | it { should report_lint line: 2 } 51 | it { should report_lint line: 3 } 52 | it { should report_lint line: 4 } 53 | end 54 | end 55 | 56 | context 'uppercase style' do 57 | let(:style) { 'uppercase' } 58 | 59 | context 'with uppercase hex codes' do 60 | let(:scss) { <<-SCSS } 61 | p { 62 | background: #CCC; 63 | color: #CCCCCC; 64 | @include crazy-color(#FFF); 65 | } 66 | SCSS 67 | 68 | it { should_not report_lint } 69 | end 70 | 71 | context 'when rule contains properties with lowercase hex code' do 72 | let(:scss) { <<-SCSS } 73 | p { 74 | background: #ccc; 75 | color: #cccccc; 76 | @include crazy-color(#fff); 77 | } 78 | SCSS 79 | 80 | it { should report_lint line: 2 } 81 | it { should report_lint line: 3 } 82 | it { should report_lint line: 4 } 83 | end 84 | end 85 | 86 | context 'when ID selector starts with a hex code' do 87 | let(:scss) { <<-SCSS } 88 | #aabbcc { 89 | } 90 | SCSS 91 | 92 | it { should_not report_lint } 93 | end 94 | 95 | context 'when color is specified as a color keyword' do 96 | let(:scss) { <<-SCSS } 97 | p { 98 | @include box-shadow(0 0 1px 1px gold); 99 | } 100 | SCSS 101 | 102 | it { should_not report_lint } 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/hex_validation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::HexValidation do 4 | context 'when rule is empty' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | } 8 | SCSS 9 | 10 | it { should_not report_lint } 11 | end 12 | 13 | context 'when rule contains valid hex codes or color keyword' do 14 | gradient_css = 'progid:DXImageTransform.Microsoft.gradient' \ 15 | '(startColorstr=#99000000, endColorstr=#99000000)' 16 | 17 | let(:scss) { <<-SCSS } 18 | p { 19 | background: #000; 20 | color: #FFFFFF; 21 | border-color: red; 22 | filter: #{gradient_css}; 23 | } 24 | SCSS 25 | 26 | it { should_not report_lint } 27 | end 28 | 29 | context 'when rule contains invalid hex codes' do 30 | let(:scss) { <<-SCSS } 31 | p { 32 | background: #dd; 33 | color: #dddd; 34 | } 35 | SCSS 36 | 37 | it { should report_lint line: 2 } 38 | it { should report_lint line: 3 } 39 | end 40 | 41 | context 'when rule contains hex codes in a longer string' do 42 | let(:scss) { <<-SCSS } 43 | p { 44 | content: 'foo#bad'; 45 | content: 'foo #ba'; 46 | } 47 | SCSS 48 | 49 | it { should report_lint line: 3 } 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/id_selector_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::IdSelector do 4 | context 'when rule is a type' do 5 | let(:scss) { 'p {}' } 6 | 7 | it { should_not report_lint } 8 | end 9 | 10 | context 'when rule is an ID' do 11 | let(:scss) { '#identifier {}' } 12 | 13 | it { should report_lint line: 1 } 14 | end 15 | 16 | context 'when rule is a class' do 17 | let(:scss) { '.class {}' } 18 | 19 | it { should_not report_lint } 20 | end 21 | 22 | context 'when rule is a type with a class' do 23 | let(:scss) { 'a.class {}' } 24 | 25 | it { should_not report_lint } 26 | end 27 | 28 | context 'when rule is a type with an ID' do 29 | let(:scss) { 'a#identifier {}' } 30 | 31 | it { should report_lint line: 1 } 32 | end 33 | 34 | context 'when rule is an ID with a pseudo-selector' do 35 | let(:scss) { '#identifier:active {}' } 36 | 37 | it { should report_lint line: 1 } 38 | end 39 | 40 | context 'when rule contains a nested rule with type and ID' do 41 | let(:scss) { <<-SCSS } 42 | p { 43 | a#identifier {} 44 | } 45 | SCSS 46 | 47 | it { should report_lint line: 2 } 48 | end 49 | 50 | context 'when rule contains multiple selectors' do 51 | context 'when all of the selectors are just IDs, classes, or types' do 52 | let(:scss) { <<-SCSS } 53 | #identifier, 54 | .class, 55 | a { 56 | } 57 | SCSS 58 | 59 | it { should report_lint line: 1 } 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/important_rule_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::ImportantRule do 4 | context 'when !important is not used' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | color: #000; 8 | } 9 | SCSS 10 | 11 | it { should_not report_lint } 12 | end 13 | 14 | context 'when !important is used' do 15 | let(:scss) { <<-SCSS } 16 | p { 17 | color: #000 !important; 18 | } 19 | SCSS 20 | 21 | it { should report_lint line: 2 } 22 | end 23 | 24 | context 'when !important is used in property containing Sass script' do 25 | let(:scss) { <<-SCSS } 26 | p { 27 | color: \#{$my-var} !important; 28 | } 29 | SCSS 30 | 31 | it { should report_lint line: 2 } 32 | end 33 | 34 | context 'when property contains a list literal with an empty list' do 35 | let(:scss) { <<-SCSS } 36 | p { 37 | content: 0 (); 38 | } 39 | SCSS 40 | 41 | it { should_not report_lint } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/placeholder_in_extend_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::PlaceholderInExtend do 4 | context 'when extending with a class' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | @extend .error; 8 | } 9 | SCSS 10 | 11 | it { should report_lint line: 2 } 12 | end 13 | 14 | context 'when extending with a type' do 15 | let(:scss) { <<-SCSS } 16 | p { 17 | @extend span; 18 | } 19 | SCSS 20 | 21 | it { should report_lint line: 2 } 22 | end 23 | 24 | context 'when extending with an id' do 25 | let(:scss) { <<-SCSS } 26 | p { 27 | @extend #some-identifer; 28 | } 29 | SCSS 30 | 31 | it { should report_lint line: 2 } 32 | end 33 | 34 | context 'when extending with a comma sequence starting with a placeholder' do 35 | let(:scss) { <<-SCSS } 36 | p { 37 | @extend %placeholder, .item; 38 | } 39 | SCSS 40 | 41 | it { should report_lint line: 2 } 42 | end 43 | 44 | context 'when extending with a placeholder' do 45 | let(:scss) { <<-SCSS } 46 | p { 47 | @extend %placeholder; 48 | } 49 | SCSS 50 | 51 | it { should_not report_lint } 52 | end 53 | 54 | context 'when extending with a selector whose prefix is not a placeholder' do 55 | let(:scss) { <<-SCSS } 56 | p { 57 | @extend .blah-\#{$dynamically_generated_name}; 58 | } 59 | SCSS 60 | 61 | it { should report_lint line: 2 } 62 | end 63 | 64 | context 'when extending with a selector whose prefix is dynamic' do 65 | let(:scss) { <<-SCSS } 66 | p { 67 | @extend \#{$dynamically_generated_placeholder_name}; 68 | } 69 | SCSS 70 | 71 | it { should_not report_lint } 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/property_count_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::PropertyCount do 4 | let(:linter_config) { { 'max_properties' => 3 } } 5 | 6 | context 'when the number of properties in each individual rule set is under the limit' do 7 | let(:scss) { <<-SCSS } 8 | p { 9 | margin: 0; 10 | padding: 0; 11 | float: left; 12 | 13 | a { 14 | color: #f00; 15 | text-decoration: none; 16 | text-transform: uppercase; 17 | } 18 | } 19 | 20 | i { 21 | color: #000; 22 | text-decoration: underline; 23 | text-transform: lowercase; 24 | } 25 | SCSS 26 | 27 | it { should_not report_lint } 28 | end 29 | 30 | context 'when the number of properties in an individual rule set is over the limit' do 31 | let(:scss) { <<-SCSS } 32 | p { 33 | margin: 0; 34 | padding: 0; 35 | float: left; 36 | 37 | a { 38 | color: #f00; 39 | font: 15px arial, sans-serif; 40 | text-decoration: none; 41 | text-transform: uppercase; 42 | } 43 | } 44 | 45 | i { 46 | color: #000; 47 | text-decoration: underline; 48 | text-transform: lowercase; 49 | } 50 | SCSS 51 | 52 | it { should_not report_lint line: 1 } 53 | it { should report_lint line: 6 } 54 | end 55 | 56 | context 'when nested rule sets are included in the count' do 57 | let(:linter_config) { super().merge('include_nested' => true) } 58 | 59 | context 'when the number of total nested properties under the limit' do 60 | let(:scss) { <<-SCSS } 61 | p { 62 | margin: 0; 63 | 64 | a { 65 | color: #f00; 66 | text-transform: uppercase; 67 | } 68 | } 69 | 70 | i { 71 | color: #000; 72 | text-decoration: underline; 73 | text-transform: lowercase; 74 | } 75 | SCSS 76 | 77 | it { should_not report_lint } 78 | end 79 | 80 | context 'when the number of total nested properties is over the limit' do 81 | let(:scss) { <<-SCSS } 82 | p { 83 | margin: 0; 84 | padding: 0; 85 | 86 | a { 87 | color: #f00; 88 | text-decoration: none; 89 | text-transform: uppercase; 90 | } 91 | } 92 | 93 | i { 94 | color: #000; 95 | text-decoration: underline; 96 | text-transform: lowercase; 97 | } 98 | SCSS 99 | 100 | it { should report_lint line: 1 } 101 | it { should_not report_lint line: 12 } 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/property_spelling_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::PropertySpelling do 4 | context 'with a regular property' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | margin: 5px; 8 | } 9 | SCSS 10 | 11 | it { should_not report_lint } 12 | end 13 | 14 | context 'with a property containing interpolation' do 15 | let(:scss) { <<-SCSS } 16 | p { 17 | \#{$property-name}: 5px; 18 | } 19 | SCSS 20 | 21 | it { should_not report_lint } 22 | end 23 | 24 | context 'with a non-existent property' do 25 | let(:scss) { <<-SCSS } 26 | p { 27 | peanut-butter: jelly-time; 28 | } 29 | SCSS 30 | 31 | it { should report_lint } 32 | end 33 | 34 | context 'when extra properties are specified' do 35 | let(:linter_config) { { 'extra_properties' => ['made-up-property'] } } 36 | 37 | context 'with a non-existent property' do 38 | let(:scss) { <<-SCSS } 39 | p { 40 | peanut-butter: jelly-time; 41 | } 42 | SCSS 43 | 44 | it { should report_lint } 45 | end 46 | 47 | context 'with a property listed as an extra property' do 48 | let(:scss) { <<-SCSS } 49 | p { 50 | made-up-property: value; 51 | } 52 | SCSS 53 | 54 | it { should_not report_lint } 55 | end 56 | end 57 | 58 | context 'when disabled properties are specified' do 59 | let(:linter_config) do 60 | { 61 | 'disabled_properties' => ['margin'], 62 | } 63 | end 64 | 65 | context 'with a non-existent property' do 66 | let(:scss) { <<-SCSS } 67 | p { 68 | peanut-butter: jelly-time; 69 | } 70 | SCSS 71 | 72 | it { should report_lint } 73 | end 74 | 75 | context 'with a property listed as an disabled property' do 76 | let(:scss) { <<-SCSS } 77 | p { 78 | margin: 0; 79 | } 80 | SCSS 81 | 82 | it { should report_lint } 83 | end 84 | end 85 | 86 | context 'with valid nested properties' do 87 | let(:scss) { <<-SCSS } 88 | p { 89 | text: { 90 | align: center; 91 | transform: uppercase; 92 | } 93 | } 94 | SCSS 95 | 96 | it { should_not report_lint } 97 | end 98 | 99 | context 'with invalid nested properties' do 100 | let(:scss) { <<-SCSS } 101 | p { 102 | text: { 103 | aligned: center; 104 | transformed: uppercase; 105 | } 106 | } 107 | SCSS 108 | 109 | it { should report_lint line: 3 } 110 | it { should report_lint line: 4 } 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/pseudo_element_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::PseudoElement do 4 | context 'when a pseudo-element has two colons' do 5 | let(:scss) { <<-SCSS } 6 | ::before {} 7 | p::before {} 8 | p#nav::before {} 9 | p div::before {} 10 | p::before div {} 11 | p, div::before {} 12 | p::before, div {} 13 | SCSS 14 | 15 | it { should_not report_lint } 16 | end 17 | 18 | context 'when a pseudo-element has one colon' do 19 | let(:scss) { <<-SCSS } 20 | :before {} 21 | p:before {} 22 | p#nav:before {} 23 | p div:before {} 24 | p:before div {} 25 | p, div:before {} 26 | p:before, div {} 27 | SCSS 28 | 29 | it { should report_lint line: 1 } 30 | it { should report_lint line: 2 } 31 | it { should report_lint line: 3 } 32 | it { should report_lint line: 4 } 33 | it { should report_lint line: 5 } 34 | it { should report_lint line: 6 } 35 | it { should report_lint line: 7 } 36 | end 37 | 38 | context 'when a pseudo-selector has one colon' do 39 | let(:scss) { <<-SCSS } 40 | :hover {} 41 | p:hover {} 42 | p#nav:hover {} 43 | p div:hover {} 44 | p:hover div {} 45 | p, div:hover {} 46 | p:hover, div {} 47 | SCSS 48 | 49 | it { should_not report_lint } 50 | end 51 | 52 | context 'when a pseudo-selector has two colons' do 53 | let(:scss) { <<-SCSS } 54 | ::hover {} 55 | p::hover {} 56 | p#nav::hover {} 57 | p div::hover {} 58 | p::hover div {} 59 | p, div::hover {} 60 | p::hover, div {} 61 | SCSS 62 | 63 | it { should report_lint line: 1 } 64 | it { should report_lint line: 2 } 65 | it { should report_lint line: 3 } 66 | it { should report_lint line: 4 } 67 | it { should report_lint line: 5 } 68 | it { should report_lint line: 6 } 69 | it { should report_lint line: 7 } 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/qualifying_element_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::QualifyingElement do 4 | context 'when selector does not include an element' do 5 | let(:scss) { <<-SCSS } 6 | .foo {} 7 | #bar {} 8 | [foobar] {} 9 | .foo .bar {} 10 | #bar > .foo {} 11 | [foobar] #bar .foo {} 12 | SCSS 13 | 14 | it { should_not report_lint } 15 | end 16 | 17 | context 'when selector includes an element' do 18 | context 'and element does not qualify' do 19 | let(:scss) { <<-SCSS } 20 | ul {} 21 | SCSS 22 | 23 | it { should_not report_lint } 24 | end 25 | 26 | context 'and element qualifies class' do 27 | let(:scss) { <<-SCSS } 28 | ul.list {} 29 | SCSS 30 | 31 | it { should report_lint line: 1 } 32 | end 33 | 34 | context 'and element qualifies attribute' do 35 | let(:scss) { <<-SCSS } 36 | a[href] {} 37 | SCSS 38 | 39 | it { should report_lint line: 1 } 40 | end 41 | 42 | context 'and element qualifies id' do 43 | let(:scss) { <<-SCSS } 44 | ul#list {} 45 | SCSS 46 | 47 | it { should report_lint line: 1 } 48 | end 49 | 50 | context 'and selector is in a group' do 51 | context 'and element does not qualify' do 52 | let(:scss) { <<-SCSS } 53 | .list li, 54 | .item > span {} 55 | SCSS 56 | 57 | it { should_not report_lint } 58 | end 59 | 60 | context 'and element qualifies class' do 61 | let(:scss) { <<-SCSS } 62 | .item span, 63 | ul > li.item {} 64 | SCSS 65 | 66 | it { should report_lint line: 1 } 67 | end 68 | 69 | context 'and element qualifies attribute' do 70 | let(:scss) { <<-SCSS } 71 | .item + span, 72 | li a[href] {} 73 | SCSS 74 | 75 | it { should report_lint line: 1 } 76 | end 77 | 78 | context 'and element qualifies id' do 79 | let(:scss) { <<-SCSS } 80 | #foo, 81 | li#item + li {} 82 | SCSS 83 | 84 | it { should report_lint line: 1 } 85 | end 86 | end 87 | 88 | context 'and selector involves a combinator' do 89 | context 'and element does not qualify' do 90 | let(:scss) { <<-SCSS } 91 | .list li {} 92 | .list > li {} 93 | .item + li {} 94 | .item ~ li {} 95 | SCSS 96 | 97 | it { should_not report_lint } 98 | end 99 | 100 | context 'and element qualifies class' do 101 | let(:scss) { <<-SCSS } 102 | ul > li.item {} 103 | SCSS 104 | 105 | it { should report_lint line: 1 } 106 | end 107 | 108 | context 'and element qualifies attribute' do 109 | let(:scss) { <<-SCSS } 110 | li a[href] {} 111 | SCSS 112 | 113 | it { should report_lint line: 1 } 114 | end 115 | 116 | context 'and element qualifies id' do 117 | let(:scss) { <<-SCSS } 118 | li#item + li {} 119 | SCSS 120 | 121 | it { should report_lint line: 1 } 122 | end 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/single_line_per_property_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::SingleLinePerProperty do 4 | context 'when properties are each on their own line' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | color: #fff; 8 | margin: 0; 9 | padding: 5px; 10 | } 11 | SCSS 12 | 13 | it { should_not report_lint } 14 | end 15 | 16 | context 'when two properties share a line' do 17 | let(:scss) { <<-SCSS } 18 | p { 19 | color: #fff; 20 | margin: 0; padding: 5px; 21 | } 22 | SCSS 23 | 24 | it { should_not report_lint line: 2 } 25 | it { should report_lint line: 3, count: 1 } 26 | end 27 | 28 | context 'when multiple properties share a line' do 29 | let(:scss) { <<-SCSS } 30 | p { 31 | color: #fff; margin: 0; padding: 5px; 32 | } 33 | SCSS 34 | 35 | it { should report_lint line: 2, count: 2 } 36 | end 37 | 38 | context 'when multiple properties share a line on a single line rule set' do 39 | let(:scss) { <<-SCSS } 40 | p { color: #fff; margin: 0; padding: 5px; } 41 | SCSS 42 | 43 | context 'and single line rule sets are allowed' do 44 | let(:linter_config) { { 'allow_single_line_rule_sets' => true } } 45 | 46 | it { should_not report_lint } 47 | end 48 | 49 | context 'and single line rule sets are not allowed' do 50 | let(:linter_config) { { 'allow_single_line_rule_sets' => false } } 51 | 52 | it { should report_lint } 53 | end 54 | end 55 | 56 | context 'when a single line rule set contains a single property' do 57 | let(:scss) { <<-SCSS } 58 | p { color: #fff; } 59 | SCSS 60 | 61 | context 'and single line rule sets are allowed' do 62 | let(:linter_config) { { 'allow_single_line_rule_sets' => true } } 63 | 64 | it { should_not report_lint } 65 | end 66 | 67 | context 'and single line rule sets are not allowed' do 68 | let(:linter_config) { { 'allow_single_line_rule_sets' => false } } 69 | 70 | it { should report_lint } 71 | end 72 | end 73 | 74 | context 'when a nested single line rule set contains a single property' do 75 | let(:scss) { <<-SCSS } 76 | .my-selector { 77 | p { color: #fff; } 78 | } 79 | SCSS 80 | 81 | context 'and single line rule sets are allowed' do 82 | let(:linter_config) { { 'allow_single_line_rule_sets' => true } } 83 | 84 | it { should_not report_lint } 85 | end 86 | 87 | context 'and single line rule sets are not allowed' do 88 | let(:linter_config) { { 'allow_single_line_rule_sets' => false } } 89 | 90 | it { should report_lint } 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/space_after_property_name_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::SpaceAfterPropertyName do 4 | context 'when a property name is followed by a space' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | margin : 0; 8 | } 9 | SCSS 10 | 11 | it { should report_lint line: 2 } 12 | end 13 | 14 | context 'when a property name is not followed by a space' do 15 | let(:scss) { <<-SCSS } 16 | p { 17 | margin: 0; 18 | } 19 | SCSS 20 | 21 | it { should_not report_lint } 22 | end 23 | 24 | context 'when interpolation within single quotes is followed by inline property' do 25 | context 'and property name is followed by a space' do 26 | let(:scss) { "[class~='\#{$test}'] { width: 100%; }" } 27 | 28 | it { should_not report_lint } 29 | end 30 | 31 | context 'and property name is not followed by a space' do 32 | let(:scss) { "[class~='\#{$test}'] { width : 100%; }" } 33 | 34 | it { should report_lint } 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/space_after_variable_name_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::SpaceAfterVariableName do 4 | let(:scss) { <<-SCSS } 5 | $none: #fff; 6 | $one : #fff; 7 | $two : #fff; 8 | SCSS 9 | 10 | it { should_not report_lint line: 1 } 11 | it { should report_lint line: 2 } 12 | it { should report_lint line: 3 } 13 | 14 | context 'when a map contains aligned colons' do 15 | let(:scss) { <<-SCSS } 16 | $map: ( 17 | 'one' : 350px, 18 | 'two' : 450px, 19 | 'three' : 560px, 20 | ); 21 | SCSS 22 | 23 | it { should_not report_lint } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/trailing_whitespace_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::TrailingWhitespace do 4 | context 'when lines contain trailing spaces' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | margin: 0;\s\s 8 | } 9 | SCSS 10 | 11 | it { should report_lint line: 2 } 12 | end 13 | 14 | context 'when lines contain trailing tabs' do 15 | let(:scss) { <<-SCSS } 16 | p { 17 | margin: 0;\t\t 18 | } 19 | SCSS 20 | 21 | it { should report_lint line: 2 } 22 | end 23 | 24 | context 'when lines does not contain trailing whitespace' do 25 | let(:scss) { <<-SCSS } 26 | p { 27 | margin: 0; 28 | } 29 | SCSS 30 | 31 | it { should_not report_lint } 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/transition_all_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::TransitionAll do 4 | context 'when transition-property is not set' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | } 8 | SCSS 9 | 10 | it { should_not report_lint } 11 | end 12 | 13 | context 'when transition-property is set to none' do 14 | let(:scss) { <<-SCSS } 15 | p { 16 | transition-property: none; 17 | } 18 | SCSS 19 | 20 | it { should_not report_lint } 21 | end 22 | 23 | context 'when transition-property is not set to all' do 24 | let(:scss) { <<-SCSS } 25 | p { 26 | transition-property: color; 27 | } 28 | SCSS 29 | 30 | it { should_not report_lint } 31 | end 32 | 33 | context 'when transition-property is set to all' do 34 | let(:scss) { <<-SCSS } 35 | p { 36 | transition-property: all; 37 | } 38 | SCSS 39 | 40 | it { should report_lint line: 2 } 41 | end 42 | 43 | context 'when transition shorthand for transition-property is not set' do 44 | let(:scss) { <<-SCSS } 45 | p { 46 | } 47 | SCSS 48 | 49 | it { should_not report_lint } 50 | end 51 | 52 | context 'when transition shorthand for transition-property is set to none' do 53 | let(:scss) { <<-SCSS } 54 | p { 55 | transition: none; 56 | } 57 | SCSS 58 | 59 | it { should_not report_lint } 60 | end 61 | 62 | context 'when transition shorthand for transition-property is not set to all' do 63 | let(:scss) { <<-SCSS } 64 | p { 65 | transition: color 1s linear; 66 | } 67 | SCSS 68 | 69 | it { should_not report_lint } 70 | end 71 | 72 | context 'when transition shorthand for transition-property is set to all' do 73 | let(:scss) { <<-SCSS } 74 | p { 75 | transition: all 1s linear; 76 | } 77 | SCSS 78 | 79 | it { should report_lint line: 2 } 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/unnecessary_mantissa_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::UnnecessaryMantissa do 4 | context 'when value is zero' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | margin: 0; 8 | padding: func(0); 9 | top: 0em; 10 | } 11 | SCSS 12 | 13 | it { should_not report_lint } 14 | end 15 | 16 | context 'when value contains no mantissa' do 17 | let(:scss) { <<-SCSS } 18 | p { 19 | margin: 1; 20 | padding: func(1); 21 | top: 1em; 22 | } 23 | SCSS 24 | 25 | it { should_not report_lint } 26 | end 27 | 28 | context 'when value contains a mantissa with a zero' do 29 | let(:scss) { <<-SCSS } 30 | p { 31 | margin: 1.0; 32 | padding: func(1.0); 33 | top: 1.0em; 34 | } 35 | SCSS 36 | 37 | it { should report_lint line: 2 } 38 | it { should report_lint line: 3 } 39 | it { should report_lint line: 4 } 40 | end 41 | 42 | context 'when value contains a mantissa with multiple zeroes' do 43 | let(:scss) { <<-SCSS } 44 | p { 45 | margin: 1.000; 46 | padding: func(1.000); 47 | top: 1.000em; 48 | } 49 | SCSS 50 | 51 | it { should report_lint line: 2 } 52 | it { should report_lint line: 3 } 53 | it { should report_lint line: 4 } 54 | end 55 | 56 | context 'when value contains a mantissa with multiple zeroes followed by a number' do 57 | let(:scss) { <<-SCSS } 58 | p { 59 | margin: 1.0001; 60 | padding: func(1.0001); 61 | top: 1.0001em; 62 | } 63 | SCSS 64 | 65 | it { should_not report_lint } 66 | end 67 | 68 | context 'when a decimal value appears in a single-quoted string' do 69 | let(:scss) { <<-SCSS } 70 | p { 71 | content: '1.0'; 72 | } 73 | SCSS 74 | 75 | it { should_not report_lint } 76 | end 77 | 78 | context 'when a decimal value appears in a double-quoted string' do 79 | let(:scss) { <<-SCSS } 80 | p { 81 | content: "1.0"; 82 | } 83 | SCSS 84 | 85 | it { should_not report_lint } 86 | end 87 | 88 | context 'when a decimal value appears in a URL' do 89 | let(:scss) { <<-SCSS } 90 | p { 91 | background: url(https://www.example.com/v1.0/image.jpg); 92 | } 93 | SCSS 94 | 95 | it { should_not report_lint } 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/unnecessary_parent_reference_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::UnnecessaryParentReference do 4 | context 'when an amperand precedes a direct descendant operator' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | & > a {} 8 | } 9 | SCSS 10 | 11 | it { should report_lint line: 2 } 12 | end 13 | 14 | context 'when an amperand precedes a general child' do 15 | let(:scss) { <<-SCSS } 16 | p { 17 | & a {} 18 | } 19 | SCSS 20 | 21 | it { should report_lint line: 2 } 22 | end 23 | 24 | context 'when an amperand is chained with class' do 25 | let(:scss) { <<-SCSS } 26 | p { 27 | &.foo {} 28 | } 29 | SCSS 30 | 31 | it { should_not report_lint } 32 | end 33 | 34 | context 'when an amperand follows a direct descendant operator' do 35 | let(:scss) { <<-SCSS } 36 | p { 37 | .foo > & {} 38 | } 39 | SCSS 40 | 41 | it { should_not report_lint } 42 | end 43 | 44 | context 'when an ampersand precedes a sibling operator' do 45 | let(:scss) { <<-SCSS } 46 | p { 47 | & + & {} 48 | & ~ & {} 49 | } 50 | SCSS 51 | 52 | it { should_not report_lint } 53 | end 54 | 55 | context 'when multiple ampersands exist with one concatenated' do 56 | let(:scss) { <<-SCSS } 57 | p { 58 | & + &:hover {} 59 | } 60 | SCSS 61 | 62 | it { should_not report_lint } 63 | end 64 | 65 | context 'when an amperand is used in a comma sequence to DRY up code' do 66 | let(:scss) { <<-SCSS } 67 | p { 68 | &, 69 | .foo, 70 | .bar { 71 | margin: 0; 72 | } 73 | } 74 | SCSS 75 | 76 | it { should_not report_lint } 77 | end 78 | 79 | context 'when an ampersand is used by itself' do 80 | let(:scss) { <<-SCSS } 81 | p { 82 | & {} 83 | } 84 | SCSS 85 | 86 | it { should report_lint line: 2 } 87 | end 88 | 89 | context 'when an ampersand is used in concatentation' do 90 | let(:scss) { <<-SCSS } 91 | .icon { 92 | &-small {} 93 | } 94 | SCSS 95 | 96 | it { should_not report_lint } 97 | end 98 | 99 | context 'when an ampersand is used in concatentation following an ampersand' do 100 | let(:scss) { <<-SCSS } 101 | .icon { 102 | & &-small {} 103 | } 104 | SCSS 105 | 106 | it { should_not report_lint } 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/url_format_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::UrlFormat do 4 | shared_examples_for 'UrlFormat linter' do 5 | context 'when URL contains protocol' do 6 | let(:url) { 'https://something.com/image.png' } 7 | 8 | it { should report_lint } 9 | end 10 | 11 | context 'when URL contains domain with protocol-less double slashes' do 12 | let(:url) { '//something.com/image.png' } 13 | 14 | it { should report_lint } 15 | end 16 | 17 | context 'when URL contains absolute path' do 18 | let(:url) { '/absolute/path/to/image.png' } 19 | 20 | it { should_not report_lint } 21 | end 22 | 23 | context 'when URL contains relative path' do 24 | let(:url) { 'relative/path/to/image.png' } 25 | 26 | it { should_not report_lint } 27 | end 28 | 29 | context 'when URL is a data URI' do 30 | let(:url) { 'data:image/png;base64,iVBORI=' } 31 | 32 | it { should_not report_lint } 33 | end 34 | 35 | context 'when URL contains a variable' do 36 | let(:scss) { <<-SCSS } 37 | .block { 38 | background: url('${url}'); 39 | } 40 | SCSS 41 | 42 | it { should_not report_lint } 43 | end 44 | end 45 | 46 | context 'when URL is enclosed in quotes' do 47 | let(:scss) { <<-SCSS } 48 | .block { 49 | background: url('#{url}'); 50 | } 51 | SCSS 52 | 53 | it_should_behave_like 'UrlFormat linter' 54 | end 55 | 56 | context 'when URL is not enclosed in quotes' do 57 | let(:scss) { <<-SCSS } 58 | .block { 59 | background: url(#{url}); 60 | } 61 | SCSS 62 | 63 | it_should_behave_like 'UrlFormat linter' 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/url_quotes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::UrlQuotes do 4 | context 'when property has a literal URL' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | background: url(example.png); 8 | } 9 | SCSS 10 | 11 | it { should report_lint line: 2 } 12 | end 13 | 14 | context 'when property has a URL enclosed in single quotes' do 15 | let(:scss) { <<-SCSS } 16 | p { 17 | background: url('example.png'); 18 | } 19 | SCSS 20 | 21 | it { should_not report_lint } 22 | end 23 | 24 | context 'when property has a URL enclosed in double quotes' do 25 | let(:scss) { <<-SCSS } 26 | p { 27 | background: url("example.png"); 28 | } 29 | SCSS 30 | 31 | it { should_not report_lint } 32 | end 33 | 34 | context 'when property has a literal URL in a list' do 35 | let(:scss) { <<-SCSS } 36 | p { 37 | background: transparent url(example.png); 38 | } 39 | SCSS 40 | 41 | it { should report_lint line: 2 } 42 | end 43 | 44 | context 'when property has a single-quoted URL in a list' do 45 | let(:scss) { <<-SCSS } 46 | p { 47 | background: transparent url('example.png'); 48 | } 49 | SCSS 50 | 51 | it { should_not report_lint } 52 | end 53 | 54 | context 'when property has a double-quoted URL in a list' do 55 | let(:scss) { <<-SCSS } 56 | p { 57 | background: transparent url("example.png"); 58 | } 59 | SCSS 60 | 61 | it { should_not report_lint } 62 | end 63 | 64 | context 'when property has a data URI' do 65 | let(:scss) { <<-SCSS } 66 | .tracking-pixel { 67 | background: url(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==); 68 | } 69 | SCSS 70 | 71 | it { should_not report_lint } 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/scss_lint/linter/zero_unit_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Linter::ZeroUnit do 4 | context 'when no properties exist' do 5 | let(:scss) { <<-SCSS } 6 | p { 7 | } 8 | SCSS 9 | 10 | it { should_not report_lint } 11 | end 12 | 13 | context 'when properties with unit-less zeros exist' do 14 | let(:scss) { <<-SCSS } 15 | p { 16 | margin: 0; 17 | } 18 | SCSS 19 | 20 | it { should_not report_lint } 21 | end 22 | 23 | context 'when properties with non-zero values exist' do 24 | let(:scss) { <<-SCSS } 25 | p { 26 | margin: 5px; 27 | line-height: 1.5em; 28 | } 29 | SCSS 30 | 31 | it { should_not report_lint } 32 | end 33 | 34 | context 'when properties with zero values contain units' do 35 | let(:scss) { <<-SCSS } 36 | p { 37 | margin: 0px; 38 | } 39 | SCSS 40 | 41 | it { should report_lint line: 2 } 42 | end 43 | 44 | context 'when properties with multiple zero values containing units exist' do 45 | let(:scss) { <<-SCSS } 46 | p { 47 | margin: 5em 0em 2em 0px; 48 | } 49 | SCSS 50 | 51 | it { should report_lint line: 2, count: 2 } 52 | end 53 | 54 | context 'when function call contains a zero value with units' do 55 | let(:scss) { <<-SCSS } 56 | p { 57 | margin: some-function(0em); 58 | } 59 | SCSS 60 | 61 | it { should report_lint line: 2 } 62 | end 63 | 64 | context 'when mixin include contains a zero value with units' do 65 | let(:scss) { <<-SCSS } 66 | p { 67 | @include some-mixin(0em); 68 | } 69 | SCSS 70 | 71 | it { should report_lint line: 2 } 72 | end 73 | 74 | context 'when string contains a zero value with units' do 75 | let(:scss) { <<-SCSS } 76 | p { 77 | content: func("0em"); 78 | } 79 | SCSS 80 | 81 | it { should_not report_lint } 82 | end 83 | 84 | context 'when property value has a ".0" fractional component' do 85 | let(:scss) { <<-SCSS } 86 | p { 87 | margin: 4.0em; 88 | } 89 | SCSS 90 | 91 | it { should_not report_lint } 92 | end 93 | 94 | context 'when property value has a color hex with a leading 0' do 95 | let(:scss) { <<-SCSS } 96 | p { 97 | color: #0af; 98 | } 99 | SCSS 100 | 101 | it { should_not report_lint } 102 | end 103 | 104 | context 'when property with zero value is a dimension' do 105 | let(:scss) { <<-SCSS } 106 | p { 107 | transition-delay: 0s; 108 | } 109 | SCSS 110 | 111 | it { should_not report_lint } 112 | end 113 | 114 | context 'when calc expression with zero value has units' do 115 | let(:scss) { <<-SCSS } 116 | p { 117 | width: calc(0px + 1.5em); 118 | } 119 | SCSS 120 | 121 | it { should_not report_lint } 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /spec/scss_lint/linter_registry_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::LinterRegistry do 4 | context 'when including the LinterRegistry module' do 5 | after do 6 | described_class.linters.delete(FakeLinter) 7 | end 8 | 9 | it 'adds the linter to the set of registered linters' do 10 | expect do 11 | class FakeLinter < SCSSLint::Linter 12 | include SCSSLint::LinterRegistry 13 | end 14 | end.to change { described_class.linters.count }.by(1) 15 | end 16 | end 17 | 18 | describe '.extract_linters_from' do 19 | module SCSSLint 20 | class Linter::SomeLinter < Linter; end 21 | class Linter::SomeOtherLinter < Linter; end 22 | end 23 | 24 | let(:linters) do 25 | [SCSSLint::Linter::SomeLinter, SCSSLint::Linter::SomeOtherLinter] 26 | end 27 | 28 | before do 29 | described_class.stub(:linters).and_return(linters) 30 | end 31 | 32 | context 'when the linters exist' do 33 | let(:linter_names) { %w[SomeLinter SomeOtherLinter] } 34 | 35 | it 'returns the linters' do 36 | subject.extract_linters_from(linter_names).should == linters 37 | end 38 | end 39 | 40 | context "when the linters don't exist" do 41 | let(:linter_names) { ['SomeRandomLinter'] } 42 | 43 | it 'raises an error' do 44 | expect do 45 | subject.extract_linters_from(linter_names) 46 | end.to raise_error(SCSSLint::NoSuchLinter) 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/scss_lint/location_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Location do 4 | let(:engine) { described_class.new(css) } 5 | 6 | describe '#<=>' do 7 | let(:locations) do 8 | [ 9 | SCSSLint::Location.new(2, 2, 2), 10 | SCSSLint::Location.new(2, 2, 1), 11 | SCSSLint::Location.new(2, 1, 2), 12 | SCSSLint::Location.new(2, 1, 1), 13 | SCSSLint::Location.new(1, 2, 2), 14 | SCSSLint::Location.new(1, 2, 1), 15 | SCSSLint::Location.new(1, 1, 2), 16 | SCSSLint::Location.new(1, 1, 1) 17 | ] 18 | end 19 | 20 | it 'allows locations to be sorted' do 21 | locations.sort.should == [ 22 | SCSSLint::Location.new(1, 1, 1), 23 | SCSSLint::Location.new(1, 1, 2), 24 | SCSSLint::Location.new(1, 2, 1), 25 | SCSSLint::Location.new(1, 2, 2), 26 | SCSSLint::Location.new(2, 1, 1), 27 | SCSSLint::Location.new(2, 1, 2), 28 | SCSSLint::Location.new(2, 2, 1), 29 | SCSSLint::Location.new(2, 2, 2) 30 | ] 31 | end 32 | 33 | context 'when the same location is passed' do 34 | let(:location) { SCSSLint::Location.new(1, 1, 1) } 35 | let(:other_location) { SCSSLint::Location.new(1, 1, 1) } 36 | 37 | it 'returns 0' do 38 | (location <=> other_location).should == 0 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/scss_lint/logger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Logger do 4 | let(:io) { StringIO.new } 5 | let(:logger) { described_class.new(io) } 6 | 7 | describe '#color_enabled' do 8 | subject { io.string } 9 | 10 | before do 11 | logger.color_enabled = enabled 12 | logger.success('Success!') 13 | end 14 | 15 | context 'when color is enabled' do 16 | let(:enabled) { true } 17 | 18 | it { should include '32' } 19 | end 20 | 21 | context 'when color is disabled' do 22 | let(:enabled) { false } 23 | 24 | it { should_not include '32' } 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/scss_lint/options_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'scss_lint/options' 3 | 4 | describe SCSSLint::Options do 5 | describe '#parse' do 6 | subject { super().parse(args) } 7 | 8 | context 'when no arguments are specified' do 9 | let(:args) { [] } 10 | 11 | it { should be_a Hash } 12 | 13 | it 'defines no files to lint by default' do 14 | subject[:files].should be_empty 15 | end 16 | 17 | it 'specifies the DefaultReporter by default' do 18 | subject[:reporters].first.should include 'Default' 19 | end 20 | 21 | it 'outputs to STDOUT' do 22 | subject[:reporters].first.should include :stdout 23 | end 24 | end 25 | 26 | context 'when a non-existent flag is specified' do 27 | let(:args) { ['--totally-made-up-flag'] } 28 | 29 | it 'raises an error' do 30 | expect { subject }.to raise_error SCSSLint::Exceptions::InvalidCLIOption 31 | end 32 | end 33 | 34 | context 'color' do 35 | describe 'manually on' do 36 | let(:args) { ['--color'] } 37 | 38 | it 'sets the `color` option to true' do 39 | subject.should include color: true 40 | end 41 | end 42 | 43 | describe 'manually off' do 44 | let(:args) { ['--no-color'] } 45 | 46 | it 'sets the `color option to false' do 47 | subject.should include color: false 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/scss_lint/plugins/linter_dir_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Plugins::LinterDir do 4 | let(:plugin_directory) { File.expand_path('../fixtures/plugins', __dir__) } 5 | let(:subject) { described_class.new(plugin_directory) } 6 | 7 | describe '#load' do 8 | let(:config_file) { File.join(plugin_directory, '.scss-lint.yml') } 9 | let(:config_file_exists) { false } 10 | 11 | before do 12 | File.stub(:exist?).with(config_file).and_return(config_file_exists) 13 | end 14 | 15 | it 'requires each file in the plugin directory' do 16 | subject.should_receive(:require) 17 | .with(File.join(plugin_directory, 'linter_plugin.rb')).once 18 | 19 | subject.load 20 | end 21 | 22 | context 'when the dir does not include a configuration file' do 23 | it 'loads an empty configuration' do 24 | subject.load 25 | subject.config.should == SCSSLint::Config.new({}) 26 | end 27 | end 28 | 29 | context 'when a config file exists in the dir' do 30 | let(:config_file_exists) { true } 31 | let(:fake_config) { SCSSLint::Config.new('linters' => { 'FakeLinter' => {} }) } 32 | 33 | before do 34 | SCSSLint::Config.should_receive(:load) 35 | .with(config_file, merge_with_default: false) 36 | .and_return(fake_config) 37 | end 38 | 39 | it 'loads the configuration' do 40 | subject.load 41 | subject.config.should == fake_config 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/scss_lint/plugins/linter_gem_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module SCSSLint 4 | describe Plugins::LinterGem do 5 | let(:subject) { described_class.new('a_gem') } 6 | 7 | describe '#load' do 8 | let(:gem_dir) { '/gem_dir' } 9 | let(:config_file) { File.join(gem_dir, '.scss-lint.yml') } 10 | let(:config_file_exists) { false } 11 | 12 | before do 13 | File.stub(:exist?).with(config_file).and_return(config_file_exists) 14 | end 15 | 16 | context 'when the gem does not exist' do 17 | it 'raises an exception' do 18 | expect { subject.load }.to raise_error Exceptions::PluginGemLoadError 19 | end 20 | end 21 | 22 | context 'when the gem exists' do 23 | before do 24 | subject.stub(:require).with('a_gem').and_return(true) 25 | Gem::Specification.stub(:find_by_name) 26 | .with('a_gem') 27 | .and_return(double(gem_dir: gem_dir)) 28 | end 29 | 30 | it 'requires the gem' do 31 | subject.should_receive(:require).with('a_gem').once 32 | subject.load 33 | end 34 | 35 | context 'when the gem does not include a configuration file' do 36 | it 'loads an empty configuration' do 37 | subject.load 38 | subject.config.should == Config.new({}) 39 | end 40 | end 41 | 42 | context 'when a config file exists in the gem' do 43 | let(:config_file_exists) { true } 44 | let(:fake_config) { Config.new('linters' => { 'FakeLinter' => {} }) } 45 | 46 | before do 47 | Config.should_receive(:load) 48 | .with(config_file, merge_with_default: false) 49 | .and_return(fake_config) 50 | end 51 | 52 | it 'loads the configuration' do 53 | subject.load 54 | subject.config.should == fake_config 55 | end 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/scss_lint/plugins_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module SCSSLint 4 | describe Plugins do 5 | let(:subject) { described_class.new(Config.new(config_options, Config.user_file)) } 6 | 7 | describe '#load' do 8 | context 'when gem plugins are specified' do 9 | let(:config_options) { { 'plugin_gems' => ['a_gem'] } } 10 | let(:plugin) { double(load: nil) } 11 | 12 | before do 13 | Plugins::LinterGem.stub(:new).with('a_gem').and_return(plugin) 14 | end 15 | 16 | it 'loads the plugin' do 17 | plugin.should_receive(:load) 18 | subject.load 19 | end 20 | end 21 | 22 | context 'when directory plugins are specified' do 23 | let(:config_options) { { 'plugin_directories' => ['some_dir'] } } 24 | let(:plugin) { double(load: nil) } 25 | let(:plugin_dir) { File.join(File.dirname(Config.user_file), 'some_dir') } 26 | 27 | before do 28 | Plugins::LinterDir.stub(:new).with(plugin_dir).and_return(plugin) 29 | end 30 | 31 | it 'loads the plugin' do 32 | plugin.should_receive(:load) 33 | subject.load 34 | end 35 | end 36 | 37 | context 'when plugins options are empty lists' do 38 | let(:config_options) { { 'plugin_directories' => [], 'plugin_gems' => [] } } 39 | 40 | it 'returns empty array' do 41 | subject.load.should == [] 42 | end 43 | end 44 | 45 | context 'when no plugins options are specified' do 46 | let(:config_options) { {} } 47 | 48 | it 'returns empty array' do 49 | subject.load.should == [] 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/scss_lint/preprocess_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Engine do 4 | let(:engine) { described_class.new(options) } 5 | let(:command) { 'my-command' } 6 | let(:scss) { <<-SCSS } 7 | --- 8 | --- 9 | $red: #f00; 10 | SCSS 11 | let(:processed) { <<-SCSS } 12 | $red: #f00; 13 | SCSS 14 | 15 | context 'preprocess_command is specified' do 16 | let(:options) { { code: scss, preprocess_command: command } } 17 | 18 | it 'preprocesses, and Sass is able to parse' do 19 | open3 = class_double('Open3').as_stubbed_const 20 | open3.should_receive(:capture2).with(command, stdin_data: scss).and_return([processed, 0]) 21 | 22 | variable = engine.tree.children[0] 23 | expect(variable).to be_instance_of(Sass::Tree::VariableNode) 24 | expect(variable.name).to eq('red') 25 | end 26 | end 27 | 28 | context 'preprocessor fails' do 29 | let(:options) { { code: scss, preprocess_command: command } } 30 | 31 | it 'preprocesses, and Sass is able to parse' do 32 | open3 = class_double('Open3').as_stubbed_const 33 | open3.should_receive(:capture2).with(command, stdin_data: scss).and_return([processed, 1]) 34 | 35 | expect { engine }.to raise_error(SCSSLint::Exceptions::PreprocessorError) 36 | end 37 | end 38 | 39 | context 'both preprocess_command and preprocess_files are specified' do 40 | let(:path) { 'foo/a.scss' } 41 | 42 | context 'file should be preprocessed' do 43 | let(:options) do 44 | { path: path, 45 | preprocess_command: command, 46 | preprocess_files: ['foo/*.scss'] } 47 | end 48 | 49 | it 'preprocesses, and Sass is able to parse' do 50 | open3 = class_double('Open3').as_stubbed_const 51 | open3.should_receive(:capture2).with(command, stdin_data: scss).and_return([processed, 0]) 52 | File.should_receive(:read).with(path).and_return(scss) 53 | 54 | variable = engine.tree.children[0] 55 | expect(variable).to be_instance_of(Sass::Tree::VariableNode) 56 | expect(variable.name).to eq('red') 57 | end 58 | end 59 | 60 | context 'file should not be preprocessed' do 61 | let(:options) do 62 | { path: path, 63 | preprocess_command: command, 64 | preprocess_files: ['bar/*.scss'] } 65 | end 66 | 67 | it 'does not preprocess, and Sass throws' do 68 | File.should_receive(:read).with(path).and_return(scss) 69 | expect { engine }.to raise_error(Sass::SyntaxError) 70 | end 71 | end 72 | 73 | context 'code should never be preprocessed' do 74 | let(:options) do 75 | { code: scss, 76 | preprocess_command: command, 77 | preprocess_files: ['foo/*.scss'] } 78 | end 79 | 80 | it 'does not preprocess, and Sass throws' do 81 | expect { engine }.to raise_error(Sass::SyntaxError) 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/scss_lint/rake_task_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'scss_lint/rake_task' 3 | require 'tempfile' 4 | 5 | describe SCSSLint::RakeTask do 6 | before do 7 | $stdout.stub(:write) # Silence console output 8 | end 9 | 10 | after(:each) do 11 | Rake::Task['scss_lint'].clear if Rake::Task.task_defined?('scss_lint') 12 | end 13 | 14 | let(:file) do 15 | Tempfile.new(%w[scss-file .scss]).tap do |f| 16 | f.write(scss) 17 | f.close 18 | end 19 | end 20 | 21 | def run_task 22 | Rake::Task[:scss_lint].tap do |t| 23 | t.reenable # Allows us to execute task multiple times 24 | t.invoke(file.path) 25 | end 26 | end 27 | 28 | context 'basic RakeTask' do 29 | before(:each) do 30 | SCSSLint::RakeTask.new 31 | end 32 | 33 | context 'when SCSS document is valid with no lints' do 34 | let(:scss) { '' } 35 | 36 | it 'does not call Kernel.exit' do 37 | expect { run_task }.not_to raise_error 38 | end 39 | end 40 | 41 | context 'when SCSS document is invalid' do 42 | let(:scss) { '.class {' } 43 | 44 | it 'calls Kernel.exit with the status code' do 45 | expect { run_task }.to raise_error SystemExit 46 | end 47 | end 48 | end 49 | 50 | context 'configured RakeTask with a config file' do 51 | let(:scss) { '' } 52 | 53 | let(:config_file) do 54 | config = Tempfile.new(%w[foo .yml]) 55 | config.write('') 56 | config.close 57 | config.path 58 | end 59 | 60 | it 'passes config files to the CLI' do 61 | SCSSLint::RakeTask.new.tap do |t| 62 | t.config = config_file 63 | end 64 | 65 | cli = double(SCSSLint::CLI) 66 | SCSSLint::CLI.should_receive(:new) { cli } 67 | args = ['--config', config_file, file.path] 68 | cli.should_receive(:run).with(args) { 0 } 69 | 70 | expect { run_task }.not_to raise_error 71 | end 72 | end 73 | 74 | context 'configured RakeTask with args' do 75 | let(:scss) { '' } 76 | 77 | it 'passes args to the CLI' do 78 | formatter_args = ['--formatter', 'JSON'] 79 | 80 | SCSSLint::RakeTask.new.tap do |t| 81 | t.args = formatter_args 82 | end 83 | 84 | cli = double(SCSSLint::CLI) 85 | SCSSLint::CLI.should_receive(:new) { cli } 86 | args = formatter_args + [file.path] 87 | cli.should_receive(:run).with(args) { 0 } 88 | 89 | expect { run_task }.not_to raise_error 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/scss_lint/reporter/clean_files_reporter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Reporter::CleanFilesReporter do 4 | subject { described_class.new(lints, files, nil) } 5 | 6 | describe '#report_lints' do 7 | context 'when there are no lints and no files' do 8 | let(:files) { [] } 9 | let(:lints) { [] } 10 | 11 | it 'returns nil' do 12 | subject.report_lints.should be_nil 13 | end 14 | end 15 | 16 | context 'when there are no lints but some files were linted' do 17 | let(:files) { [{ 'path' => 'c.scss' }, { 'path' => 'b.scss' }, { 'path' => 'a.scss' }] } 18 | let(:lints) { [] } 19 | 20 | it 'prints each file on its own line' do 21 | subject.report_lints.count("\n").should == 3 22 | end 23 | 24 | it 'prints the files in order' do 25 | subject.report_lints.split("\n")[0].should eq 'a.scss' 26 | subject.report_lints.split("\n")[1].should eq 'b.scss' 27 | subject.report_lints.split("\n")[2].should eq 'c.scss' 28 | end 29 | 30 | it 'prints a trailing newline' do 31 | subject.report_lints[-1].should == "\n" 32 | end 33 | end 34 | 35 | context 'when there are lints in some files' do 36 | let(:dirty_files) { [{ 'path' => 'a.scss' }, { 'path' => 'b.scss' }] } 37 | let(:clean_files) { [{ 'path' => 'c.scss' }, { 'path' => 'd.scss' }] } 38 | let(:files) { dirty_files + clean_files } 39 | 40 | let(:lints) do 41 | dirty_files.map do |file| 42 | SCSSLint::Lint.new(SCSSLint::Linter::Comment.new, file['path'], SCSSLint::Location.new, '') 43 | end 44 | end 45 | 46 | it 'prints the file for each lint' do 47 | clean_files.each do |file| 48 | subject.report_lints.scan(file['path']).count.should == 1 49 | end 50 | end 51 | 52 | it 'does not print clean files' do 53 | dirty_files.each do |file| 54 | subject.report_lints.scan(file['path']).count.should == 0 55 | end 56 | end 57 | end 58 | 59 | context 'when there are lints in every file' do 60 | let(:files) { %w[a.scss b.scss c.scss d.scss] } 61 | 62 | let(:lints) do 63 | files.map do |file| 64 | SCSSLint::Lint.new(SCSSLint::Linter::Comment.new, file, SCSSLint::Location.new, '') 65 | end 66 | end 67 | 68 | it 'does not print clean files' do 69 | subject.report_lints.should be_nil 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/scss_lint/reporter/config_reporter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Reporter::ConfigReporter do 4 | subject { YAML.load(result) } 5 | let(:result) { described_class.new(lints, [], nil).report_lints } 6 | 7 | describe '#report_lints' do 8 | context 'when there are no lints' do 9 | let(:lints) { [] } 10 | 11 | it 'returns nil' do 12 | result.should be_nil 13 | end 14 | end 15 | 16 | context 'when there are lints' do 17 | let(:linters) do 18 | [SCSSLint::Linter::FinalNewline, SCSSLint::Linter::BorderZero, 19 | SCSSLint::Linter::BorderZero] 20 | end 21 | let(:lints) do 22 | linters.each.map do |linter| 23 | SCSSLint::Lint.new(linter.new, '', 24 | SCSSLint::Location.new, '') 25 | end 26 | end 27 | 28 | it 'adds one entry per linter' do 29 | subject['linters'].size.should eq 2 30 | end 31 | 32 | it 'sorts linters by name' do 33 | subject['linters'].map(&:first).should eq %w[BorderZero FinalNewline] 34 | end 35 | 36 | it 'disables all found linters' do 37 | subject['linters']['BorderZero']['enabled'].should eq false 38 | subject['linters']['FinalNewline']['enabled'].should eq false 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/scss_lint/reporter/default_reporter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Reporter::DefaultReporter do 4 | let(:logger) { SCSSLint::Logger.new($stdout) } 5 | subject { SCSSLint::Reporter::DefaultReporter.new(lints, [], logger) } 6 | 7 | describe '#report_lints' do 8 | context 'when there are no lints' do 9 | let(:lints) { [] } 10 | 11 | it 'returns nil' do 12 | subject.report_lints.should be_nil 13 | end 14 | end 15 | 16 | context 'when there are lints' do 17 | let(:filenames) { ['some-filename.scss', 'other-filename.scss'] } 18 | let(:locations) { [[502, 3], [724, 6]] } 19 | let(:descriptions) { ['Description of lint 1', 'Description of lint 2'] } 20 | let(:severities) { [:warning] * 2 } 21 | let(:lints) do 22 | filenames.each_with_index.map do |filename, index| 23 | line, column = locations[index] 24 | location = SCSSLint::Location.new(line, column, 10) 25 | SCSSLint::Lint.new(SCSSLint::Linter::Comment, filename, location, descriptions[index], 26 | severities[index]) 27 | end 28 | end 29 | 30 | it 'prints each lint on its own line' do 31 | subject.report_lints.count("\n").should == 2 32 | end 33 | 34 | it 'prints a trailing newline' do 35 | subject.report_lints[-1].should == "\n" 36 | end 37 | 38 | it 'prints the filename for each lint' do 39 | filenames.each do |filename| 40 | subject.report_lints.scan(filename).count.should == 1 41 | end 42 | end 43 | 44 | it 'prints the line number for each lint' do 45 | locations.each do |location| 46 | subject.report_lints.scan(location[0].to_s).count.should == 1 47 | end 48 | end 49 | 50 | it 'prints the column number for each lint' do 51 | locations.each do |location| 52 | subject.report_lints.scan(location[1].to_s).count.should == 1 53 | end 54 | end 55 | 56 | it 'prints the description for each lint' do 57 | descriptions.each do |description| 58 | subject.report_lints.scan(description).count.should == 1 59 | end 60 | end 61 | 62 | context 'when lints are warnings' do 63 | it 'prints the warning severity code on each line' do 64 | subject.report_lints.split("\n").each do |line| 65 | line.scan(/\[W\]/).count.should == 1 66 | end 67 | end 68 | end 69 | 70 | context 'when lints are errors' do 71 | let(:severities) { [:error] * 2 } 72 | 73 | it 'prints the error severity code on each line' do 74 | subject.report_lints.split("\n").each do |line| 75 | line.scan(/\[E\]/).count.should == 1 76 | end 77 | end 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/scss_lint/reporter/files_reporter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Reporter::FilesReporter do 4 | subject { described_class.new(lints, [], nil) } 5 | 6 | describe '#report_lints' do 7 | context 'when there are no lints' do 8 | let(:lints) { [] } 9 | 10 | it 'returns nil' do 11 | subject.report_lints.should be_nil 12 | end 13 | end 14 | 15 | context 'when there are lints' do 16 | let(:filenames) { ['some-filename.scss', 'some-filename.scss', 'other-filename.scss'] } 17 | 18 | let(:lints) do 19 | filenames.map do |filename| 20 | SCSSLint::Lint.new(SCSSLint::Linter::Comment.new, filename, SCSSLint::Location.new, '') 21 | end 22 | end 23 | 24 | it 'prints each file on its own line' do 25 | subject.report_lints.count("\n").should == 2 26 | end 27 | 28 | it 'prints a trailing newline' do 29 | subject.report_lints[-1].should == "\n" 30 | end 31 | 32 | it 'prints the filename for each lint' do 33 | filenames.each do |filename| 34 | subject.report_lints.scan(filename).count.should == 1 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/scss_lint/reporter/json_reporter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Reporter::JSONReporter do 4 | subject { SCSSLint::Reporter::JSONReporter.new(lints, [], nil) } 5 | 6 | describe '#report_lints' do 7 | let(:json) { JSON.parse(subject.report_lints) } 8 | 9 | shared_examples_for 'parsed JSON' do 10 | it 'is a Hash' do 11 | json.is_a?(Hash) 12 | end 13 | end 14 | 15 | context 'when there are no lints' do 16 | let(:lints) { [] } 17 | 18 | it_should_behave_like 'parsed JSON' 19 | end 20 | 21 | context 'when there are lints' do 22 | let(:filenames) { ['f1.scss', 'f2.scss', 'f1.scss'] } 23 | # Include invalid XML characters in the third description to validate 24 | # that escaping happens for preventing broken XML output 25 | let(:descriptions) { ['lint 1', 'lint 2', 'lint 3 " \' < & >'] } 26 | let(:severities) { [:warning] * 3 } 27 | 28 | let(:locations) do 29 | [ 30 | SCSSLint::Location.new(5, 2, 3), 31 | SCSSLint::Location.new(7, 6, 2), 32 | SCSSLint::Location.new(9, 10, 1) 33 | ] 34 | end 35 | 36 | let(:lints) do 37 | filenames.each_with_index.map do |filename, index| 38 | SCSSLint::Lint.new(SCSSLint::LinterRegistry.linters.sample, filename, locations[index], 39 | descriptions[index], severities[index]) 40 | end 41 | end 42 | 43 | it_should_behave_like 'parsed JSON' 44 | 45 | it 'contains an node for each lint' do 46 | json.values.inject(0) { |sum, issues| sum + issues.size }.should == 3 47 | end 48 | 49 | it 'contains a group of issues for each file' do 50 | json.keys.should == filenames.uniq 51 | end 52 | 53 | it 'contains nodes grouped by ' do 54 | json.values.map(&:size).should == [2, 1] 55 | end 56 | 57 | it 'marks each issue with a line number' do 58 | json.values.flat_map { |issues| issues.map { |issue| issue['line'] } } 59 | .should =~ locations.map(&:line) 60 | end 61 | 62 | it 'marks each issue with a column number' do 63 | json.values.flat_map { |issues| issues.map { |issue| issue['column'] } } 64 | .should =~ locations.map(&:column) 65 | end 66 | 67 | it 'marks each issue with a length' do 68 | json.values.flat_map { |issues| issues.map { |issue| issue['length'] } } 69 | .should =~ locations.map(&:length) 70 | end 71 | 72 | it 'marks each issue with a reason containing the lint description' do 73 | json.values.flat_map { |issues| issues.map { |issue| issue['reason'] } } 74 | .should =~ descriptions 75 | end 76 | 77 | context 'when lints are warnings' do 78 | it 'marks each issue with a severity of "warning"' do 79 | json.values.inject(0) do |sum, issues| 80 | sum + issues.count { |i| i['severity'] == 'warning' } 81 | end.should == 3 82 | end 83 | end 84 | 85 | context 'when lints are errors' do 86 | let(:severities) { [:error] * 3 } 87 | 88 | it 'marks each issue with a severity of "error"' do 89 | json.values.inject(0) do |sum, issues| 90 | sum + issues.count { |i| i['severity'] == 'error' } 91 | end.should == 3 92 | end 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/scss_lint/reporter/tap_reporter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Reporter::TAPReporter do 4 | let(:logger) { SCSSLint::Logger.new($stdout) } 5 | let(:files) { filenames.map { |filename| { path: filename } } } 6 | subject { described_class.new(lints, files, logger) } 7 | 8 | describe '#report_lints' do 9 | context 'when there are no files' do 10 | let(:filenames) { [] } 11 | let(:lints) { [] } 12 | 13 | it 'returns the TAP version, plan, and explanation' do 14 | subject.report_lints.should == "TAP version 13\n1..0 # No files to lint\n" 15 | end 16 | end 17 | 18 | context 'when there are files but no lints' do 19 | let(:filenames) { ['file.scss', 'another-file.scss'] } 20 | let(:lints) { [] } 21 | 22 | it 'returns the TAP version, plan, and ok test lines' do 23 | subject.report_lints.should eq(<<-LINES) 24 | TAP version 13 25 | 1..2 26 | ok 1 - file.scss 27 | ok 2 - another-file.scss 28 | LINES 29 | end 30 | end 31 | 32 | context 'when there are some lints' do 33 | let(:filenames) { %w[ok1.scss not-ok1.scss not-ok2.scss ok2.scss] } 34 | 35 | let(:lints) do 36 | [ 37 | SCSSLint::Lint.new( 38 | SCSSLint::Linter::PrivateNamingConvention, 39 | filenames[1], 40 | SCSSLint::Location.new(123, 10, 8), 41 | 'Description of lint 1', 42 | :warning 43 | ), 44 | SCSSLint::Lint.new( 45 | SCSSLint::Linter::PrivateNamingConvention, 46 | filenames[2], 47 | SCSSLint::Location.new(20, 2, 6), 48 | 'Description of lint 2', 49 | :error 50 | ), 51 | SCSSLint::Lint.new( 52 | SCSSLint::Linter::PrivateNamingConvention, 53 | filenames[2], 54 | SCSSLint::Location.new(21, 3, 4), 55 | 'Description of lint 3', 56 | :warning 57 | ), 58 | ] 59 | end 60 | 61 | it 'returns the TAP version, plan, and correct test lines' do 62 | subject.report_lints.should eq(<<-LINES) 63 | TAP version 13 64 | 1..5 65 | ok 1 - ok1.scss 66 | not ok 2 - not-ok1.scss:123:10 SCSSLint::Linter::PrivateNamingConvention 67 | --- 68 | message: Description of lint 1 69 | severity: warning 70 | file: not-ok1.scss 71 | line: 123 72 | column: 10 73 | name: SCSSLint::Linter::PrivateNamingConvention 74 | ... 75 | not ok 3 - not-ok2.scss:20:2 SCSSLint::Linter::PrivateNamingConvention 76 | --- 77 | message: Description of lint 2 78 | severity: error 79 | file: not-ok2.scss 80 | line: 20 81 | column: 2 82 | name: SCSSLint::Linter::PrivateNamingConvention 83 | ... 84 | not ok 4 - not-ok2.scss:21:3 SCSSLint::Linter::PrivateNamingConvention 85 | --- 86 | message: Description of lint 3 87 | severity: warning 88 | file: not-ok2.scss 89 | line: 21 90 | column: 3 91 | name: SCSSLint::Linter::PrivateNamingConvention 92 | ... 93 | ok 5 - ok2.scss 94 | LINES 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/scss_lint/reporter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SCSSLint::Reporter do 4 | class SCSSLint::Reporter::FakeReporter < SCSSLint::Reporter; end 5 | 6 | describe '#descendants' do 7 | it 'contains FakeReporter' do 8 | SCSSLint::Reporter.descendants.should include(SCSSLint::Reporter::FakeReporter) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['TRAVIS'] 2 | # When running in Travis, report coverage stats to Coveralls. 3 | require 'coveralls' 4 | Coveralls.wear! 5 | else 6 | # Otherwise render coverage information in coverage/index.html and display 7 | # coverage percentage in the console. 8 | require 'simplecov' 9 | end 10 | 11 | require 'scss_lint' 12 | 13 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f } 14 | 15 | RSpec.configure do |config| 16 | config.expect_with :rspec do |c| 17 | c.syntax = %i[expect should] 18 | end 19 | 20 | config.mock_with :rspec do |c| 21 | c.syntax = :should 22 | end 23 | 24 | config.before(:each) do 25 | # If running a linter spec, run the described linter against the CSS code 26 | # for each example. This significantly DRYs up our linter specs to contain 27 | # only tests, since all the setup code is now centralized here. 28 | if described_class && described_class <= SCSSLint::Linter 29 | initial_indent = scss[/\A(\s*)/, 1] 30 | normalized_css = scss.gsub(/^#{initial_indent}/, '') 31 | 32 | # Use the configuration settings defined by default unless a specific 33 | # configuration has been provided for the test. 34 | local_config = if respond_to?(:linter_config) 35 | linter_config 36 | else 37 | SCSSLint::Config.default.linter_options(subject) 38 | end 39 | 40 | subject.run(SCSSLint::Engine.new(code: normalized_css), local_config) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/support/isolated_environment.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'tmpdir' 3 | 4 | shared_context 'isolated environment' do 5 | around do |example| 6 | Dir.mktmpdir do |tmpdir| 7 | original_home = ENV['HOME'] 8 | 9 | begin 10 | virtual_home = File.expand_path(File.join(tmpdir, 'home')) 11 | Dir.mkdir(virtual_home) 12 | ENV['HOME'] = virtual_home 13 | 14 | working_dir = File.join(tmpdir, 'work') 15 | Dir.mkdir(working_dir) 16 | 17 | Dir.chdir(working_dir) do 18 | example.run 19 | end 20 | ensure 21 | ENV['HOME'] = original_home 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/support/matchers/report_lint.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :report_lint do |options| 2 | options ||= {} 3 | count = options[:count] 4 | expected_line = options[:line] 5 | 6 | match do |linter| 7 | has_lints?(linter, expected_line, count) 8 | end 9 | 10 | failure_message do |linter| 11 | expected_count = 12 | if count.nil? 13 | 'a lint' 14 | elsif count == 1 15 | 'exactly 1 lint' 16 | else 17 | "exactly #{count} lints" 18 | end 19 | 20 | "expected that #{expected_count} would be reported" + 21 | (expected_line ? " on line #{expected_line}" : '') + 22 | case linter.lints.count 23 | when 0 24 | '' 25 | when 1 26 | ", but one lint was reported on line #{linter.lints.first.location.line}" 27 | else 28 | lines = lint_lines(linter) 29 | ", but lints were reported on lines #{lines[0...-1].join(', ')} and #{lines.last}" 30 | end 31 | end 32 | 33 | failure_message_when_negated do 34 | 'expected that a lint would not be reported' 35 | end 36 | 37 | description do 38 | "report a lint#{expected_line ? " on line #{expected_line}" : ''}" 39 | end 40 | 41 | def has_lints?(linter, expected_line, count) 42 | if expected_line && count 43 | linter.lints.count == count && 44 | lint_lines(linter).all? { |line| line == expected_line } 45 | elsif expected_line 46 | lint_lines(linter).include?(expected_line) 47 | elsif count 48 | linter.lints.count == count 49 | else 50 | linter.lints.count > 0 51 | end 52 | end 53 | 54 | def lint_lines(linter) 55 | linter.lints.map { |lint| lint.location.line } 56 | end 57 | end 58 | --------------------------------------------------------------------------------