DEV: stop mutating inputs as a side effect

We had quite a few cases in core where inputs are being mutated as a side
effect of calling a method.

This handles all the cases where specs caught this.

Mutating inputs makes code harder to reason about. Eg:

```
frog = "frog"
jump(frog)
puts frog
"fly" # ?????
```

This commit is part of a followup commit that adds # frozen_string_literal
to all our specs.
This commit is contained in:
Sam Saffron 2019-04-30 10:25:53 +10:00
parent 442111af91
commit 0a5a6dfded
10 changed files with 55 additions and 23 deletions

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require_dependency 'trust_level' require_dependency 'trust_level'
class AdminUserIndexQuery class AdminUserIndexQuery
@ -108,7 +110,7 @@ class AdminUserIndexQuery
filter = params[:filter] filter = params[:filter]
if filter.present? if filter.present?
filter.strip! filter = filter.strip
if ip = IPAddr.new(filter) rescue nil if ip = IPAddr.new(filter) rescue nil
@query.where('ip_address <<= :ip OR registration_ip_address <<= :ip', ip: ip.to_cidr_s) @query.where('ip_address <<= :ip OR registration_ip_address <<= :ip', ip: ip.to_cidr_s)
else else

View File

@ -111,8 +111,9 @@ module DiscourseTagging
term = opts[:term] term = opts[:term]
if term.present? if term.present?
term.gsub!("_", "\\_") term = term.gsub("_", "\\_")
term = clean_tag(term).downcase clean_tag(term)
term.downcase!
query = query.where('lower(tags.name) like ?', "%#{term}%") query = query.where('lower(tags.name) like ?', "%#{term}%")
end end

View File

@ -118,8 +118,13 @@ module Email
end end
def body def body
body = @opts[:body] body = nil
body = I18n.t("#{@opts[:template]}.text_body_template", template_args).dup if @opts[:template]
if @opts[:template]
body = I18n.t("#{@opts[:template]}.text_body_template", template_args).dup
else
body = @opts[:body].dup
end
if @template_args[:unsubscribe_instructions].present? if @template_args[:unsubscribe_instructions].present?
body << "\n" body << "\n"

View File

@ -382,7 +382,7 @@ module SiteSettingExtension
def filter_value(name, value) def filter_value(name, value)
if HOSTNAME_SETTINGS.include?(name) if HOSTNAME_SETTINGS.include?(name)
value.split("|").map { |url| get_hostname(url) }.compact.uniq.join("|") value.split("|").map { |url| url.strip!; get_hostname(url) }.compact.uniq.join("|")
else else
value value
end end
@ -488,7 +488,6 @@ module SiteSettingExtension
end end
def get_hostname(url) def get_hostname(url)
url.strip!
host = begin host = begin
URI.parse(url)&.host URI.parse(url)&.host

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# #
# Clean up a text # Clean up a text
# #
@ -27,6 +29,8 @@ class TextCleaner
end end
def self.clean(text, opts = {}) def self.clean(text, opts = {})
text = text.dup
# Remove invalid byte sequences # Remove invalid byte sequences
text.scrub!("") text.scrub!("")
# Replace !!!!! with a single ! # Replace !!!!! with a single !
@ -38,7 +42,7 @@ class TextCleaner
# Capitalize first letter, but only when entire first word is lowercase # Capitalize first letter, but only when entire first word is lowercase
first, rest = text.split(' ', 2) first, rest = text.split(' ', 2)
if first && opts[:capitalize_first_letter] && first == first.mb_chars.downcase if first && opts[:capitalize_first_letter] && first == first.mb_chars.downcase
text = "#{first.mb_chars.capitalize}#{rest ? ' ' + rest : ''}" text = +"#{first.mb_chars.capitalize}#{rest ? ' ' + rest : ''}"
end end
# Remove unnecessary periods at the end # Remove unnecessary periods at the end
text.sub!(/([^.])\.+(\s*)\z/, '\1\2') if opts[:remove_all_periods_from_the_end] text.sub!(/([^.])\.+(\s*)\z/, '\1\2') if opts[:remove_all_periods_from_the_end]

View File

