├── .github └── workflows │ └── build.yml ├── .gitignore ├── .rspec ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── LICENCE ├── README.rdoc ├── Rakefile ├── bin └── magic_frozen_string_literal ├── lib └── add_magic_comment.rb ├── magic_frozen_string_literal.gemspec └── spec ├── acceptance_spec.rb ├── fixtures ├── expected │ ├── Gemfile │ ├── Rakefile │ ├── blank_line.rb │ ├── config.ru │ ├── empty.rb │ ├── only_comment.rb │ ├── shebang.rb │ ├── shebang_blank_line.rb │ ├── t.Rakefile │ ├── t1.erb │ ├── t1.haml │ ├── t1.rb │ ├── t1.slim │ └── utf8.rb └── input │ ├── Gemfile │ ├── Rakefile │ ├── blank_line.rb │ ├── config.ru │ ├── empty.rb │ ├── only_comment.rb │ ├── shebang.rb │ ├── shebang_blank_line.rb │ ├── t.Rakefile │ ├── t1.erb │ ├── t1.haml │ ├── t1.rb │ ├── t1.slim │ └── utf8.rb ├── newline_code_spec.rb └── spec_helper.rb /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test 3 | on: [push, pull_request] 4 | jobs: 5 | test: 6 | name: Unit Tests 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | ruby: [2.5, 2.6, 2.7, '3.0', 3.1] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: ruby/setup-ruby@v1 15 | with: 16 | ruby-version: ${{ matrix.ruby }} 17 | bundler: 2.2.29 18 | bundler-cache: true 19 | - run: bundle exec rspec 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | /test-output/ 16 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.5 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | Encoding.default_external = Encoding::UTF_8 6 | Encoding.default_internal = Encoding::UTF_8 7 | 8 | # Specify your gem's dependencies in magic_frozen_string_literal 9 | gemspec 10 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | magic_frozen_string_literal (1.2.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | diff-lcs (1.4.4) 10 | rake (10.5.0) 11 | rspec (3.10.0) 12 | rspec-core (~> 3.10.0) 13 | rspec-expectations (~> 3.10.0) 14 | rspec-mocks (~> 3.10.0) 15 | rspec-core (3.10.0) 16 | rspec-support (~> 3.10.0) 17 | rspec-expectations (3.10.0) 18 | diff-lcs (>= 1.2.0, < 2.0) 19 | rspec-support (~> 3.10.0) 20 | rspec-mocks (3.10.0) 21 | diff-lcs (>= 1.2.0, < 2.0) 22 | rspec-support (~> 3.10.0) 23 | rspec-support (3.10.0) 24 | 25 | PLATFORMS 26 | ruby 27 | 28 | DEPENDENCIES 29 | bundler (~> 1.7) 30 | magic_frozen_string_literal! 31 | rake 32 | rspec 33 | 34 | BUNDLED WITH 35 | 2.2.29 36 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Manuel Ryan 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. -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Magic_frozen_string_literal 2 | 3 | Magic_frozen_string_literal is a little tool that allows you to quickly 4 | add the magic comment that indicates that the file 5 | can safely have its string literals frozen, as will be {the default in Ruby 3.0}[https://bugs.ruby-lang.org/issues/8976]. 6 | 7 | Cloned from https://github.com/m-ryan/magic_encoding 8 | 9 | == Installation 10 | 11 | gem install magic_frozen_string_literal 12 | rbenv rehash 13 | 14 | == Usage 15 | 16 | Run the tool from the command line: 17 | 18 | magic_frozen_string_literal 19 | 20 | this will prepend every "*.rb", "*.ru", "Rakefile", "*.rake", "Gemfile", "*.gemspec", "*.rabl", "*.jbuilder", "*.haml", "*.slim" file in the given (recursively) 21 | with the following magic comment followed by a blank line: 22 | 23 | # frozen_string_literal: true 24 | 25 | (".haml" and ".slim" files will have a "-" prefix before the comment and no blank line after.) 26 | 27 | The parameter is optional. It defaults to the current working directory. 28 | 29 | 30 | Notes: 31 | - existing +frozen_string_literal+ magic comments are replaced 32 | - the rest of the file remains unchanged 33 | - empty files are not touched 34 | - if the file starts with #! (shebang), that line is preserved and the magic comment is added just below it 35 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "shellwords" 5 | 6 | task default: :test 7 | 8 | task :test do 9 | system("rm -rf ./test-output") 10 | system("cp -r ./test-input ./test-output") 11 | system("ruby -Ilib/ ./bin/magic_frozen_string_literal ./test-output/") 12 | 13 | success = true 14 | 15 | count = 0 16 | successes = 0 17 | Dir.glob("test-expected/*").each do |filename| 18 | count += 1 19 | escaped_args = [filename, "./test-output/#{File.basename(filename)}"].map { |word| Shellwords.shellescape(word) } 20 | if system("diff -c #{escaped_args.join(' ')}") 21 | successes += 1 22 | end 23 | end 24 | 25 | puts "#{successes} of #{count} tests passed" 26 | if successes == count 27 | system("rm -rf ./test-output") 28 | exit 0 29 | else 30 | exit 1 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /bin/magic_frozen_string_literal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # A simple tool to prepend magic '# frozen_string_literal: true' comments to multiple ".rb" files 4 | 5 | require_relative '../lib/add_magic_comment' 6 | 7 | AddMagicComment.process(ARGV) 8 | -------------------------------------------------------------------------------- /lib/add_magic_comment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # A simple library to prepend magic comments to all Ruby files in a given folder 4 | 5 | module AddMagicComment 6 | MAGIC_COMMENT_PREFIX = "frozen_string_literal".freeze 7 | MAGIC_COMMENT_PATTERN = /^(-|(<%))?#\s*#{MAGIC_COMMENT_PREFIX}\s*(%>)?/ 8 | MAGIC_COMMENT = "#{MAGIC_COMMENT_PREFIX}: true".freeze 9 | EMPTY_LINE_PATTERN = /^\s$/ 10 | SHEBANG_PATTERN = /^#!/ 11 | 12 | EXTENSION_COMMENTS = { 13 | "*.rb" => "# #{MAGIC_COMMENT}\n\n", 14 | "*.ru" => "# #{MAGIC_COMMENT}\n\n", 15 | "Rakefile" => "# #{MAGIC_COMMENT}\n\n", 16 | "*.rake" => "# #{MAGIC_COMMENT}\n\n", 17 | "Gemfile" => "# #{MAGIC_COMMENT}\n\n", 18 | "*.gemspec" => "# #{MAGIC_COMMENT}\n\n", 19 | "*.rabl" => "# #{MAGIC_COMMENT}\n\n", 20 | "*.jbuilder" => "# #{MAGIC_COMMENT}\n\n", 21 | "*.haml" => "-# #{MAGIC_COMMENT}\n", 22 | "*.slim" => "-# #{MAGIC_COMMENT}\n" 23 | } 24 | 25 | def self.process(argv) 26 | directory = argv.first || Dir.pwd 27 | 28 | count = 0 29 | EXTENSION_COMMENTS.each do |pattern, comment| 30 | filename_pattern = File.join(directory, "**", "#{pattern}") 31 | Dir.glob(filename_pattern).each do |filename| 32 | File.open(filename, "rb+") do |file| 33 | lines = file.readlines 34 | newline = detect_newline(lines.first) 35 | next unless lines.any? 36 | count += 1 37 | 38 | if lines.first =~ SHEBANG_PATTERN 39 | shebang = lines.shift 40 | end 41 | 42 | # remove current magic comment(s) 43 | while lines.first && (lines.first.match(MAGIC_COMMENT_PATTERN) || lines.first.match(EMPTY_LINE_PATTERN)) 44 | lines.shift 45 | end 46 | 47 | # add magic comment as the first line 48 | lines.unshift(comment.gsub("\n", newline)) 49 | 50 | # put shebang back 51 | if shebang 52 | lines.unshift(shebang) 53 | end 54 | 55 | file.pos = 0 56 | file.print(*lines) 57 | file.truncate(file.pos) 58 | end 59 | end 60 | end 61 | 62 | puts "Magic comments added to #{count} source file(s)" 63 | end 64 | 65 | def self.detect_newline(line) 66 | (line[/\R/] if line) || $/ 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /magic_frozen_string_literal.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "magic_frozen_string_literal" 5 | spec.version = "1.2.0" 6 | spec.authors = ["Colin Kelley after Jared Roesch after Manuel Ryan"] 7 | spec.email = ["colin@invoca.com"] 8 | spec.summary = "Easily add magic comments '# frozen_string_literal: true' followed by a blank line to multiple Ruby source files" 9 | 10 | spec.homepage = "https://github.com/Invoca/magic_frozen_string_literal" 11 | spec.license = "MIT" 12 | 13 | spec.metadata = { 14 | "allowed_push_host" => "https://rubygems.org" 15 | } 16 | 17 | spec.files = Dir.glob("{bin,lib}/**/*") + %w[README.rdoc LICENCE] 18 | spec.executables = ["magic_frozen_string_literal"] 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.7" 22 | spec.add_development_dependency "rake" 23 | spec.add_development_dependency "rspec" 24 | end 25 | -------------------------------------------------------------------------------- /spec/acceptance_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/add_magic_comment.rb' 4 | 5 | RSpec.describe "acceptance" do 6 | before { setup_test_files } 7 | after { teardown_test_files } 8 | 9 | let(:directories) do 10 | { 11 | test: File.expand_path('../tmp/test', __dir__), 12 | input: File.expand_path('fixtures/input', __dir__), 13 | expected: File.expand_path('fixtures/expected', __dir__), 14 | } 15 | end 16 | 17 | it "inserts magic comments where expected", :aggregate_failures do 18 | AddMagicComment.process([directories[:test]]) 19 | 20 | Dir["#{directories[:test]}/*"].each do |path| 21 | assert_file_matches_expected(File.basename(path)) 22 | end 23 | end 24 | 25 | def setup_test_files 26 | FileUtils.mkdir_p(File.dirname(directories[:test])) 27 | FileUtils.cp_r(directories[:input], directories[:test]) 28 | end 29 | 30 | def teardown_test_files 31 | FileUtils.rm_rf(directories[:test]) 32 | end 33 | 34 | def assert_file_matches_expected(filename) 35 | paths = { 36 | test: File.join(directories[:test], filename), 37 | expected: File.join(directories[:expected], filename), 38 | } 39 | expect(FileUtils).to be_identical(*paths.values), <<~ERROR 40 | Expected contents of #{paths[:test]} to match #{paths[:expected]}. 41 | 42 | Expected: 43 | #{File.read(paths[:expected])} 44 | 45 | Actual: 46 | #{File.read(paths[:test])} 47 | ERROR 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/fixtures/expected/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'magic_frozen_string_literal' 6 | -------------------------------------------------------------------------------- /spec/fixtures/expected/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | 5 | task :test do 6 | puts 'test here' 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/expected/blank_line.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | puts "hello" 4 | -------------------------------------------------------------------------------- /spec/fixtures/expected/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require_relative 'config/environment' 6 | 7 | run Rails.application 8 | -------------------------------------------------------------------------------- /spec/fixtures/expected/empty.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Invoca/magic_frozen_string_literal/6ef8caf704cbbe13c62393fec0ce14d07ecfb086/spec/fixtures/expected/empty.rb -------------------------------------------------------------------------------- /spec/fixtures/expected/only_comment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | -------------------------------------------------------------------------------- /spec/fixtures/expected/shebang.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # frozen_string_literal: true 3 | 4 | puts "hello" 5 | -------------------------------------------------------------------------------- /spec/fixtures/expected/shebang_blank_line.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # frozen_string_literal: true 3 | 4 | puts "hello" 5 | -------------------------------------------------------------------------------- /spec/fixtures/expected/t.Rakefile: -------------------------------------------------------------------------------- 1 | puts 'not a Rakefile!' 2 | -------------------------------------------------------------------------------- /spec/fixtures/expected/t1.erb: -------------------------------------------------------------------------------- 1 | Hello! 2 | -------------------------------------------------------------------------------- /spec/fixtures/expected/t1.haml: -------------------------------------------------------------------------------- 1 | -# frozen_string_literal: true 2 | Hello! 3 | -------------------------------------------------------------------------------- /spec/fixtures/expected/t1.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | puts "hello" 4 | -------------------------------------------------------------------------------- /spec/fixtures/expected/t1.slim: -------------------------------------------------------------------------------- 1 | -# frozen_string_literal: true 2 | Hello! 3 | -------------------------------------------------------------------------------- /spec/fixtures/expected/utf8.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | puts " " 4 | -------------------------------------------------------------------------------- /spec/fixtures/input/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'magic_frozen_string_literal' 6 | -------------------------------------------------------------------------------- /spec/fixtures/input/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | 5 | task :test do 6 | puts 'test here' 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/input/blank_line.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | puts "hello" 4 | -------------------------------------------------------------------------------- /spec/fixtures/input/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require_relative 'config/environment' 6 | 7 | run Rails.application 8 | -------------------------------------------------------------------------------- /spec/fixtures/input/empty.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Invoca/magic_frozen_string_literal/6ef8caf704cbbe13c62393fec0ce14d07ecfb086/spec/fixtures/input/empty.rb -------------------------------------------------------------------------------- /spec/fixtures/input/only_comment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | -------------------------------------------------------------------------------- /spec/fixtures/input/shebang.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # frozen_string_literal: true 3 | 4 | puts "hello" 5 | -------------------------------------------------------------------------------- /spec/fixtures/input/shebang_blank_line.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # frozen_string_literal: true 3 | 4 | puts "hello" 5 | -------------------------------------------------------------------------------- /spec/fixtures/input/t.Rakefile: -------------------------------------------------------------------------------- 1 | puts 'not a Rakefile!' 2 | -------------------------------------------------------------------------------- /spec/fixtures/input/t1.erb: -------------------------------------------------------------------------------- 1 | Hello! 2 | -------------------------------------------------------------------------------- /spec/fixtures/input/t1.haml: -------------------------------------------------------------------------------- 1 | -# frozen_string_literal: true 2 | Hello! 3 | -------------------------------------------------------------------------------- /spec/fixtures/input/t1.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | puts "hello" 4 | -------------------------------------------------------------------------------- /spec/fixtures/input/t1.slim: -------------------------------------------------------------------------------- 1 | -# frozen_string_literal: true 2 | Hello! 3 | -------------------------------------------------------------------------------- /spec/fixtures/input/utf8.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | puts " " 4 | -------------------------------------------------------------------------------- /spec/newline_code_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/add_magic_comment.rb' 4 | 5 | RSpec.describe "newline code" do 6 | before { setup_test_files } 7 | after { teardown_test_files } 8 | 9 | let(:test_directory) do 10 | File.expand_path('../tmp/test', __dir__) 11 | end 12 | 13 | let(:newlines) do 14 | { lf: "\n", crlf: "\r\n", cr: "\r" } 15 | end 16 | 17 | let(:test_files) do 18 | newlines 19 | .keys 20 | .map { |newline_name| [newline_name, File.join(test_directory, "#{newline_name}.rb")] } 21 | .to_h 22 | end 23 | 24 | it "leaves newline code as it is", :aggregate_failures do 25 | AddMagicComment.process([test_directory]) 26 | newlines.each do |newline_name, newline| 27 | expected = ["# frozen_string_literal: true", "", "foo", "bar"].join(newline) 28 | actual = File.binread(test_files[newline_name]) 29 | expect(actual).to eq(expected), <<~ERROR 30 | Expected newline code to be leaved as it is but it was changed. 31 | 32 | Expected: 33 | #{expected.inspect} 34 | 35 | Actual: 36 | #{actual.inspect} 37 | ERROR 38 | end 39 | end 40 | 41 | def setup_test_files 42 | FileUtils.mkdir_p(test_directory) 43 | newlines.each do |newline_name, newline| 44 | setup_test_file(newline_name, newline) 45 | end 46 | end 47 | 48 | def setup_test_file(newline_name, newline) 49 | File.binwrite(test_files[newline_name], "foo#{newline}bar") 50 | end 51 | 52 | def teardown_test_files 53 | FileUtils.rm_rf(test_directory) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'fileutils' 4 | 5 | RSpec.configure do |config| 6 | config.expect_with :rspec do |expectations| 7 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 8 | end 9 | 10 | config.mock_with :rspec do |mocks| 11 | mocks.verify_partial_doubles = true 12 | end 13 | 14 | config.shared_context_metadata_behavior = :apply_to_host_groups 15 | config.disable_monkey_patching! 16 | end 17 | --------------------------------------------------------------------------------