CocoaPods/Xcodeproj

View on GitHub
lib/xcodeproj/project/object.rb

Summary

Maintainability
C
1 day
Test Coverage
A
97%
module Xcodeproj
  class Project
    # This is the namespace in which all the classes that wrap the objects in
    # a Xcode project reside.
    #
    # The base class from which all classes inherit is AbstractObject.
    #
    # If you need to deal with these classes directly, it's possible to include
    # this namespace into yours, making it unnecessary to prefix them with
    # Xcodeproj::Project::Object.
    #
    # @example
    #   class SourceFileSorter
    #     include Xcodeproj::Project::Object
    #   end
    #
    module Object
      # @abstract
      #
      # This is the base class of all object types that can exist in a Xcode
      # project. As such it provides common behavior, but you can only use
      # instances of subclasses of AbstractObject, because this class does
      # not exist in actual Xcode projects.
      #
      # Almost all the methods implemented by this class are not expected to be
      # used by {Xcodeproj} clients.
      #
      # Subclasses should clearly identify which methods reflect the xcodeproj
      # document model and which methods are offered as convenience. Object
      # lists always represent a relationship to many of the model while simple
      # arrays represent dynamically generated values offered a convenience for
      # clients.
      #
      class AbstractObject
        # @!group AbstractObject

        # @return [String] the ISA of the class.
        #
        def self.isa
          @isa ||= name.split('::').last
        end

        # @return [String] the object's class name.
        #
        attr_reader :isa

        # It is not recommended to instantiate objects through this
        # constructor. To create objects manually is easier to use
        # the {Project#new}. Otherwise, it is possible to use the convenience
        # methods offered by {Xcodeproj} which take care of configuring the
        # objects for common usage cases.
        #
        # @param [Project] project
        #   the project that will host the object.
        #
        # @param [String] uuid
        #   the UUID of the new object.
        #
        # @visibility private
        #
        def initialize(project, uuid)
          @project = project
          @uuid = uuid
          @isa = self.class.isa
          @referrers = []
          unless @isa =~ /^(PBX|XC)/
            raise "[Xcodeproj] Attempt to initialize an abstract class (#{self.class})."
          end
        end

        # Initializes the object with the default values of simple attributes.
        #
        # This method is called by the {Project#new} and is not performed on
        # initialization to prevent adding defaults to objects generated by a
        # plist.
        #
        # @return [void]
        #
        # @visibility private
        #
        def initialize_defaults
          simple_attributes.each { |a| a.set_default(self) }
        end

        # @return [String] the object universally unique identifier.
        #
        attr_reader :uuid

        # @return [Project] the project that owns the object.
        #
        attr_reader :project

        # Removes the object from the project by asking to its referrers to
        # remove the reference to it.
        #
        # @note The root object is owned by the project and should not be
        #       manipulated with this method.
        #
        # @return [void]
        #
        def remove_from_project
          mark_project_as_dirty!
          project.objects_by_uuid.delete(uuid)

          referrers.dup.each do |referrer|
            referrer.remove_reference(self)
          end

          to_one_attributes.each do |attrb|
            object = attrb.get_value(self)
            object.remove_referrer(self) if object
          end

          to_many_attributes.each do |attrb|
            list = attrb.get_value(self)
            list.clear
          end

          unless referrers.count == 0
            raise "[Xcodeproj] BUG: #{self} should have no referrers instead" \
              "the following objects are still referencing it #{referrers}"
          end
        end

        # Returns the value of the name attribute or returns a generic name for
        # the object.
        #
        # @note Not all concrete classes implement the name attribute and this
        #       method prevents from overriding it in plist.
        #
        # @return [String] a name for the object.
        #
        def display_name
          declared_name = name if self.respond_to?(:name)
          if declared_name && !declared_name.empty?
            declared_name
          else
            isa.gsub(/^(PBX|XC)/, '')
          end
        end
        alias_method :to_s, :display_name

        # Sorts the to many attributes of the object according to the display
        # name.
        #
        def sort(_options = nil)
          to_many_attributes.each do |attrb|
            list = attrb.get_value(self)
            list.sort! do |x, y|
              x.display_name.downcase <=> y.display_name.downcase
            end
          end
        end

        # Sorts the object and the objects that it references.
        #
        # @param  [Hash] options
        #         the sorting options.
        # @option options [Symbol] :groups_position
        #         the position of the groups can be either `:above` or
        #         `:below`.
        #
        # @note   Some objects may in turn refer back to objects higher in the
        #         object tree, which will lead to stack level deep errors.
        #         These objects should **not** try to perform a recursive sort,
        #         also because these objects would get sorted through other
        #         paths in the tree anyways.
        #
        #         At the time of writing the only known case is
        #         `PBXTargetDependency`.
        #
        def sort_recursively(options = nil)
          to_one_attributes.each do |attrb|
            value = attrb.get_value(self)
            value.sort_recursively(options) if value
          end

          to_many_attributes.each do |attrb|
            list = attrb.get_value(self)
            list.each { |entry| entry.sort_recursively(options) }
          end

          sort(options)
        end

        # @!group Reference counting

        # @return [Array<ObjectList>] The list of the objects that have a
        #   reference to this object.
        #
        # @visibility private
        #
        attr_reader :referrers

        # Informs the object that another object is referencing it. If the
        # object had no previous references it is added to the project UUIDs
        # hash.
        #
        # @return [void]
        #
        # @visibility private
        #
        def add_referrer(referrer)
          @referrers << referrer
          @project.objects_by_uuid[uuid] = self
        end

        # Informs the object that another object stopped referencing it. If the
        # object has no other references it is removed from the project UUIDs
        # hash because it is unreachable.
        #
        # @return [void]
        #
        # @visibility private
        #
        def remove_referrer(referrer)
          @referrers.delete(referrer)
          if @referrers.count == 0
            mark_project_as_dirty!
            @project.objects_by_uuid.delete(uuid)
          end
        end

        # Removes all the references to a given object.
        #
        # @return [void]
        #
        # @visibility private
        #
        def remove_reference(object)
          to_one_attributes.each do |attrb|
            value = attrb.get_value(self)
            attrb.set_value(self, nil) if value.equal?(object)
          end

          to_many_attributes.each do |attrb|
            list = attrb.get_value(self)
            list.delete(object)
          end

          references_by_keys_attributes.each do |attrb|
            list = attrb.get_value(self)
            list.each { |dictionary| dictionary.remove_reference(object) }
          end
        end

        # Marks the project that this object belongs to as having been modified.
        #
        # @return [void]
        #
        # @visibility private
        #
        def mark_project_as_dirty!
          project.mark_dirty!
        end

        #---------------------------------------------------------------------#

        public

        # @!group Plist related methods

        # Configures the object with the objects hash from a plist.
        #
        # **Implementation detail**: it is important that the attributes for a
        # given concrete class are unique because the value is removed from the
        # array at each iteration and duplicate would result in nil values.
        #
        # @return [void]
        #
        # @visibility private
        #
        def configure_with_plist(objects_by_uuid_plist)
          object_plist = objects_by_uuid_plist[uuid].dup

          unless object_plist['isa'] == isa
            raise "[Xcodeproj] Attempt to initialize `#{isa}` from plist with " \
              "different isa `#{object_plist}`"
          end
          object_plist.delete('isa')

          simple_attributes.each do |attrb|
            attrb.set_value(self, object_plist[attrb.plist_name])
            object_plist.delete(attrb.plist_name)
          end

          to_one_attributes.each do |attrb|
            ref_uuid = object_plist[attrb.plist_name]
            if ref_uuid
              ref = object_with_uuid(ref_uuid, objects_by_uuid_plist, attrb)
              attrb.set_value(self, ref) if ref
            end
            object_plist.delete(attrb.plist_name)
          end

          to_many_attributes.each do |attrb|
            ref_uuids = object_plist[attrb.plist_name] || []
            list = attrb.get_value(self)
            ref_uuids.each do |uuid|
              ref = object_with_uuid(uuid, objects_by_uuid_plist, attrb)
              list << ref if ref
            end
            object_plist.delete(attrb.plist_name)
          end

          references_by_keys_attributes.each do |attrb|
            hashes = object_plist[attrb.plist_name] || {}
            list = attrb.get_value(self)
            hashes.each do |hash|
              dictionary = ObjectDictionary.new(attrb, self)
              hash.each do |key, uuid|
                ref = object_with_uuid(uuid, objects_by_uuid_plist, attrb)
                dictionary[key] = ref if ref
              end
              list << dictionary
            end
            object_plist.delete(attrb.plist_name)
          end

          unless object_plist.empty?
            UI.warn "[!] Xcodeproj doesn't know about the following " \
                    "attributes #{object_plist.inspect} for the '#{isa}' isa." \
                    "\nIf this attribute was generated by Xcode please file " \
                    'an issue: https://github.com/CocoaPods/Xcodeproj/issues/new'
          end
        end

        # Initializes and returns the object with the given UUID.
        #
        # @param  [String] uuid
        #         The UUID of the object that should be initialized.
        #
        # @param  [Hash{String=>String}] objects_by_uuid_plist
        #         The hash contained by `objects` key of the plist containing
        #         the information about the object that should be initialized.
        #
        # @param  [AbstractObjectAttribute] attribute
        #         The attribute that requested the object. It is used only for
        #         exceptions.
        #
        # @raise  If the hash for the given UUID contains an unknown ISA.
        #
        # @return [AbstractObject] the initialized object.
        # @return [Nil] if the UUID could not be found in the objects hash. In
        #         this case a warning is printed to STDERR.
        #
        # @visibility private
        #
        def object_with_uuid(uuid, objects_by_uuid_plist, attribute)
          unless object = project.objects_by_uuid[uuid] || project.new_from_plist(uuid, objects_by_uuid_plist)
            UI.warn "`#{inspect}` attempted to initialize an object with " \
              "an unknown UUID. `#{uuid}` for attribute: " \
              "`#{attribute.name}`. This can be the result of a merge and " \
              'the unknown UUID is being discarded.'
          end
          object
        rescue NameError
          attributes = objects_by_uuid_plist[uuid]
          raise "`#{isa}` attempted to initialize an object with unknown ISA "\
                "`#{attributes['isa']}` from attributes: `#{attributes}`\n" \
                'If this ISA was generated by Xcode please file an issue: ' \
                'https://github.com/CocoaPods/Xcodeproj/issues/new'
        end

        # Returns a cascade representation of the object with UUIDs.
        #
        # @return [Hash] a hash representation of the project.
        #
        # @visibility public
        #
        # @note the key for simple and to_one attributes usually appears only
        #       if there is a value. To-many keys always appear with an empty
        #       array.
        #
        def to_hash
          to_hash_as
        end

        def to_hash_as(method = :to_hash)
          plist = {}
          plist['isa'] = isa

          simple_attributes.each do |attrb|
            value = attrb.get_value(self)
            plist[attrb.plist_name] = value if value
          end

          to_one_attributes.each do |attrb|
            obj = attrb.get_value(self)
            plist[attrb.plist_name] = nested_object_for_hash(obj, method) if obj
          end

          to_many_attributes.each do |attrb|
            list = attrb.get_value(self)
            plist[attrb.plist_name] = list.map { |o| nested_object_for_hash(o, method) }
          end

          references_by_keys_attributes.each do |attrb|
            list = attrb.get_value(self)
            plist[attrb.plist_name] = list.map(&method)
          end

          plist
        end
        private :to_hash_as

        def nested_object_for_hash(object, method)
          case method
          when :to_ascii_plist
            Nanaimo::String.new(object.uuid, object.ascii_plist_annotation)
          else
            object.uuid
          end
        end

        def ascii_plist_annotation
          " #{display_name} "
        end

        def to_ascii_plist
          Nanaimo::Dictionary.new(to_hash_as(:to_ascii_plist), ascii_plist_annotation)
        end

        # Returns a cascade representation of the object without UUIDs.
        #
        # This method is designed to work in conjunction with
        # {Hash#recursive_diff} to provide a complete, yet readable, diff of
        # two projects *not* affected by ISA differences.
        #
        # @todo   The current implementation might lead to infinite loops.
        #
        # @return [Hash] a hash representation of the project different from
        #         the plist one.
        #
        # @visibility private
        #
        def to_tree_hash
          hash = {}
          hash['displayName'] = display_name
          hash['isa'] = isa

          simple_attributes.each do |attrb|
            value = attrb.get_value(self)
            hash[attrb.plist_name] = value if value
          end

          to_one_attributes.each do |attrb|
            obj = attrb.get_value(self)
            hash[attrb.plist_name] = obj.to_tree_hash if obj
          end

          to_many_attributes.each do |attrb|
            list = attrb.get_value(self)
            hash[attrb.plist_name] = list.map(&:to_tree_hash)
          end

          references_by_keys_attributes.each do |attrb|
            list = attrb.get_value(self)
            hash[attrb.plist_name] = list.map(&:to_tree_hash)
          end

          hash
        end

        # @return [Hash{String => Hash}] A hash suitable to display the object
        #         to the user.
        #
        def pretty_print
          if to_many_attributes.count == 1
            children = to_many_attributes.first.get_value(self)
            { display_name => children.map(&:pretty_print) }
          else
            display_name
          end
        end

        #---------------------------------------------------------------------#

        public

        # @!group Object methods

        def ==(other)
          other.is_a?(AbstractObject) && to_hash == other.to_hash
        end

        def <=>(other)
          uuid <=> other.uuid
        end

        def inspect
          optional = ''
          optional << " name=`#{name}`" if respond_to?(:name) && name
          optional << " path=`#{path}`" if respond_to?(:path) && path
          "<#{isa}#{optional} UUID=`#{uuid}`>"
        end
      end
    end
  end
