<commit_before># This file is part of NIT ( http://www.nitlanguage.org ).
#
# Copyright 2014 <NAME> <<EMAIL>>
#
# 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.

# Annotations to gather metadata on Android projects. Get the metadata
# by calling `ModelBuilder::android_project_for`.
module android_annotations

import parser_util
import modelbuilder
import modelize_property
import literal
import typing

# Metadata associated to an Android project
class AndroidProject
	# Name of the resulting application
	var name: nullable String = null

	# Java package used to identify the APK
	var java_package: nullable String = null

	# Version of the Android application and APK
	var version: nullable String = null

	redef fun to_s do return """
name: {{{name or else "null"}}}
namespace: {{{java_package or else "null"}}}
version: {{{version or else "null"}}}"""
end

redef class ModelBuilder
	# Get the `AndroidProject` gathered from `mmodule` and its importations
	fun android_project_for(mmodule: MModule): AndroidProject
	do
		var project = new AndroidProject

		var annot = priority_annotation_on_modules("app_name", mmodule)
		if annot != null then project.name = annot.arg_as_string(self)

		annot =  priority_annotation_on_modules("app_version", mmodule)
		if annot != null then project.version =  annot.as_version(self)

		annot = priority_annotation_on_modules("java_package", mmodule)
		if annot != null then project.java_package = annot.arg_as_string(self)

		toolcontext.check_errors

		return project
	end

	# Get an annotation by name from `mmodule` and its super modules. Will recursively search
	# in imported module to find the "latest" declaration and detects priority conflicts.
	private fun priority_annotation_on_modules(name: String, mmodule: MModule): nullable AAnnotation
	do
		if mmodule2nmodule.keys.has(mmodule) then
			var amod = mmodule2nmodule[mmodule]
			var module_decl = amod.n_moduledecl
			if module_decl != null then
				var annotations = module_decl.collect_annotations_by_name(name)
				if annotations.length == 1 then
					return annotations.first
				else if annotations.length > 1 then
					toolcontext.error(mmodule.location,
						"Multiple declaration of annotation {name}, it must be defined only once.")
				end
			end
		end

		var sources = new Array[MModule]
		var annotations = null
		for mmod in mmodule.in_importation.direct_greaters do
			var res = priority_annotation_on_modules(name, mmod)
			if res != null then
				sources.add mmod
				annotations = res
			end
		end
		if sources.length > 1 then
			toolcontext.error(mmodule.location,
				"Priority conflict on annotation {name}, it has been defined in: {sources.join(", ")}")
			return null
		end
		return annotations
	end
end

redef class AAnnotation
	# Get the single argument of `self` as a `String`. Raise error on any inconsistency.
	private fun arg_as_string(modelbuilder: ModelBuilder): String
	do
		var annotation_name = n_atid.n_id.text
		var format_error = "Annotation error: \"{annotation_name}\" expects a single String as argument."

		var args = n_args
		var platform_name
		if args.length != 1 then
			modelbuilder.error(self, format_error)
			return ""
		else
			var arg = args.first
			
			if not arg isa AExprAtArg then
				modelbuilder.error(self, format_error)
				return ""
			end

			var expr = arg.n_expr
			if not expr isa AStringFormExpr then
				modelbuilder.error(self, format_error)
				return ""
			end
			return expr.value.as(not null)
		end
	end

	# Returns a version string (example: "1.5.6b42a7c") from an annotation `version(1, 5, git_revision)`.
	#
	# The user can enter as many fields as needed. The call to `git_revision` will be replaced by the short
	# revision number. If the working tree is dirty, it will append another field with "d" for dirty.
	private fun as_version(modelbuilder: ModelBuilder): String
	do
		var annotation_name = n_atid.n_id.text
		var version_fields = new Array[Object]

		var args = n_args
		var platform_name
		if args.length < 1 then
			modelbuilder.error(self, "Annotation error: \"{annotation_name}\" expects at least a single argument.")
			return ""
		else
			for arg in args do
				var format_error = "Annotation error: \"{annotation_name}\" expects its arguments to be of type Int or a call to `git_revision`"
				
				if not arg isa AExprAtArg then
					modelbuilder.error(self, format_error)
					return ""
				end

				var expr = arg.n_expr
				if expr isa AIntExpr then
					var value = expr.value
					assert value != null
					version_fields.add value
				else if expr isa AStringFormExpr then
					version_fields.add expr.value.as(not null)
				else if expr isa ACallExpr then
					# We support calls to "git" only
					var exec_args = expr.n_args.to_a
					if expr.n_id.text != "git_revision" or not exec_args.is_empty then
						modelbuilder.error(self,
							"Annotation error: \"{annotation_name}\" accepts only calls to `git_revision` with the command as arguments.")
						return ""
					end

					# Get Git short revision
					var proc = new IProcess("git", "rev-parse", "--short", "HEAD")
					proc.wait
					assert proc.status == 0
					var lines = proc.read_all
					var revision = lines.split("\n").first

					# Is it dirty?
					# If not, the return of `git diff --shortstat` is an empty line
					proc = new IProcess("git", "diff-index", "--quiet", "HEAD")
					proc.wait
					var dirty = proc.status != 0
					if dirty then revision += ".d"

					version_fields.add revision
				else
					modelbuilder.error(self, format_error)
					return ""
				end
			end
		end

		return version_fields.join(".")
	end
