# frozen_string_literal: true # # Copyright:: 2020-2022, Chef Software, Inc. # Author:: Tim Smith () # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # module RuboCop module Cop module Chef module RedundantCode # Use the `:create_if_missing` action instead of `not_if` with a `::File.exist(FOO)` check. # # @example # # #### incorrect # cookbook_file '/logs/foo/error.log' do # source 'error.log' # owner 'root' # group 'root' # mode '0644' # not_if { ::File.exists?('/logs/foo/error.log') } # end # # remote_file 'Download file' do # path '/foo/bar' # source 'https://foo.com/bar' # owner 'root' # group 'root' # mode '0644' # not_if { ::File.exist?('/foo/bar') } # end # # #### correct # cookbook_file '/logs/foo/error.log' do # source 'error.log' # owner 'root' # group 'root' # mode '0644' # action :create_if_missing # end # # remote_file 'Download file' do # path '/foo/bar' # source 'https://foo.com/bar' # owner 'root' # group 'root' # mode '0644' # action :create_if_missing # end # class UseCreateIfMissing < Base include RuboCop::Chef::CookbookHelpers extend AutoCorrector include RangeHelp MSG = 'Use the :create_if_missing action instead of not_if with a ::File.exist(FOO) check.' RESOURCES = %i(cookbook_file file remote_directory cron_d remote_file template).freeze def_node_matcher :file_exist_value, <<-PATTERN (send (const {nil? (cbase)} :File) {:exist? :exists?} $(...)) PATTERN def_node_search :has_action?, '(send nil? :action ...)' def_node_search :create_action, '(send nil? :action $sym)' def_node_search :path_property_node, '(send nil? :path $...)' def on_block(node) match_property_in_resource?(RESOURCES, :not_if, node) do |prop| # if it's not a block type then it's not a ruby block with a file.exist return unless prop.block_type? file_exist_value(prop.body) do |exists_content| # check the contents of the ruby block that's passed # not an offense if: # - The resource block name (the last arg of the send) doesn't match the exists check content # - If a path property is used it doesn't match the exists check content return unless exists_content == node.send_node.last_argument || exists_content == path_property_node(node)&.first&.first # we have an action so check if it is :create. If that's the case we can replace that value # and delete the not_if line. Otherwise it's an action like :remove and while the whole resource # no longer makes sense that's not our problem here. create_action(node) do |create_action| return unless create_action == s(:sym, :create) add_offense(prop, message: MSG, severity: :refactor) do |corrector| corrector.replace(create_action, ':create_if_missing') corrector.remove(range_by_whole_lines(prop.source_range, include_final_newline: true)) end return end # if we got this far we didn't return above when we had an action # so we can just replace the not_if line with a new :create_if_missing action add_offense(prop, message: MSG, severity: :refactor) do |corrector| corrector.replace(prop, 'action :create_if_missing') end end end end end end end end end