Property definitions

nitc $ ModelBuilder :: defaultinit
# A model builder knows how to load nit source files and build the associated model
class ModelBuilder
	# The model where new modules, classes and properties are added
	var model: Model

	# The toolcontext used to control the interaction with the user (getting options and displaying messages)
	var toolcontext: ToolContext

	# Instantiate a modelbuilder for a model and a toolcontext
	# Important, the options of the toolcontext must be correctly set (parse_option already called)
	init
	do
		assert toolcontext.modelbuilder_real == null
		toolcontext.modelbuilder_real = self
	end

	# Return a class named `name` visible by the module `mmodule`.
	# Visibility in modules is correctly handled.
	# If no such a class exists, then null is returned.
	# If more than one class exists, then an error on `anode` is displayed and null is returned.
	# FIXME: add a way to handle class name conflict
	fun try_get_mclass_by_name(anode: nullable ANode, mmodule: MModule, name: String): nullable MClass
	do
		var classes = model.get_mclasses_by_name(name)
		if classes == null then
			return null
		end

		var res: nullable MClass = null
		for mclass in classes do
			if not mmodule.in_importation <= mclass.intro_mmodule then continue
			if not mmodule.is_visible(mclass.intro_mmodule, mclass.visibility) then continue
			if res == null then
				res = mclass
			else
				error(anode, "Error: ambiguous class name `{name}`; conflict between `{mclass.full_name}` and `{res.full_name}`.")
				return null
			end
		end
		return res
	end

	# Return a class identified by `qid` visible by the module `mmodule`.
	# Visibility in modules and qualified names are correctly handled.
	#
	# If more than one class exists, then null is silently returned.
	# It is up to the caller to post-analysis the result and display a correct error message.
	# The method `class_not_found` can be used to display such a message.
	fun try_get_mclass_by_qid(qid: AQclassid, mmodule: MModule): nullable MClass
	do
		var name = qid.n_id.text

		var classes = model.get_mclasses_by_name(name)
		if classes == null then
			return null
		end

		var res: nullable MClass = null
		for mclass in classes do
			if not mmodule.in_importation <= mclass.intro_mmodule then continue
			if not mmodule.is_visible(mclass.intro_mmodule, mclass.visibility) then continue
			if not qid.accept(mclass) then continue
			if res == null then
				res = mclass
			else
				return null
			end
		end

		return res
	end

	# Like `try_get_mclass_by_name` but display an error message when the class is not found
	fun get_mclass_by_name(node: nullable ANode, mmodule: MModule, name: String): nullable MClass
	do
		var mclass = try_get_mclass_by_name(node, mmodule, name)
		if mclass == null then
			error(node, "Type Error: missing primitive class `{name}'.")
		end
		return mclass
	end

	# Return a property named `name` on the type `mtype` visible in the module `mmodule`.
	# Visibility in modules is correctly handled.
	# Protected properties are returned (it is up to the caller to check and reject protected properties).
	# If no such a property exists, then null is returned.
	# If more than one property exists, then an error on `anode` is displayed and null is returned.
	# FIXME: add a way to handle property name conflict
	fun try_get_mproperty_by_name2(anode: nullable ANode, mmodule: MModule, mtype: MType, name: String): nullable MProperty
	do
		var props = self.model.get_mproperties_by_name(name)
		if props == null then
			return null
		end

		var cache = self.try_get_mproperty_by_name2_cache[mmodule, mtype, name]
		if cache != null then return cache

		var res: nullable MProperty = null
		var ress: nullable Array[MProperty] = null
		for mprop in props do
			if not mtype.has_mproperty(mmodule, mprop) then continue
			if not mmodule.is_visible(mprop.intro_mclassdef.mmodule, mprop.visibility) then continue

			# new-factories are invisible outside of the class
			if mprop isa MMethod and mprop.is_new and (not mtype isa MClassType or mprop.intro_mclassdef.mclass != mtype.mclass) then
				continue
			end

			if res == null then
				res = mprop
				continue
			end

			# Two global properties?
			# First, special case for init, keep the most specific ones
			if res isa MMethod and mprop isa MMethod and res.is_init and mprop.is_init then
				var restype = res.intro_mclassdef.bound_mtype
				var mproptype = mprop.intro_mclassdef.bound_mtype
				if mproptype.is_subtype(mmodule, null, restype) then
					# found a most specific constructor, so keep it
					res = mprop
					continue
				end
			end

			# Ok, just keep all prop in the ress table
			if ress == null then
				ress = new Array[MProperty]
				ress.add(res)
			end
			ress.add(mprop)
		end

		# There is conflict?
		if ress != null and res isa MMethod and res.is_init then
			# special case forinit again
			var restype = res.intro_mclassdef.bound_mtype
			var ress2 = new Array[MProperty]
			for mprop in ress do
				var mproptype = mprop.intro_mclassdef.bound_mtype
				if not restype.is_subtype(mmodule, null, mproptype) then
					ress2.add(mprop)
				else if not mprop isa MMethod or not mprop.is_init then
					ress2.add(mprop)
				end
			end
			if ress2.is_empty then
				ress = null
			else
				ress = ress2
				ress.add(res)
			end
		end

		if ress != null then
			assert ress.length > 1
			var s = new Array[String]
			for mprop in ress do s.add mprop.full_name
			self.error(anode, "Error: ambiguous property name `{name}` for `{mtype}`; conflict between {s.join(" and ")}.")
		end

		self.try_get_mproperty_by_name2_cache[mmodule, mtype, name] = res
		return res
	end

	private var try_get_mproperty_by_name2_cache = new HashMap3[MModule, MType, String, nullable MProperty]


	# Alias for try_get_mproperty_by_name2(anode, mclassdef.mmodule, mclassdef.mtype, name)
	fun try_get_mproperty_by_name(anode: nullable ANode, mclassdef: MClassDef, name: String): nullable MProperty
	do
		return try_get_mproperty_by_name2(anode, mclassdef.mmodule, mclassdef.bound_mtype, name)
	end

	# Helper function to display an error on a node.
	# Alias for `self.toolcontext.error(n.hot_location, text)`
	#
	# This automatically sets `n.is_broken` to true.
	fun error(n: nullable ANode, text: String)
	do
		var l = null
		if n != null then
			l = n.hot_location
			n.is_broken = true
		end
		self.toolcontext.error(l, text)
	end

	# Helper function to display a warning on a node.
	# Alias for: `self.toolcontext.warning(n.hot_location, text)`
	fun warning(n: nullable ANode, tag, text: String)
	do
		var l = null
		if n != null then l = n.hot_location
		self.toolcontext.warning(l, tag, text)
	end

	# Helper function to display an advice on a node.
	# Alias for: `self.toolcontext.advice(n.hot_location, text)`
	fun advice(n: nullable ANode, tag, text: String)
	do
		var l = null
		if n != null then l = n.hot_location
		self.toolcontext.advice(l, tag, text)
	end

	# Force to get the primitive method named `name` on the type `recv` or do a fatal error on `n`
	fun force_get_primitive_method(n: nullable ANode, name: String, recv: MClass, mmodule: MModule): MMethod
	do
		var res = mmodule.try_get_primitive_method(name, recv)
		if res == null then
			var l = null
			if n != null then l = n.hot_location
			self.toolcontext.fatal_error(l, "Fatal Error: `{recv}` must have a property named `{name}`.")
			abort
		end
		return res
	end

	# Return the static type associated to the node `ntype`.
	#
	# `mclassdef` is the context where the call is made (used to understand
	# formal types).
	# In case of problem, an error is displayed on `ntype` and null is returned.
	#
	# Same as `resolve_mtype_unchecked3`, but get the context (module, class and
	# anchor) from `mclassdef`.
	#
	# SEE: `resolve_mtype`
	# SEE: `resolve_mtype3_unchecked`
	#
	# FIXME: Find a better name for this method.
	fun resolve_mtype_unchecked(mclassdef: MClassDef, ntype: AType, with_virtual: Bool): nullable MType
	do
		return resolve_mtype3_unchecked(
			mclassdef.mmodule,
			mclassdef.mclass,
			mclassdef.bound_mtype,
			ntype,
			with_virtual
		)
	end

	# Return the static type associated to the node `ntype`.
	#
	# `mmodule`, `mclass` and `anchor` compose the context where the call is
	# made (used to understand formal types).
	# In case of problem, an error is displayed on `ntype` and null is returned.
	#
	# Note: The “3” is for 3 contextual parameters.
	#
	# SEE: `resolve_mtype`
	# SEE: `resolve_mtype_unchecked`
	#
	# FIXME: Find a better name for this method.
	fun resolve_mtype3_unchecked(mmodule: MModule, mclass: nullable MClass, anchor: nullable MClassType, ntype: AType, with_virtual: Bool): nullable MType
	do
		var qid = ntype.n_qid
		var name = qid.n_id.text
		var res: MType

		# Check virtual type
		if anchor != null and with_virtual then
			var prop = try_get_mproperty_by_name2(ntype, mmodule, anchor, name).as(nullable MVirtualTypeProp)
			if prop != null then
				if not ntype.n_types.is_empty then
					error(ntype, "Type Error: formal type `{name}` cannot have formal parameters.")
				end
				res = prop.mvirtualtype
				if ntype.n_kwnullable != null then res = res.as_nullable
				ntype.mtype = res
				return res
			end
		end

		# Check parameter type
		if mclass != null then
			for p in mclass.mparameters do
				if p.name != name then continue

				if not ntype.n_types.is_empty then
					error(ntype, "Type Error: formal type `{name}` cannot have formal parameters.")
				end

				res = p
				if ntype.n_kwnullable != null then res = res.as_nullable
				ntype.mtype = res
				return res
			end
		end

		# Check class
		var found_class = try_get_mclass_by_qid(qid, mmodule)
		if found_class != null then
			var arity = ntype.n_types.length
			if arity != found_class.arity then
				if arity == 0 then
					error(ntype, "Type Error: `{found_class.signature_to_s}` is a generic class.")
				else if found_class.arity == 0 then
					error(ntype, "Type Error: `{name}` is not a generic class.")
				else
					error(ntype, "Type Error: expected {found_class.arity} formal argument(s) for `{found_class.signature_to_s}`; got {arity}.")
				end
				return null
			end
			if arity == 0 then
				res = found_class.mclass_type
				if ntype.n_kwnullable != null then res = res.as_nullable
				ntype.mtype = res
				return res
			else
				var mtypes = new Array[MType]
				for nt in ntype.n_types do
					var mt = resolve_mtype3_unchecked(mmodule, mclass, anchor, nt, with_virtual)
					if mt == null then return null # Forward error
					mtypes.add(mt)
				end
				res = found_class.get_mtype(mtypes)
				if ntype.n_kwnullable != null then res = res.as_nullable
				ntype.mtype = res
				return res
			end
		end

		# If everything fail, then give up with class by proposing things.
		#
		# TODO Give hints on formal types (param and virtual)
		class_not_found(qid, mmodule)
		ntype.is_broken = true
		return null
	end

	# Print an error and suggest hints when the class identified by `qid` in `mmodule` is not found.
	#
	# This just print error messages.
	fun class_not_found(qid: AQclassid, mmodule: MModule)
	do
		var name = qid.n_id.text
		var qname = qid.full_name

		if bad_class_names[mmodule].has(qname) then
			error(qid, "Error: class `{qname}` not found in module `{mmodule}`.")
			return
		end
		bad_class_names[mmodule].add(qname)

		var all_classes = model.get_mclasses_by_name(name)
		var hints = new Array[String]

		# Look for conflicting classes.
		if all_classes != null then for c in all_classes do
			if not mmodule.is_visible(c.intro_mmodule, c.visibility) then continue
			if not qid.accept(c) then continue
			hints.add "`{c.full_name}`"
		end
		if hints.length > 1 then
			error(qid, "Error: ambiguous class name `{qname}` in module `{mmodule}`. Conflicts are between {hints.join(",", " and ")}.")
			return
		end
		hints.clear

		# Look for imported but invisible classes.
		if all_classes != null then for c in all_classes do
			if not mmodule.in_importation <= c.intro_mmodule then continue
			if mmodule.is_visible(c.intro_mmodule, c.visibility) then continue
			if not qid.accept(c) then continue
			error(qid, "Error: class `{c.full_name}` not visible in module `{mmodule}`.")
			return
		end

		# Look for not imported but known classes from importable modules
		if all_classes != null then for c in all_classes do
			if mmodule.in_importation <= c.intro_mmodule then continue
			if c.intro_mmodule.in_importation <= mmodule then continue
			if c.visibility <= private_visibility then continue
			if not qid.accept(c) then continue
			hints.add "`{c.intro_mmodule.full_name}`"
		end
		if hints.not_empty then
			error(qid, "Error: class `{qname}` not found in module `{mmodule}`. Maybe import {hints.join(",", " or ")}?")
			return
		end

		# Look for classes with an approximative name.
		var bests = new BestDistance[MClass](qname.length - name.length / 2) # limit up to 50% name change
		for c in model.mclasses do
			if not mmodule.in_importation <= c.intro_mmodule then continue
			if not mmodule.is_visible(c.intro_mmodule, c.visibility) then continue
			var d = qname.levenshtein_distance(c.name)
			bests.update(d, c)
			d = qname.levenshtein_distance(c.full_name)
			bests.update(d, c)
		end
		if bests.best_items.not_empty then
			for c in bests.best_items do hints.add "`{c.full_name}`"
			error(qid, "Error: class `{qname}` not found in module `{mmodule}`. Did you mean {hints.join(",", " or ")}?")
			return
		end

		error(qid, "Error: class `{qname}` not found in module `{mmodule}`.")
	end

	# List of already reported bad class names.
	# Used to not perform and repeat hints again and again.
	private var bad_class_names = new MultiHashMap[MModule, String]

	# Return the static type associated to the node `ntype`.
	#
	# `mclassdef` is the context where the call is made (used to understand
	# formal types).
	# In case of problem, an error is displayed on `ntype` and null is returned.
	#
	# Same as `resolve_mtype3`, but get the context (module, class and ) from
	# `mclassdef`.
	#
	# SEE: `resolve_mtype3`
	# SEE: `resolve_mtype_unchecked`
	#
	# FIXME: Find a better name for this method.
	fun resolve_mtype(mclassdef: MClassDef, ntype: AType): nullable MType
	do
		return resolve_mtype3(
			mclassdef.mmodule,
			mclassdef.mclass,
			mclassdef.bound_mtype,
			ntype
		)
	end

	# Return the static type associated to the node `ntype`.
	#
	# `mmodule`, `mclass` and `anchor` compose the context where the call is
	# made (used to understand formal types).
	# In case of problem, an error is displayed on `ntype` and null is returned.
	#
	# Note: The “3” is for 3 contextual parameters.
	#
	# SEE: `resolve_mtype`
	# SEE: `resolve_mtype_unchecked`
	#
	# FIXME: Find a better name for this method.
	fun resolve_mtype3(mmodule: MModule, mclass: nullable MClass, anchor: nullable MClassType, ntype: AType): nullable MType
	do
		var mtype = ntype.mtype
		if mtype == null then mtype = resolve_mtype3_unchecked(mmodule, mclass, anchor, ntype, true)
		if mtype == null then return null # Forward error

		if ntype.checked_mtype then return mtype
		if mtype isa MGenericType then
			var found_class = mtype.mclass
			for i in [0..found_class.arity[ do
				var intro = found_class.try_intro
				if intro == null then return null # skip error
				var bound = intro.bound_mtype.arguments[i]
				var nt = ntype.n_types[i]
				var mt = resolve_mtype3(mmodule, mclass, anchor, nt)
				if mt == null then return null # forward error
				if not check_subtype(nt, mmodule, anchor, mt, bound) then
					error(nt, "Type Error: expected `{bound}`, got `{mt}`.")
					return null
				end
			end
		end
		ntype.checked_mtype = true
		return mtype
	end

	# Check that `sub` is a subtype of `sup`.
	# Do not display an error message.
	#
	# This method is used a an entry point for the modelize phase to test static subtypes.
	# Some refinements could redefine it to collect statictics.
	fun check_subtype(node: ANode, mmodule: MModule, anchor: nullable MClassType, sub, sup: MType): Bool
	do
		return sub.is_subtype(mmodule, anchor, sup)
	end

	# Check that `sub` and `sup` are equvalent types.
	# Do not display an error message.
	#
	# This method is used a an entry point for the modelize phase to test static equivalent types.
	# Some refinements could redefine it to collect statictics.
	fun check_sametype(node: ANode, mmodule: MModule, anchor: nullable MClassType, sub, sup: MType): Bool
	do
		return sub.is_subtype(mmodule, anchor, sup) and sup.is_subtype(mmodule, anchor, sub)
	end
end
src/modelbuilder_base.nit:42,1--529,3