end
<commit_msg>android: intro annotation to customize Android manifest<commit_after># This file is part of NIT ( http://www.nitlanguage.org ).
#
# Copyright 2014 <NAME> <<EMAIL>>
#
# 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.

# Annotations to gather metadata on Android projects. Get the metadata
# by calling `ModelBuilder::android_project_for`.
module android_annotations

import parser_util
import modelbuilder
import modelize_property
import literal
import typing

# Metadata associated to an Android project
class AndroidProject
	# Name of the resulting application
	var name: nullable String = null

	# Java package used to identify the APK
	var java_package: nullable String = null

	# Version of the Android application and APK
	var version: nullable String = null

	# Custom lines to add to the AndroidManifest.xml in the <manifest> node
	var manifest_lines = new Array[String]

	# Custom lines to add to the AndroidManifest.xml in the <application> node
	var manifest_application_lines = new Array[String]

	redef fun to_s do return """
name: {{{name or else "null"}}}
namespace: {{{java_package or else "null"}}}
version: {{{version or else "null"}}}"""
end

redef class ModelBuilder
	# Get the `AndroidProject` gathered from `mmodule` and its importations
	fun android_project_for(mmodule: MModule): AndroidProject
	do
		var project = new AndroidProject

		var annot = priority_annotation_on_modules("app_name", mmodule)
		if annot != null then project.name = annot.arg_as_string(self)

		annot =  priority_annotation_on_modules("app_version", mmodule)
		if annot != null then project.version =  annot.as_version(self)

		annot = priority_annotation_on_modules("java_package", mmodule)
		if annot != null then project.java_package = annot.arg_as_string(self)

		var annots = collect_annotations_on_modules("android_manifest", mmodule)
		for an in annots do project.manifest_lines.add an.arg_as_string(self)

		annots = collect_annotations_on_modules("android_manifest_application", mmodule)
		for an in annots do project.manifest_application_lines.add an.arg_as_string(self)

		toolcontext.check_errors

		return project
	end

	# Recursively collect all annotations by name in `mmodule` and its importations (direct and indirect)
	private fun collect_annotations_on_modules(name: String, mmodule: MModule): Array[AAnnotation]
	do
		var annotations = new Array[AAnnotation]
		for mmod in mmodule.in_importation.greaters do
			if not mmodule2nmodule.keys.has(mmod) then continue
			var amod = mmodule2nmodule[mmod]
			var module_decl = amod.n_moduledecl
			if module_decl == null then continue
			var aas = module_decl.collect_annotations_by_name(name)
			annotations.add_all aas
		end
		return annotations
	end

	# Get an annotation by name from `mmodule` and its super modules. Will recursively search
	# in imported module to find the "latest" declaration and detects priority conflicts.
	private fun priority_annotation_on_modules(name: String, mmodule: MModule): nullable AAnnotation
	do
		if mmodule2nmodule.keys.has(mmodule) then
			var amod = mmodule2nmodule[mmodule]
			var module_decl = amod.n_moduledecl
			if module_decl != null then
				var annotations = module_decl.collect_annotations_by_name(name)
				if annotations.length == 1 then
					return annotations.first
				else if annotations.length > 1 then
					toolcontext.error(mmodule.location,
						"Multiple declaration of annotation {name}, it must be defined only once.")
				end
			end
		end

		var sources = new Array[MModule]
		var annotations = null
		for mmod in mmodule.in_importation.direct_greaters do
			var res = priority_annotation_on_modules(name, mmod)
			if res != null then
				sources.add mmod
				annotations = res
			end
		end
		if sources.length > 1 then
			toolcontext.error(mmodule.location,
				"Priority conflict on annotation {name}, it has been defined in: {sources.join(", ")}")
			return null
		end
		return annotations
	end