@ -685,17 +685,26 @@ class TopicQuery
if SiteSetting.tagging_enabled if SiteSetting.tagging_enabled
result = result.preload(:tags) result = result.preload(:tags)
if @options[:tags] && @options[:tags].size > 0 tags = @options[:tags]
@options[:tags] = @options[:tags].split unless @options[:tags].respond_to?('each')
@options[:tags].each { |t| t.downcase! if t.is_a? String } if tags && tags.size > 0
tags = tags.split if String === tags
tags = tags.map do |t|
if String === t
t.downcase
else
t
end
end
if @options[:match_all_tags] if @options[:match_all_tags]
# ALL of the given tags: # ALL of the given tags:
tags_count = @options[:tags].length tags_count = tags.length
@options[:tags] = Tag.where_name(@options[:tags]).pluck(:id) unless @options[:tags][0].is_a?(Integer) tags = Tag.where_name(tags).pluck(:id) unless Integer === tags[0]
if tags_count == @options[:tags].length if tags_count == tags.length
@options[:tags].each_with_index do |tag, index| tags.each_with_index do |tag, index|
sql_alias = ['t', index].join sql_alias = ['t', index].join
result = result.joins("INNER JOIN topic_tags #{sql_alias} ON #{sql_alias}.topic_id = topics.id AND #{sql_alias}.tag_id = #{tag}") result = result.joins("INNER JOIN topic_tags #{sql_alias} ON #{sql_alias}.topic_id = topics.id AND #{sql_alias}.tag_id = #{tag}")
end end
@ -705,12 +714,17 @@ class TopicQuery
else else
# ANY of the given tags: # ANY of the given tags:
result = result.joins(:tags) result = result.joins(:tags)
if @options[:tags][0].is_a?(Integer) if Integer === tags[0]
result = result.where("tags.id in (?)", @options[:tags]) result = result.where("tags.id in (?)", tags)
else else
result = result.where("lower(tags.name) in (?)", @options[:tags]) result = result.where("lower(tags.name) in (?)", tags)
end end
end end
# TODO: this is very side-effecty and should be changed
# It is done cause further up we expect normalized tags
@options[:tags] = tags
elsif @options[:no_tags] elsif @options[:no_tags]
# the following will do: ("topics"."id" NOT IN (SELECT DISTINCT "topic_tags"."topic_id" FROM "topic_tags")) # the following will do: ("topics"."id" NOT IN (SELECT DISTINCT "topic_tags"."topic_id" FROM "topic_tags"))
result = result.where.not(id: TopicTag.distinct.pluck(:topic_id)) result = result.where.not(id: TopicTag.distinct.pluck(:topic_id))

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module UserNameSuggester module UserNameSuggester
GENERIC_NAMES = ['i', 'me', 'info', 'support', 'admin', 'webmaster', 'hello', 'mail', 'office', 'contact', 'team'] GENERIC_NAMES = ['i', 'me', 'info', 'support', 'admin', 'webmaster', 'hello', 'mail', 'office', 'contact', 'team']
@ -41,7 +43,7 @@ module UserNameSuggester
end end
def self.sanitize_username(name) def self.sanitize_username(name)
name = name.to_s name = name.to_s.dup
if SiteSetting.unicode_usernames if SiteSetting.unicode_usernames
name.unicode_normalize! name.unicode_normalize!

View File

@ -1,15 +1,16 @@
# frozen_string_literal: true
class ReplyByEmailAddressValidator class ReplyByEmailAddressValidator
def initialize(opts = {}) def initialize(opts = {})
@opts = opts @opts = opts
end end
def valid_value?(val) def valid_value?(val)
val&.strip!
return true if val.blank? return true if val.blank?
return false if !val.include?("@") return false if !val.include?("@")
value = val.dup value = val.dup
value.strip!
if SiteSetting.find_related_post_with_key if SiteSetting.find_related_post_with_key
return false if !value.include?("%{reply_key}") return false if !value.include?("%{reply_key}")

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class Wizard class Wizard
class StepUpdater class StepUpdater
include ActiveModel::Model include ActiveModel::Model
@ -29,7 +31,7 @@ class Wizard
end end
def update_setting(id, value) def update_setting(id, value)
value.strip! if value.is_a?(String) value = value.strip if value.is_a?(String)
if !value.is_a?(Upload) && SiteSetting.type_supervisor.get_type(id) == :upload if !value.is_a?(Upload) && SiteSetting.type_supervisor.get_type(id) == :upload
value = Upload.get_from_url(value) || '' value = Upload.get_from_url(value) || ''

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
require_dependency 'site_setting_extension' require_dependency 'site_setting_extension'
require_dependency 'site_settings/local_process_provider' require_dependency 'site_settings/local_process_provider'
@ -776,7 +778,7 @@ describe SiteSettingExtension do
describe "get_hostname" do describe "get_hostname" do
it "properly extracts the hostname" do it "properly extracts the hostname" do
expect(settings.send(:get_hostname, "discourse.org")).to eq("discourse.org") # consider testing this through a public interface, this tests implementation details
expect(settings.send(:get_hostname, "discourse.org")).to eq("discourse.org") expect(settings.send(:get_hostname, "discourse.org")).to eq("discourse.org")
expect(settings.send(:get_hostname, "@discourse.org")).to eq("discourse.org") expect(settings.send(:get_hostname, "@discourse.org")).to eq("discourse.org")
expect(settings.send(:get_hostname, "https://discourse.org")).to eq("discourse.org") expect(settings.send(:get_hostname, "https://discourse.org")).to eq("discourse.org")