Skip to content

Commit

Permalink
Add preprocess_command option
Browse files Browse the repository at this point in the history
  • Loading branch information
srawlins authored and sds committed Feb 23, 2016
1 parent 9d4598f commit f3d0f94
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 8 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ it into your [SCM hooks](https://github.com/brigade/overcommit).
* [Exit Status Codes](#exit-status-codes)
* [Linters](#linters)
* [Custom Linters](#custom-linters)
* [Preprocessing](#preprocessing)
* [Editor Integration](#editor-integration)
* [Git Integration](#git-integration)
* [Rake Integration](#rake-integration)
Expand Down Expand Up @@ -451,6 +452,29 @@ gem 'scss_lint_plugin_example', git: 'git://github.com/cih/scss_lint_plugin_exam
As long as you execute `scss-lint` via `bundle exec scss-lint`, it should be
able to load the gem.

## Preprocessing

Sometimes SCSS files need to be preprocessed before being linted. This is made
possible with two options that can be specified in your configuration file.

The `preprocess_command` option specifies the command to run once per SCSS
file. The command can be specified with arguments. The contents of a SCSS
file will be written to STDIN, and the processed SCSS contents must be written
to STDOUT. If the process exits with a code other than 0, scss-lint will
immediately exit with an error.

For example, `preprocess_command: "cat"` specifies a simple no-op preprocessor
(on Unix-like systems). `cat` simply writes the contents of STDIN back out to
STDOUT. To preprocess SCSS files with
[Jekyll front matter](http://jekyllrb.com/docs/assets/), you can use
`preprocess_command: "sed '1,2s/---//'"`. This will strip out any Jekyll front
matter, but preserve line numbers.
If only some SCSS files need to be preprocessed, you may use the
`preprocess_files` option to specify a list of file globs that need
preprocessing. Preprocessing only a subset of files should make scss-lint more
performant.

## Editor Integration

### Vim
Expand Down
4 changes: 4 additions & 0 deletions lib/scss_lint/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class CLI
config: 78, # Configuration error
no_files: 80, # No files matched by specified glob patterns
plugin: 82, # Plugin loading error
preprocessor: 84, # Preprocessor error
}.freeze

# Create a CLI that outputs to the specified logger.
Expand Down Expand Up @@ -106,6 +107,9 @@ def handle_runtime_exception(exception, options) # rubocop:disable Metrics/AbcSi
when NoSuchLinter
log.error exception.message
halt :usage
when SCSSLint::Exceptions::PreprocessorError
log.error exception.message
halt :preprocessor
else
config_file = relevant_configuration_file(options) if options

Expand Down
35 changes: 29 additions & 6 deletions lib/scss_lint/engine.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require 'sass'

require 'open3'

module SCSSLint
class FileEncodingError < StandardError; end

Expand All @@ -15,13 +17,14 @@ class Engine
#
# @param options [Hash]
# @option options [String] :file The file to load
# @option options [String] :path The path of the file to load
# @option options [String] :code The code to parse
# @option options [String] :preprocess_command A preprocessing command
# @option options [List<String>] :preprocess_files A list of files that should be preprocessed
def initialize(options = {})
if options[:path]
build_from_file(options)
elsif options[:code]
build_from_string(options[:code])
end
@preprocess_command = options[:preprocess_command]
@preprocess_files = options[:preprocess_files]
build(options)

# Need to force encoding to avoid Windows-related bugs.
# Need to encode with universal newline to avoid other Windows-related bugs.
Expand All @@ -45,25 +48,45 @@ def initialize(options = {})

private

def build(options)
if options[:path]
build_from_file(options)
elsif options[:code]
build_from_string(options[:code])
end
end

# @param options [Hash]
# @option file [IO] if provided, us this as the file object
# @option path [String] path of file, loading from this if `file` object not
# given
def build_from_file(options)
@filename = options[:path]
@contents = options[:file] ? options[:file].read : File.read(@filename)
preprocess_contents
@engine = Sass::Engine.new(@contents, ENGINE_OPTIONS.merge(filename: @filename))
end

# @param scss [String]
def build_from_string(scss)
@engine = Sass::Engine.new(scss, ENGINE_OPTIONS)
@contents = scss
preprocess_contents
@engine = Sass::Engine.new(@contents, ENGINE_OPTIONS)
end

def find_any_control_commands
@any_control_commands =
@lines.any? { |line| line['scss-lint:disable'] || line['scss-line:enable'] }
end

def preprocess_contents # rubocop:disable CyclomaticComplexity
return unless @preprocess_command
# Never preprocess :code scss if @preprocess_files is specified.
return if @preprocess_files && @filename.nil?
return if @preprocess_files &&
@preprocess_files.none? { |pattern| File.fnmatch(pattern, @filename) }
@contents, status = Open3.capture2(@preprocess_command, stdin_data: @contents)
raise SCSSLint::Exceptions::PreprocessorError if status != 0
end
end
end
3 changes: 3 additions & 0 deletions lib/scss_lint/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ class RequiredLibraryMissingError < StandardError; end

# Raised when a linter gem plugin is required but not installed.
class PluginGemLoadError < StandardError; end

# Raised when the preprocessor tool exits with a non-zero code.
class PreprocessorError < StandardError; end
end
6 changes: 4 additions & 2 deletions lib/scss_lint/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ def run(files)
# @param file [Hash]
# @option file [String] File object
# @option path [String] path to File (determines which Linter config to apply)
def find_lints(file)
engine = Engine.new(file)
def find_lints(file) # rubocop:disable AbcSize
options = file.merge(preprocess_command: @config.options['preprocess_command'],
preprocess_files: @config.options['preprocess_files'])
engine = Engine.new(options)

@linters.each do |linter|
begin
Expand Down
85 changes: 85 additions & 0 deletions spec/scss_lint/preprocess_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
require 'spec_helper'

describe SCSSLint::Engine do
let(:engine) { described_class.new(options) }
let(:command) { 'my-command' }
let(:scss) { <<-SCSS }
---
---
$red: #f00;
SCSS
let(:processed) { <<-SCSS }
$red: #f00;
SCSS

context 'preprocess_command is specified' do
let(:options) { { code: scss, preprocess_command: command } }

it 'preprocesses, and Sass is able to parse' do
open3 = class_double('Open3').as_stubbed_const
open3.should_receive(:capture2).with(command, stdin_data: scss).and_return([processed, 0])

variable = engine.tree.children[0]
expect(variable).to be_instance_of(Sass::Tree::VariableNode)
expect(variable.name).to eq('red')
end
end

context 'preprocessor fails' do
let(:options) { { code: scss, preprocess_command: command } }

it 'preprocesses, and Sass is able to parse' do
open3 = class_double('Open3').as_stubbed_const
open3.should_receive(:capture2).with(command, stdin_data: scss).and_return([processed, 1])

expect { engine }.to raise_error(SCSSLint::Exceptions::PreprocessorError)
end
end

context 'both preprocess_command and preprocess_files are specified' do
let(:path) { 'foo/a.scss' }

context 'file should be preprocessed' do
let(:options) do
{ path: path,
preprocess_command: command,
preprocess_files: ['foo/*.scss'] }
end

it 'preprocesses, and Sass is able to parse' do
open3 = class_double('Open3').as_stubbed_const
open3.should_receive(:capture2).with(command, stdin_data: scss).and_return([processed, 0])
File.should_receive(:read).with(path).and_return(scss)

variable = engine.tree.children[0]
expect(variable).to be_instance_of(Sass::Tree::VariableNode)
expect(variable.name).to eq('red')
end
end

context 'file should not be preprocessed' do
let(:options) do
{ path: path,
preprocess_command: command,
preprocess_files: ['bar/*.scss'] }
end

it 'does not preprocess, and Sass throws' do
File.should_receive(:read).with(path).and_return(scss)
expect { engine }.to raise_error(Sass::SyntaxError)
end
end

context 'code should never be preprocessed' do
let(:options) do
{ code: scss,
preprocess_command: command,
preprocess_files: ['foo/*.scss'] }
end

it 'does not preprocess, and Sass throws' do
expect { engine }.to raise_error(Sass::SyntaxError)
end
end
end
end

0 comments on commit f3d0f94

Please sign in to comment.