end

redef class AAnnotation
	# Get the single argument of `self` as a `String`. Raise error on any inconsistency.
	private fun arg_as_string(modelbuilder: ModelBuilder): String
	do
		var annotation_name = n_atid.n_id.text
		var format_error = "Annotation error: \"{annotation_name}\" expects a single String as argument."

		var args = n_args
		var platform_name
		if args.length != 1 then
			modelbuilder.error(self, format_error)
			return ""
		else
			var arg = args.first
			
			if not arg isa AExprAtArg then
				modelbuilder.error(self, format_error)
				return ""
			end

			var expr = arg.n_expr
			if not expr isa AStringFormExpr then
				modelbuilder.error(self, format_error)
				return ""
			end
			return expr.value.as(not null)
		end
	end

	# Returns a version string (example: "1.5.6b42a7c") from an annotation `version(1, 5, git_revision)`.
	#
	# The user can enter as many fields as needed. The call to `git_revision` will be replaced by the short
	# revision number. If the working tree is dirty, it will append another field with "d" for dirty.
	private fun as_version(modelbuilder: ModelBuilder): String
	do
		var annotation_name = n_atid.n_id.text
		var version_fields = new Array[Object]

		var args = n_args
		var platform_name
		if args.length < 1 then
			modelbuilder.error(self, "Annotation error: \"{annotation_name}\" expects at least a single argument.")
			return ""
		else
			for arg in args do
				var format_error = "Annotation error: \"{annotation_name}\" expects its arguments to be of type Int or a call to `git_revision`"
				
				if not arg isa AExprAtArg then
					modelbuilder.error(self, format_error)
					return ""
				end

				var expr = arg.n_expr
				if expr isa AIntExpr then
					var value = expr.value
					assert value != null
					version_fields.add value
				else if expr isa AStringFormExpr then
					version_fields.add expr.value.as(not null)
				else if expr isa ACallExpr then
					# We support calls to "git" only
					var exec_args = expr.n_args.to_a
					if expr.n_id.text != "git_revision" or not exec_args.is_empty then
						modelbuilder.error(self,
							"Annotation error: \"{annotation_name}\" accepts only calls to `git_revision` with the command as arguments.")
						return ""
					end

					# Get Git short revision
					var proc = new IProcess("git", "rev-parse", "--short", "HEAD")
					proc.wait
					assert proc.status == 0
					var lines = proc.read_all
					var revision = lines.split("\n").first

					# Is it dirty?
					# If not, the return of `git diff --shortstat` is an empty line
					proc = new IProcess("git", "diff-index", "--quiet", "HEAD")
					proc.wait
					var dirty = proc.status != 0
					if dirty then revision += ".d"

					version_fields.add revision
				else
					modelbuilder.error(self, format_error)
					return ""
				end
			end
		end

		return version_fields.join(".")
	end
end

{'fraction_non_alphanumeric': 0.07820770893908895, 'fraction_numerical': 0.0038768359054648475, 'mean_word_length': 3.733239237826394, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 4, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4863775', 'n_tokens_mistral': 4598, 'n_tokens_neox': 3914, 'n_words': 1475}