end

require 'xcodeproj/project/case_converter'
require 'xcodeproj/project/object_attributes'
require 'xcodeproj/project/object_dictionary'
require 'xcodeproj/project/object_list'

# Required because some classes have cyclical references to each other.
#
# @todo I'm sure that there is a method to achieve the same result which
# doesn't present the risk of some rubist laughing at me :-)
#
Xcodeproj::Constants::KNOWN_ISAS.each do |superclass_name, isas|
  superklass = Xcodeproj::Project::Object.const_get(superclass_name)
  isas.each do |isa|
    c = Class.new(superklass)
    Xcodeproj::Project::Object.const_set(isa, c)
  end
end

# Now load the concrete subclasses.
require 'xcodeproj/project/object/swift_package_remote_reference'
require 'xcodeproj/project/object/swift_package_local_reference'
require 'xcodeproj/project/object/swift_package_product_dependency'
require 'xcodeproj/project/object/build_configuration'
require 'xcodeproj/project/object/build_file'
require 'xcodeproj/project/object/build_phase'
require 'xcodeproj/project/object/build_rule'
require 'xcodeproj/project/object/configuration_list'
require 'xcodeproj/project/object/container_item_proxy'
require 'xcodeproj/project/object/file_reference'
require 'xcodeproj/project/object/group'
require 'xcodeproj/project/object/native_target'
require 'xcodeproj/project/object/root_object'
require 'xcodeproj/project/object/target_dependency'
require 'xcodeproj/project/object/reference_proxy'
require 'xcodeproj/project/object/file_system_synchronized_root_group'
require 'xcodeproj/project/object/file_system_synchronized_exception_set'