diff --git a/config/default.yml b/config/default.yml index 34cb6cd6..128398dc 100644 --- a/config/default.yml +++ b/config/default.yml @@ -499,6 +499,15 @@ PreCommit: flags: ['--standard=PSR2', '--report=csv'] include: '**/*.php' + PhpCsFixer: + enabled: false + description: 'Fix non compliant PHP files' + required_executable: 'php-cs-fixer' + command: 'vendor/bin/php-cs-fixer' + flags: ['fix', '-v', '--path-mode=intersection'] + install_command: 'composer global require friendsofphp/php-cs-fixer' + include: '**/*.php' + PhpStan: description: 'Analyze with phpstan' enabled: false diff --git a/lib/overcommit/hook/pre_commit/php_cs_fixer.rb b/lib/overcommit/hook/pre_commit/php_cs_fixer.rb new file mode 100644 index 00000000..a06caf09 --- /dev/null +++ b/lib/overcommit/hook/pre_commit/php_cs_fixer.rb @@ -0,0 +1,55 @@ +module Overcommit::Hook::PreCommit + # Runs `php-cs-fixer` against any modified PHP files. + class PhpCsFixer < Base + MESSAGE_REGEX = /\s+\d+\)\s+(?.*\.php)(?\s+\(\w+(?:,\s+)?\))?/ + + def run + messages = [] + feedback = '' + + # Exit status for all of the runs. Should be zero! + exit_status_sum = 0 + + applicable_files.each do |file| + result = execute(command, args: [file]) + output = result.stdout.chomp + exit_status_sum += result.status + + if result.status + messages = output.lstrip.split("\n") + end + end + + unless messages.empty? + feedback = parse_messages(messages) + end + + :pass if exit_status_sum == 0 + :pass if feedback.empty? + + feedback + end + + def parse_messages(messages) + output = [] + + messages.map do |message| + message.scan(MESSAGE_REGEX).map do |file, violated_rules| + type = :error + unless violated_rules.nil? + type = :warning + end + text = if type == :error + "Cannot process #{file}: Syntax error" + else + "#{file} has been fixed" + end + + output << Overcommit::Hook::Message.new(type, file, 0, text) + end + end + + output + end + end +end diff --git a/spec/overcommit/hook/pre_commit/phpcs_fixer_spec.rb b/spec/overcommit/hook/pre_commit/phpcs_fixer_spec.rb new file mode 100644 index 00000000..86fe8bc7 --- /dev/null +++ b/spec/overcommit/hook/pre_commit/phpcs_fixer_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +describe Overcommit::Hook::PreCommit::PhpCsFixer do + let(:config) { Overcommit::ConfigurationLoader.default_configuration } + let(:context) { double('context') } + subject { described_class.new(config, context) } + + before do + subject.stub(:applicable_files).and_return(%w[sample.php]) + end + + context 'when phpcs fixer exits successfully with fixed file' do + before do + # rubocop:disable Metrics/LineLength + sample_output = [ + 'Loaded config default.', + 'Using cache file ".php_cs.cache".', + 'F', + 'Legend: ?-unknown, I-invalid file syntax, file ignored, S-Skipped, .-no changes, F-fixed, E-error', + ' 1) foo/fixable.php (braces)', + '', + 'Fixed all files in 0.001 seconds, 10.000 MB memory used', + '', + ].join("\n") + # rubocop:enable Metrics/LineLength + + result = double('result') + result.stub(:status).and_return(0) + result.stub(:success?).and_return(true) + result.stub(:stdout).and_return(sample_output) + subject.stub(:execute).and_return(result) + end + + it { should warn } + end + + context 'when phpcs fixer exits successfully with no file to fix' do + before do + # rubocop:disable Metrics/LineLength + sample_output = [ + 'Loaded config default.', + 'Using cache file ".php_cs.cache".', + 'S', + 'Legend: ?-unknown, I-invalid file syntax, file ignored, S-Skipped, .-no changes, F-fixed, E-error', + '', + ].join("\n") + # rubocop:enable Metrics/LineLength + + result = double('result') + result.stub(:status).and_return(0) + result.stub(:success?).and_return(true) + result.stub(:stdout).and_return(sample_output) + subject.stub(:execute).and_return(result) + end + + it { should pass } + end + + context 'when phpcs exits unsuccessfully' do + before do + # rubocop:disable Metrics/LineLength + sample_output = [ + 'Loaded config default.', + 'Using cache file ".php_cs.cache".', + 'I', + 'Legend: ?-unknown, I-invalid file syntax, file ignored, S-Skipped, .-no changes, F-fixed, E-error', + 'Fixed all files in 0.001 seconds, 10.000 MB memory used', + '', + 'Files that were not fixed due to errors reported during linting before fixing:', + ' 1) /home/damien/Code/Rezdy/php/foo/broken.php', + '', + ].join("\n") + # rubocop:enable Metrics/LineLength + + result = double('result') + result.stub(:status).and_return(1) + result.stub(:success?).and_return(false) + result.stub(:stdout).and_return(sample_output) + result.stub(:stderr).and_return(sample_output) + subject.stub(:execute).and_return(result) + end + it { should fail_hook } + end +end