diff options
Diffstat (limited to 'lib/rubygems/specification_policy.rb')
-rw-r--r-- | lib/rubygems/specification_policy.rb | 198 |
1 files changed, 129 insertions, 69 deletions
diff --git a/lib/rubygems/specification_policy.rb b/lib/rubygems/specification_policy.rb index 8b5d01dda2..516c26f53c 100644 --- a/lib/rubygems/specification_policy.rb +++ b/lib/rubygems/specification_policy.rb @@ -1,22 +1,25 @@ -require_relative 'user_interaction' +# frozen_string_literal: true + +require_relative "user_interaction" class Gem::SpecificationPolicy include Gem::UserInteraction - VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/.freeze # :nodoc: + VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/ # :nodoc: - SPECIAL_CHARACTERS = /\A[#{Regexp.escape('.-_')}]+/.freeze # :nodoc: + SPECIAL_CHARACTERS = /\A[#{Regexp.escape(".-_")}]+/ # :nodoc: - VALID_URI_PATTERN = %r{\Ahttps?:\/\/([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z}.freeze # :nodoc: + VALID_URI_PATTERN = %r{\Ahttps?:\/\/([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z} # :nodoc: METADATA_LINK_KEYS = %w[ - bug_tracker_uri - changelog_uri - documentation_uri homepage_uri - mailing_list_uri + changelog_uri source_code_uri + documentation_uri wiki_uri + mailing_list_uri + bug_tracker_uri + download_uri funding_uri ].freeze # :nodoc: @@ -100,10 +103,14 @@ class Gem::SpecificationPolicy validate_dependencies + validate_required_ruby_version + validate_extensions validate_removed_attributes + validate_unique_links + if @warnings > 0 if strict error "specification has warnings" @@ -120,12 +127,12 @@ class Gem::SpecificationPolicy metadata = @specification.metadata unless Hash === metadata - error 'metadata must be a hash' + error "metadata must be a hash" end metadata.each do |key, value| entry = "metadata['#{key}']" - if !key.kind_of?(String) + unless key.is_a?(String) error "metadata keys must be a String" end @@ -133,7 +140,7 @@ class Gem::SpecificationPolicy error "metadata key is too large (#{key.size} > 128)" end - if !value.kind_of?(String) + unless value.is_a?(String) error "#{entry} value must be a String" end @@ -141,10 +148,9 @@ class Gem::SpecificationPolicy error "#{entry} value is too large (#{value.size} > 1024)" end - if METADATA_LINK_KEYS.include? key - if value !~ VALID_URI_PATTERN - error "#{entry} has invalid link: #{value.inspect}" - end + next unless METADATA_LINK_KEYS.include? key + unless VALID_URI_PATTERN.match?(value) + error "#{entry} has invalid link: #{value.inspect}" end end end @@ -161,7 +167,7 @@ class Gem::SpecificationPolicy if prev = seen[dep.type][dep.name] error_messages << <<-MESSAGE duplicate dependency on #{dep}, (#{prev.requirement}) use: - add_#{dep.type}_dependency '#{dep.name}', '#{dep.requirement}', '#{prev.requirement}' + add_#{dep.type}_dependency \"#{dep.name}\", \"#{dep.requirement}\", \"#{prev.requirement}\" MESSAGE end @@ -173,6 +179,7 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: end ## + # Checks that the gem does not depend on itself. # Checks that dependencies use requirements as we recommend. Warnings are # issued when dependencies are open-ended or overly strict for semantic # versioning. @@ -180,6 +187,10 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: def validate_dependencies # :nodoc: warning_messages = [] @specification.dependencies.each do |dep| + if dep.name == @specification.name # warn on self reference + warning_messages << "Self referencing dependency is unnecessary and strongly discouraged." + end + prerelease_dep = dep.requirements_list.any? do |req| Gem::Requirement.new(req).prerelease? end @@ -188,37 +199,42 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: prerelease_dep && !@specification.version.prerelease? open_ended = dep.requirement.requirements.all? do |op, version| - not version.prerelease? and (op == '>' or op == '>=') + !version.prerelease? && [">", ">="].include?(op) end - if open_ended - op, dep_version = dep.requirement.requirements.first + next unless open_ended + op, dep_version = dep.requirement.requirements.first - segments = dep_version.segments + segments = dep_version.segments - base = segments.first 2 + base = segments.first 2 - recommendation = if (op == '>' || op == '>=') && segments == [0] - " use a bounded requirement, such as '~> x.y'" - else - bugfix = if op == '>' - ", '> #{dep_version}'" - elsif op == '>=' and base != segments - ", '>= #{dep_version}'" - end - - " if #{dep.name} is semantically versioned, use:\n" \ - " add_#{dep.type}_dependency '#{dep.name}', '~> #{base.join '.'}'#{bugfix}" + recommendation = if [">", ">="].include?(op) && segments == [0] + " use a bounded requirement, such as \"~> x.y\"" + else + bugfix = if op == ">" + ", \"> #{dep_version}\"" + elsif op == ">=" && base != segments + ", \">= #{dep_version}\"" end - warning_messages << ["open-ended dependency on #{dep} is not recommended", recommendation].join("\n") + "\n" + " if #{dep.name} is semantically versioned, use:\n" \ + " add_#{dep.type}_dependency \"#{dep.name}\", \"~> #{base.join "."}\"#{bugfix}" end + + warning_messages << ["open-ended dependency on #{dep} is not recommended", recommendation].join("\n") + "\n" end if warning_messages.any? warning_messages.each {|warning_message| warning warning_message } end end + def validate_required_ruby_version + if @specification.required_ruby_version.requirements == [Gem::Requirement::DefaultRequirement] + warning "make sure you specify the oldest ruby version constraint (like \">= 3.0\") that you want your gem to support by setting the `required_ruby_version` gemspec attribute" + end + end + ## # Issues a warning for each file to be packaged which is world-readable. # @@ -229,7 +245,7 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: @specification.files.each do |file| next unless File.file?(file) - next if File.stat(file).mode & 0444 == 0444 + next if File.stat(file).mode & 0o444 == 0o444 warning "#{file} is not world-readable" end @@ -248,7 +264,7 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: @specification.instance_variable_get("@#{attrname}").nil? end return if nil_attributes.empty? - error "#{nil_attributes.join ', '} must not be nil" + error "#{nil_attributes.join ", "} must not be nil" end def validate_rubygems_version @@ -274,11 +290,11 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: if !name.is_a?(String) error "invalid value for attribute name: \"#{name.inspect}\" must be a string" - elsif name !~ /[a-zA-Z]/ + elsif !/[a-zA-Z]/.match?(name) error "invalid value for attribute name: #{name.dump} must include at least one letter" - elsif name !~ VALID_NAME_PATTERN + elsif !VALID_NAME_PATTERN.match?(name) error "invalid value for attribute name: #{name.dump} can only include letters, numbers, dashes, and underscores" - elsif name =~ SPECIAL_CHARACTERS + elsif SPECIAL_CHARACTERS.match?(name) error "invalid value for attribute name: #{name.dump} can not begin with a period, dash, or underscore" end end @@ -286,7 +302,7 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: def validate_require_paths return unless @specification.raw_require_paths.empty? - error 'specification must have at least one require_path' + error "specification must have at least one require_path" end def validate_non_files @@ -310,7 +326,7 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: def validate_specification_version return if @specification.specification_version.is_a?(Integer) - error 'specification_version must be an Integer (did you mean version?)' + error "specification_version must be an Integer (did you mean version?)" end def validate_platform @@ -332,13 +348,13 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: def validate_array_attribute(field) val = @specification.send(field) klass = case field - when :dependencies then - Gem::Dependency - else - String + when :dependencies then + Gem::Dependency + else + String end - unless Array === val and val.all? {|x| x.kind_of?(klass) } + unless Array === val && val.all? {|x| x.is_a?(klass) || (field == :licenses && x.nil?) } error "#{field} must be an Array of #{klass}" end end @@ -353,6 +369,8 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: licenses = @specification.licenses licenses.each do |license| + next if license.nil? + if license.length > 64 error "each license must be 64 characters or less" end @@ -363,26 +381,38 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: licenses = @specification.licenses licenses.each do |license| - if !Gem::Licenses.match?(license) - suggestions = Gem::Licenses.suggestions(license) - message = <<-WARNING -license value '#{license}' is invalid. Use a license identifier from -http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard license. - WARNING - message += "Did you mean #{suggestions.map {|s| "'#{s}'" }.join(', ')}?\n" unless suggestions.nil? - warning(message) + next if Gem::Licenses.match?(license) || license.nil? + license_id_deprecated = Gem::Licenses.deprecated_license_id?(license) + exception_id_deprecated = Gem::Licenses.deprecated_exception_id?(license) + suggestions = Gem::Licenses.suggestions(license) + + if license_id_deprecated + main_message = "License identifier '#{license}' is deprecated" + elsif exception_id_deprecated + main_message = "Exception identifier at '#{license}' is deprecated" + else + main_message = "License identifier '#{license}' is invalid" end + + message = <<-WARNING +#{main_message}. Use an identifier from +https://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard license, +or set it to nil if you don't want to specify a license. + WARNING + message += "Did you mean #{suggestions.map {|s| "'#{s}'" }.join(", ")}?\n" unless suggestions.nil? + warning(message) end warning <<-WARNING if licenses.empty? -licenses is empty, but is recommended. Use a license identifier from -http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard license. +licenses is empty, but is recommended. Use an license identifier from +https://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard license, +or set it to nil if you don't want to specify a license. WARNING end - LAZY = '"FIxxxXME" or "TOxxxDO"'.gsub(/xxx/, '') - LAZY_PATTERN = /\AFI XME|\ATO DO/x.freeze - HOMEPAGE_URI_PATTERN = /\A[a-z][a-z\d+.-]*:/i.freeze + LAZY = '"FIxxxXME" or "TOxxxDO"'.gsub(/xxx/, "") + LAZY_PATTERN = /\AFI XME|\ATO DO/x + HOMEPAGE_URI_PATTERN = /\A[a-z][a-z\d+.-]*:/i def validate_lazy_metadata unless @specification.authors.grep(LAZY_PATTERN).empty? @@ -393,25 +423,25 @@ http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard li error "#{LAZY} is not an email" end - if @specification.description =~ LAZY_PATTERN + if LAZY_PATTERN.match?(@specification.description) error "#{LAZY} is not a description" end - if @specification.summary =~ LAZY_PATTERN + if LAZY_PATTERN.match?(@specification.summary) error "#{LAZY} is not a summary" end homepage = @specification.homepage # Make sure a homepage is valid HTTP/HTTPS URI - if homepage and not homepage.empty? - require 'uri' + if homepage && !homepage.empty? + require_relative "vendor/uri/lib/uri" begin - homepage_uri = URI.parse(homepage) - unless [URI::HTTP, URI::HTTPS].member? homepage_uri.class + homepage_uri = Gem::URI.parse(homepage) + unless [Gem::URI::HTTP, Gem::URI::HTTPS].member? homepage_uri.class error "\"#{homepage}\" is not a valid HTTP URI" end - rescue URI::InvalidURIError + rescue Gem::URI::InvalidURIError error "\"#{homepage}\" is not a valid HTTP URI" end end @@ -445,7 +475,7 @@ http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard li def validate_shebang_line_in(executable) executable_path = File.join(@specification.bindir, executable) - return if File.read(executable_path, 2) == '#!' + return if File.read(executable_path, 2) == "#!" warning "#{executable_path} is missing #! line" end @@ -457,17 +487,47 @@ http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard li end def validate_extensions # :nodoc: - require_relative 'ext' + require_relative "ext" builder = Gem::Ext::Builder.new(@specification) + validate_rake_extensions(builder) + validate_rust_extensions(builder) + end + + def validate_rust_extensions(builder) # :nodoc: + rust_extension = @specification.extensions.any? {|s| builder.builder_for(s).is_a? Gem::Ext::CargoBuilder } + missing_cargo_lock = !@specification.files.any? {|f| f.end_with?("Cargo.lock") } + + error <<-ERROR if rust_extension && missing_cargo_lock +You have specified rust based extension, but Cargo.lock is not part of the gem files. Please run `cargo generate-lockfile` or any other command to generate Cargo.lock and ensure it is added to your gem files section in gemspec. + ERROR + end + + def validate_rake_extensions(builder) # :nodoc: rake_extension = @specification.extensions.any? {|s| builder.builder_for(s) == Gem::Ext::RakeBuilder } - rake_dependency = @specification.dependencies.any? {|d| d.name == 'rake' } + rake_dependency = @specification.dependencies.any? {|d| d.name == "rake" && d.type == :runtime } warning <<-WARNING if rake_extension && !rake_dependency -You have specified rake based extension, but rake is not added as dependency. It is recommended to add rake as a dependency in gemspec since there's no guarantee rake will be already installed. +You have specified rake based extension, but rake is not added as runtime dependency. It is recommended to add rake as a runtime dependency in gemspec since there's no guarantee rake will be already installed. WARNING end + def validate_unique_links + links = @specification.metadata.slice(*METADATA_LINK_KEYS) + grouped = links.group_by {|_key, uri| uri } + grouped.each do |uri, copies| + next unless copies.length > 1 + keys = copies.map(&:first).join("\n ") + warning <<~WARNING + You have specified the uri: + #{uri} + for all of the following keys: + #{keys} + Only the first one will be shown on rubygems.org + WARNING + end + end + def warning(statement) # :nodoc: @warnings += 1 |