diff --git a/.gitlab/ci/preflight.gitlab-ci.yml b/.gitlab/ci/preflight.gitlab-ci.yml index 426d36b24f4a6fd6505e4be75d4446797b2702c5..cd1a9c753cb4ed6212704f70f01b4f217a969c48 100644 --- a/.gitlab/ci/preflight.gitlab-ci.yml +++ b/.gitlab/ci/preflight.gitlab-ci.yml @@ -53,6 +53,18 @@ rails-production-server-boot-puma-cng: - retry_times_sleep 10 5 "curl http://127.0.0.1:8080" - kill $(jobs -p) +ruby_syntax: + extends: + - .preflight-job-base + before_script: + - source scripts/utils.sh + image: ${GITLAB_DEPENDENCY_PROXY_ADDRESS}ruby:${RUBY_VERSION} + parallel: + matrix: + - RUBY_VERSION: ["3.0", "3.1", "3.2"] + script: + - run_timed_command "fail_on_warnings scripts/lint/check_ruby_syntax.rb" + no-ee-check: extends: - .preflight-job-base diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 646021841b438ace18d68d756b2ae17508d63b7d..65b75c3e237a4eb27d8c03e16eba62b0a30153fc 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -2939,6 +2939,11 @@ - <<: *if-default-refs changes: *code-patterns +.preflight:rules:ruby_syntax: + rules: + - <<: *if-default-refs + changes: *backend-patterns + .preflight:rules:no-ee-check: rules: - <<: *if-not-foss diff --git a/ee/spec/lib/audit_events/strategies/group_external_destination_strategy_spec.rb b/ee/spec/lib/audit_events/strategies/group_external_destination_strategy_spec.rb index cf65b1a0330a1dcfeece5bfa576609b295f64cea..15bfe493864418cbae36a12a7cb23f05e685c8b4 100644 --- a/ee/spec/lib/audit_events/strategies/group_external_destination_strategy_spec.rb +++ b/ee/spec/lib/audit_events/strategies/group_external_destination_strategy_spec.rb @@ -98,12 +98,9 @@ it 'sends the headers with the payload' do create_list(:audit_events_streaming_header, 2, external_audit_event_destination: destination) - # rubocop:disable Lint/DuplicateHashKey expected_hash = { - /key-\d/ => "bar", /key-\d/ => "bar" } - # rubocop:enable Lint/DuplicateHashKey expect(Gitlab::HTTP).to receive(:post).with( an_instance_of(String), a_hash_including(headers: a_hash_including(expected_hash)) diff --git a/ee/spec/lib/audit_events/strategies/instance_external_destination_strategy_spec.rb b/ee/spec/lib/audit_events/strategies/instance_external_destination_strategy_spec.rb index 105f562912e2a12ff2493ce2adb455218e289d60..fa1d11106d887134fccdbfdc00268f611b67b502 100644 --- a/ee/spec/lib/audit_events/strategies/instance_external_destination_strategy_spec.rb +++ b/ee/spec/lib/audit_events/strategies/instance_external_destination_strategy_spec.rb @@ -93,12 +93,9 @@ instance_external_audit_event_destination: destination ) - # rubocop:disable Lint/DuplicateHashKey expected_hash = { - /key-\d/ => "bar", /key-\d/ => "bar" } - # rubocop:enable Lint/DuplicateHashKey expect(Gitlab::HTTP).to receive(:post).with( an_instance_of(String), a_hash_including(headers: a_hash_including(expected_hash)) diff --git a/ee/spec/workers/audit_events/audit_event_streaming_worker_spec.rb b/ee/spec/workers/audit_events/audit_event_streaming_worker_spec.rb index c2658e79b4dd8cfec4dbd6afa644babf79f7295f..abae3c5324c9094526fdc277394e525bf5171f5e 100644 --- a/ee/spec/workers/audit_events/audit_event_streaming_worker_spec.rb +++ b/ee/spec/workers/audit_events/audit_event_streaming_worker_spec.rb @@ -113,12 +113,9 @@ it 'sends the headers with the payload' do create_list(:audit_events_streaming_header, 2, external_audit_event_destination: group.external_audit_event_destinations.last) - # rubocop:disable Lint/DuplicateHashKey expected_hash = { - /key-\d/ => "bar", /key-\d/ => "bar" } - # rubocop:enable Lint/DuplicateHashKey expect(Gitlab::HTTP).to receive(:post).with(an_instance_of(String), a_hash_including(headers: a_hash_including(expected_hash))).once diff --git a/scripts/lint/check_ruby_syntax.rb b/scripts/lint/check_ruby_syntax.rb new file mode 100755 index 0000000000000000000000000000000000000000..e2cb84a625f7b7b81619098b0d174b8621495b4a --- /dev/null +++ b/scripts/lint/check_ruby_syntax.rb @@ -0,0 +1,25 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative "../../tooling/lib/tooling/check_ruby_syntax" + +files = `git ls-files -z`.split("\0") + +checker = Tooling::CheckRubySyntax.new(files) + +puts format("Checking %{files} Ruby files...", files: checker.ruby_files.size) + +errors = checker.run + +puts + +if errors.any? + puts "Syntax errors found (#{errors.size}):" + puts errors + + exit 1 +else + puts "No syntax errors found." + + exit 0 +end diff --git a/spec/tooling/lib/tooling/check_ruby_syntax_spec.rb b/spec/tooling/lib/tooling/check_ruby_syntax_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..cc1bc8e6223024e47e040c37d989b643214ad800 --- /dev/null +++ b/spec/tooling/lib/tooling/check_ruby_syntax_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require "fast_spec_helper" +require "fileutils" +require "rspec-parameterized" + +require_relative "../../../../tooling/lib/tooling/check_ruby_syntax" + +RSpec.describe Tooling::CheckRubySyntax, feature_category: :tooling do + let(:files) { Dir.glob("**/*") } + + subject(:checker) { described_class.new(files) } + + around do |example| + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + example.run + end + end + end + + describe "#ruby_files" do + subject { checker.ruby_files } + + context "without files" do + it { is_expected.to eq([]) } + end + + context "with files ending with .rb" do + before do + FileUtils.touch("foo.rb") + FileUtils.touch("bar.rb") + FileUtils.touch("baz.erb") + end + + it { is_expected.to contain_exactly("foo.rb", "bar.rb") } + end + + context "with special Ruby files" do + let(:files) do + %w[foo/Guardfile danger/Dangerfile gems/Gemfile Rakefile] + end + + before do + files.each do |file| + FileUtils.mkdir_p(File.dirname(file)) + FileUtils.touch(file) + end + end + + it { is_expected.to match_array(files) } + end + end + + describe "#run" do + subject(:errors) { checker.run } + + shared_examples "no errors" do + it { is_expected.to be_empty } + end + + context "without files" do + include_examples "no errors" + end + + context "with perfect Ruby code" do + before do + File.write("perfect.rb", "perfect = code") + end + + include_examples "no errors" + end + + context "with invalid Ruby code" do + before do + File.write("invalid.rb", "invalid,") + end + + it "has errors" do + expect(errors).to include(a_kind_of(SyntaxError)) + end + end + end +end diff --git a/tooling/lib/tooling/check_ruby_syntax.rb b/tooling/lib/tooling/check_ruby_syntax.rb new file mode 100644 index 0000000000000000000000000000000000000000..f769ca0199138b16635b9a5d0a53bf87fa949707 --- /dev/null +++ b/tooling/lib/tooling/check_ruby_syntax.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'set' # rubocop:disable Lint/RedundantRequireStatement -- Ruby 3.1 and earlier needs this. Drop this line after Ruby 3.2+ is only supported. + +module Tooling + # Checks passed files for valid Ruby syntax. + # + # It does not check for compile time warnings yet. See https://gitlab.com/-/snippets/1929968 + class CheckRubySyntax + VALID_RUBYFILES = %w[Rakefile Dangerfile Gemfile Guardfile].to_set.freeze + + attr_reader :files + + def initialize(files) + @files = files + end + + def ruby_files + @ruby_files ||= + @files.select do |file| + file.end_with?(".rb") || VALID_RUBYFILES.include?(File.basename(file)) + end + end + + def run + ruby_files.filter_map do |file| + check_ruby(file) + end + end + + private + + def check_ruby(file) + RubyVM::InstructionSequence.compile(File.open(file), file) + + nil + rescue SyntaxError => e + e + end + end +end