diff --git a/README.md b/README.md index a0d46726..b57d6237 100644 --- a/README.md +++ b/README.md @@ -539,6 +539,8 @@ issue](https://github.com/sds/overcommit/issues/238) for more details. * [Mdl](lib/overcommit/hook/pre_commit/mdl.rb) * [`*`MergeConflicts](lib/overcommit/hook/pre_commit/merge_conflicts.rb) * [NginxTest](lib/overcommit/hook/pre_commit/nginx_test.rb) +* [OxFmt](lib/overcommit/hook/pre_commit/ox_fmt.rb) +* [OxLint](lib/overcommit/hook/pre_commit/ox_lint.rb) * [PhpCs](lib/overcommit/hook/pre_commit/php_cs.rb) * [PhpCsFixer](lib/overcommit/hook/pre_commit/php_cs_fixer.rb) * [PhpLint](lib/overcommit/hook/pre_commit/php_lint.rb) diff --git a/config/default.yml b/config/default.yml index c13055fd..21ab267f 100644 --- a/config/default.yml +++ b/config/default.yml @@ -565,6 +565,20 @@ PreCommit: flags: ['-t'] include: '**/nginx.conf' + OxFmt: + enabled: false + description: 'Analyze with oxfmt' + required_executable: 'oxfmt' + flags: ['--check'] + install_command: 'npm install -g oxfmt' + + OxLint: + enabled: false + description: 'Analyze with oxlint' + required_executable: 'oxlint' + flags: ['--format=unix'] + install_command: 'npm install -g oxlint' + Pep257: # Deprecated – use Pydocstyle instead. enabled: false description: 'Analyze docstrings with pep257' diff --git a/lib/overcommit/hook/pre_commit/ox_fmt.rb b/lib/overcommit/hook/pre_commit/ox_fmt.rb new file mode 100644 index 00000000..d3772376 --- /dev/null +++ b/lib/overcommit/hook/pre_commit/ox_fmt.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Overcommit::Hook::PreCommit + # Runs `oxfmt` against any modified files. + # + # Protip: if you have an npm script set up to run oxfmt, you can configure + # this hook to run oxfmt via your npm script by using the `command` option in + # your .overcommit.yml file. This can be useful if you have some oxfmt + # configuration built into your npm script that you don't want to repeat + # somewhere else. Example: + # + # oxfmt: + # required_executable: 'npm' + # enabled: true + # command: ['npm', 'run', 'fmt', '--', '--check'] + # + # Note: This hook only supports check mode. + # + # @see https://oxc.rs + class OxFmt < Base + def run + oxfmt_regex = /^(?.+) \(\d+ms\)/ + result = execute(command, args: applicable_files) + output = result.stdout.chomp + messages = output.split("\n").grep(oxfmt_regex) + + return [:fail, result.stderr] if messages.empty? && !result.success? + return :pass if result.success? && output.empty? + + # example message: + # test.js (5ms) + extract_messages(messages, oxfmt_regex) + end + end +end diff --git a/lib/overcommit/hook/pre_commit/ox_lint.rb b/lib/overcommit/hook/pre_commit/ox_lint.rb new file mode 100644 index 00000000..93f552f6 --- /dev/null +++ b/lib/overcommit/hook/pre_commit/ox_lint.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Overcommit::Hook::PreCommit + # Runs `oxlint` against any modified JavaScript files. + # + # Protip: if you have an npm script set up to run oxlint, you can configure + # this hook to run oxlint via your npm script by using the `command` option in + # your .overcommit.yml file. This can be useful if you have some oxlint + # configuration built into your npm script that you don't want to repeat + # somewhere else. Example: + # + # OxLint: + # required_executable: 'npm' + # enabled: true + # command: ['npm', 'run', 'lint', '--', '--format=unix'] + # + # Note: This hook supports only unix format. + # + # @see https://oxc.rs + class OxLint < Base + def run + oxlint_regex = %r{^(?:file://)?(?[^:]+):(?\d+):\d+:.*?(?Error|Warning)} + result = execute(command, args: applicable_files) + output = result.stdout.chomp + messages = output.split("\n").grep(oxlint_regex) + + return [:fail, result.stderr] if messages.empty? && !result.success? + return :pass if result.success? && output.empty? + + # example message: + # file://test.js:5:1: `debugger` statement is not allowed [Error/eslint(no-debugger)] + extract_messages(messages, oxlint_regex, lambda { |type| type.downcase.to_sym }) + end + end +end diff --git a/spec/overcommit/hook/pre_commit/ox_fmt_spec.rb b/spec/overcommit/hook/pre_commit/ox_fmt_spec.rb new file mode 100644 index 00000000..75c61876 --- /dev/null +++ b/spec/overcommit/hook/pre_commit/ox_fmt_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Overcommit::Hook::PreCommit::OxFmt 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[file1.js file2.js]) + end + + context 'when oxfmt is unable to run' do + let(:result) { double('result') } + + before do + result.stub(:stderr).and_return('SyntaxError: Use of const in strict mode.') + result.stub(:stdout).and_return('') + + result.stub(:success?).and_return(false) + subject.stub(:execute).and_return(result) + end + + it { should fail_hook } + end + + context 'when oxfmt exits successfully' do + let(:result) { double('result') } + + before do + result.stub(:success?).and_return(true) + subject.stub(:execute).and_return(result) + end + + context 'with no output' do + before do + result.stub(:stdout).and_return('') + end + + it { should pass } + end + + context 'and it reports an error' do + before do + result.stub(:stdout).and_return([ + 'Checking formatting...', + '', + 'README.md (66ms)', + '', + 'Format issues found in above 1 files. Run without `--check` to fix.', + 'Finished in 66ms on 1 files using 8 threads.' + ].join("\n")) + end + + it { should fail_hook } + end + + context 'and it doesnt count false positives error messages' do + before do + result.stub(:stdout).and_return([ + '$ yarn oxfmt --check /app/project/Error.ts', + '$ /app/project/node_modules/.bin/oxfmt --check /app/project/Error.ts', + '', + ].join("\n")) + end + + it { should pass } + end + end + + context 'when oxfmt exits unsucessfully' do + let(:result) { double('result') } + + before do + result.stub(:success?).and_return(false) + subject.stub(:execute).and_return(result) + end + + context 'and it reports an error' do + before do + result.stub(:stdout).and_return([ + 'Checking formatting...', + '', + 'README.md (66ms)', + '', + 'Format issues found in above 1 files. Run without `--check` to fix.', + 'Finished in 66ms on 1 files using 8 threads.' + ].join("\n")) + end + + it { should fail_hook } + end + end +end diff --git a/spec/overcommit/hook/pre_commit/ox_lint_spec.rb b/spec/overcommit/hook/pre_commit/ox_lint_spec.rb new file mode 100644 index 00000000..4a03225e --- /dev/null +++ b/spec/overcommit/hook/pre_commit/ox_lint_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Overcommit::Hook::PreCommit::OxLint 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[file1.js file2.js]) + end + + context 'when oxlint is unable to run' do + let(:result) { double('result') } + + before do + result.stub(:stderr).and_return('SyntaxError: Use of const in strict mode.') + result.stub(:stdout).and_return('') + + result.stub(:success?).and_return(false) + subject.stub(:execute).and_return(result) + end + + it { should fail_hook } + end + + context 'when oxlint exits successfully' do + let(:result) { double('result') } + + before do + result.stub(:success?).and_return(true) + subject.stub(:execute).and_return(result) + end + + context 'with no output' do + before do + result.stub(:stdout).and_return('') + end + + it { should pass } + end + + context 'and it reports a warning' do + before do + result.stub(:stdout).and_return([ + 'file://test.js:5:1: `debugger` statement is not allowed [Warning/eslint(no-debugger)]', + '', + '1 problem' + ].join("\n")) + end + + it { should warn } + end + + context 'and it doesnt count false positives error messages' do + before do + result.stub(:stdout).and_return([ + '$ yarn oxlint --quiet --format=unix /app/project/Error.ts', + '$ /app/project/node_modules/.bin/oxlint --quiet --format=compact /app/project/Error.ts', + '', + ].join("\n")) + end + + it { should pass } + end + end + + context 'when oxlint exits unsucessfully' do + let(:result) { double('result') } + + before do + result.stub(:success?).and_return(false) + subject.stub(:execute).and_return(result) + end + + context 'and it reports an error' do + before do + result.stub(:stdout).and_return([ + 'file://test.js:5:1: `debugger` statement is not allowed [Error/eslint(no-debugger)]', + '', + '1 problem' + ].join("\n")) + end + + it { should fail_hook } + end + end +end