From 9d74b49ab62908d4acf53dde444413886b8b28e5 Mon Sep 17 00:00:00 2001 From: Radio Date: Thu, 4 Jun 2026 15:15:11 +0300 Subject: escoria setup and old branch archives --- addons/escoria-core/tools/logging/esc_log_level.gd | 35 ++ .../tools/logging/esc_log_level.gd.uid | 1 + addons/escoria-core/tools/logging/esc_logger.gd | 462 +++++++++++++++++++++ .../escoria-core/tools/logging/esc_logger.gd.uid | 1 + .../escoria-core/tools/logging/esc_safe_logging.gd | 60 +++ .../tools/logging/esc_safe_logging.gd.uid | 1 + .../analyzers/esc_exit_scene_script_analyzer.gd | 151 +++++++ .../esc_exit_scene_script_analyzer.gd.uid | 1 + .../analyzers/esc_script_analyzer.gd | 387 +++++++++++++++++ .../analyzers/esc_script_analyzer.gd.uid | 1 + .../tools/script_analysis/esc_ashes_analyzer.gd | 52 +++ .../script_analysis/esc_ashes_analyzer.gd.uid | 1 + .../tools/script_analysis/esc_static_analyzers.gd | 19 + .../script_analysis/esc_static_analyzers.gd.uid | 1 + 14 files changed, 1173 insertions(+) create mode 100644 addons/escoria-core/tools/logging/esc_log_level.gd create mode 100644 addons/escoria-core/tools/logging/esc_log_level.gd.uid create mode 100644 addons/escoria-core/tools/logging/esc_logger.gd create mode 100644 addons/escoria-core/tools/logging/esc_logger.gd.uid create mode 100644 addons/escoria-core/tools/logging/esc_safe_logging.gd create mode 100644 addons/escoria-core/tools/logging/esc_safe_logging.gd.uid create mode 100644 addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd create mode 100644 addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd.uid create mode 100644 addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd create mode 100644 addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd.uid create mode 100644 addons/escoria-core/tools/script_analysis/esc_ashes_analyzer.gd create mode 100644 addons/escoria-core/tools/script_analysis/esc_ashes_analyzer.gd.uid create mode 100644 addons/escoria-core/tools/script_analysis/esc_static_analyzers.gd create mode 100644 addons/escoria-core/tools/script_analysis/esc_static_analyzers.gd.uid (limited to 'addons/escoria-core/tools') diff --git a/addons/escoria-core/tools/logging/esc_log_level.gd b/addons/escoria-core/tools/logging/esc_log_level.gd new file mode 100644 index 0000000..517cbf5 --- /dev/null +++ b/addons/escoria-core/tools/logging/esc_log_level.gd @@ -0,0 +1,35 @@ +class_name ESCLogLevel +## Log levels for ESCLogger. + +## Valid log levels by order of granularity (E, W, I, D, T) +enum { + LOG_ERROR, + LOG_WARNING, + LOG_INFO, + LOG_DEBUG, + LOG_TRACE +} + + +## A map of log level names to log level ints +const LEVEL_MAP: Dictionary = { + "ERROR": LOG_ERROR, + "WARNING": LOG_WARNING, + "INFO": LOG_INFO, + "DEBUG": LOG_DEBUG, + "TRACE": LOG_TRACE, +} + +## Static function to determine the int log level value defined in Project Settings (Escoria>Debug>Log Level)[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a `int` value. (`int`) +static func determine_escoria_log_level() -> int: + return LEVEL_MAP[ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.LOG_LEVEL + ).to_upper()] diff --git a/addons/escoria-core/tools/logging/esc_log_level.gd.uid b/addons/escoria-core/tools/logging/esc_log_level.gd.uid new file mode 100644 index 0000000..d3fee64 --- /dev/null +++ b/addons/escoria-core/tools/logging/esc_log_level.gd.uid @@ -0,0 +1 @@ +uid://cfnhsqcchmqbd diff --git a/addons/escoria-core/tools/logging/esc_logger.gd b/addons/escoria-core/tools/logging/esc_logger.gd new file mode 100644 index 0000000..26d8d86 --- /dev/null +++ b/addons/escoria-core/tools/logging/esc_logger.gd @@ -0,0 +1,462 @@ +## Base class of all logger types. +class ESCLoggerBase: + ## Signal sent when Escoria requires performing an emergency savegame.[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## None. + ## [br] + signal perform_emergency_savegame + + ## Signal sent when an error or warning happened.[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## | Name | Type | Description | Required? |[br] + ## |:-----|:-----|:------------|:----------|[br] + ## |message|`Variant`|Error or warning message emitted through the logger.|yes|[br] + ## [br] + signal error_message_signal(message) + + ## Log filename format + const LOG_FILE_FORMAT: String = "log_%s_%s.log" + + # Configured log level + var _log_level: int + + ## If true, assert() functions will not be called, thus the program won't + ## exit or error. Resets to false after an assert() call was ignored once. + ## Useful for console calls. + var dont_assert: bool = false + + + # Constructor + func _init(): + _log_level = ESCLogLevel.determine_escoria_log_level() + + ## Formats a message depending on context, message and letter, then returns it. The formatted string[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## | Name | Type | Description | Required? |[br] + ## |:-----|:-----|:------------|:----------|[br] + ## |context|`String`|usually, the escoria file that sent the message.|yes|[br] + ## |msg|`String`|logged message.|yes|[br] + ## |letter|`String`|letter to add to the formatted log (I for Info, W for Warning...)|yes|[br] + ## [br] + ## #### Returns[br] + ## [br] + ## Returns a `String` value. (`String`) + func formatted_message(context: String, msg: String, letter: String) -> String: + return "ESC ({0}) {1} {2}: {3}".format([_formatted_date(), letter, context, msg]) + + ## Trace log[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## | Name | Type | Description | Required? |[br] + ## |:-----|:-----|:------------|:----------|[br] + ## |owner|`Object`|caller object (usually, `self`)|yes|[br] + ## |msg|`String`|logged message.|yes|[br] + ## [br] + ## #### Returns[br] + ## [br] + ## Returns nothing. + func trace(owner: Object, msg: String): + var context: String = owner.get_script().resource_path.get_file() + _trace_message(context, msg) + + # Direct message trace log (requiring a string for the context) + # + # #### PARAMETERS + # + # - owner: caller as a string value + # - msg: logged message. + func _trace_message(context: String, msg: String): + print(formatted_message(context, msg, "T")) + + ## Debug log[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## | Name | Type | Description | Required? |[br] + ## |:-----|:-----|:------------|:----------|[br] + ## |owner|`Object`|caller object (usually, `self`)|yes|[br] + ## |msg|`String`|logged message.|yes|[br] + ## [br] + ## #### Returns[br] + ## [br] + ## Returns nothing. + func debug(owner: Object, msg: String): + var context: String = owner.get_script().resource_path.get_file() + _debug_message(context, msg) + + # Static debug log (requiring a string for the context) + # + # #### PARAMETERS + # + # - owner: caller as a string value + # - msg: logged message. + func _debug_message(context: String, msg: String): + print(formatted_message(context, msg, "D")) + + ## Info log[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## | Name | Type | Description | Required? |[br] + ## |:-----|:-----|:------------|:----------|[br] + ## |owner|`Object`|caller object (usually, `self`)|yes|[br] + ## |msg|`String`|logged message.|yes|[br] + ## [br] + ## #### Returns[br] + ## [br] + ## Returns nothing. + func info(owner: Object, msg: String): + var context: String = owner.get_script().resource_path.get_file() + _info_message(context, msg) + + # Static info log (requiring a string for the context) + # + # #### PARAMETERS + # + # - owner: caller as a string value + # - msg: logged message. + func _info_message(context: String, msg: String): + print(formatted_message(context, msg, "I")) + + ## Warning log[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## | Name | Type | Description | Required? |[br] + ## |:-----|:-----|:------------|:----------|[br] + ## |owner|`Object`|caller object (usually, `self`)|yes|[br] + ## |msg|`String`|logged message.|yes|[br] + ## [br] + ## #### Returns[br] + ## [br] + ## Returns nothing. + func warn(owner: Object, msg: String): + var context: String = owner.get_script().resource_path.get_file() + _warn_message(context, msg) + + # Static warning log (requiring a string for the context) + # + # #### PARAMETERS + # + # - owner: caller as a string value + # - msg: logged message. + func _warn_message(context: String, msg: String): + print(formatted_message(context, msg, "W")) + push_warning(formatted_message(context, msg, "W")) + if ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.TERMINATE_ON_WARNINGS + ): + if not dont_assert: + assert(false) + escoria.get_tree().quit() + dont_assert = false + error_message_signal.emit(msg) + + ## Error log[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## | Name | Type | Description | Required? |[br] + ## |:-----|:-----|:------------|:----------|[br] + ## |owner|`Object`|caller object (usually, `self`)|yes|[br] + ## |msg|`String`|logged message.|yes|[br] + ## [br] + ## #### Returns[br] + ## [br] + ## Returns nothing. + func error(owner: Object, msg: String): + var context = owner.get_script().resource_path.get_file() + _error_message(context, msg) + + # Static error log (requiring a string for the context) + # + # #### PARAMETERS + # + # - owner: caller as a string value + # - msg: logged message. + func _error_message(context: String, msg: String): + printerr(formatted_message(context, msg, "E")) + push_error(formatted_message(context, msg, "E")) + if ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.TERMINATE_ON_ERRORS + ): + if not dont_assert: + assert(false) + escoria.get_tree().quit() + dont_assert = false + error_message_signal.emit(msg) + + # Formats the current system's datetime as 'YYYY-mm-ddTHH:MM:SS'. + func _formatted_date() -> String: + var info = Time.get_datetime_dict_from_system() + info["year"] = "%04d" % info["year"] + info["month"] = "%02d" % info["month"] + info["day"] = "%02d" % info["day"] + info["hour"] = "%02d" % info["hour"] + info["minute"] = "%02d" % info["minute"] + info["second"] = "%02d" % info["second"] + return "{year}-{month}-{day}T{hour}:{minute}:{second}".format(info) + + +## A logger that logs to the terminal and to a log file. +class ESCLoggerFile extends ESCLoggerBase: + ## Log file handler + var log_file: FileAccess + + # Constructor + func _init(): + super() + # This is left alone as this constructor is called from escoria.gd's own + # constructor + var log_file_path = ProjectSettings.get_setting( + ESCProjectSettingsManager.LOG_FILE_PATH + ) + var date = Time.get_datetime_dict_from_system() + log_file_path = log_file_path.path_join(LOG_FILE_FORMAT % [ + str(date["year"]) + str(date["month"]) + str(date["day"]), + str(date["hour"]) + str(date["minute"]) + str(date["second"]) + ]) + log_file = FileAccess.open( + log_file_path, + FileAccess.WRITE + ) + + ## Trace log[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## | Name | Type | Description | Required? |[br] + ## |:-----|:-----|:------------|:----------|[br] + ## |owner|`Object`|caller object (usually, `self`)|yes|[br] + ## |msg|`String`|logged message.|yes|[br] + ## [br] + ## #### Returns[br] + ## [br] + ## Returns nothing. + func trace(owner: Object, msg: String): + if _log_level >= ESCLogLevel.LOG_TRACE: + _log_to_file(owner, msg, "T") + super.trace(owner, msg) + + # Direct message trace log (requiring a string for the context) + # + # #### PARAMETERS + # + # - owner: caller as a string value + # - msg: logged message. + func _trace_message(context: String, msg: String): + if _log_level >= ESCLogLevel.LOG_TRACE: + _log_to_file_message(context, msg, "T") + super._trace_message(context, msg) + + ## Debug log[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## | Name | Type | Description | Required? |[br] + ## |:-----|:-----|:------------|:----------|[br] + ## |owner|`Object`|caller object (usually, `self`)|yes|[br] + ## |msg|`String`|logged message.|yes|[br] + ## [br] + ## #### Returns[br] + ## [br] + ## Returns nothing. + func debug(owner: Object, msg: String): + if _log_level >= ESCLogLevel.LOG_DEBUG: + _log_to_file(owner, msg, "D") + super.debug(owner, msg) + + # Static debug log (requiring a string for the context) + # + # #### PARAMETERS + # + # - owner: caller as a string value + # - msg: logged message. + func _debug_message(context: String, msg: String): + if _log_level >= ESCLogLevel.LOG_DEBUG: + _log_to_file_message(context, msg, "D") + super._debug_message(context, msg) + + ## Info log[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## | Name | Type | Description | Required? |[br] + ## |:-----|:-----|:------------|:----------|[br] + ## |owner|`Object`|caller object (usually, `self`)|yes|[br] + ## |msg|`String`|logged message.|yes|[br] + ## [br] + ## #### Returns[br] + ## [br] + ## Returns nothing. + func info(owner: Object, msg: String): + if _log_level >= ESCLogLevel.LOG_INFO: + _log_to_file(owner, msg, "I") + super.info(owner, msg) + + # Static info log (requiring a string for the context) + # + # #### PARAMETERS + # + # - owner: caller as a string value + # - msg: logged message. + func _info_message(context: String, msg: String): + if _log_level >= ESCLogLevel.LOG_INFO: + _log_to_file_message(context, msg, "I") + super._info_message(context, msg) + + ## Warning log[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## | Name | Type | Description | Required? |[br] + ## |:-----|:-----|:------------|:----------|[br] + ## |owner|`Object`|caller object (usually, `self`)|yes|[br] + ## |msg|`String`|logged message.|yes|[br] + ## [br] + ## #### Returns[br] + ## [br] + ## Returns nothing. + func warn(owner: Object, msg: String): + if _log_level >= ESCLogLevel.LOG_WARNING: + _log_to_file(owner, msg, "W") + if ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.TERMINATE_ON_WARNINGS + ): + _log_stack_trace_to_file() + print_stack() + close_logs() + super.warn(owner, msg) + + # Static warning log (requiring a string for the context) + # + # #### PARAMETERS + # + # - owner: caller as a string value + # - msg: logged message. + func _warn_message(context: String, msg: String): + if _log_level >= ESCLogLevel.LOG_WARNING: + _log_to_file_message(context, msg, "W") + if ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.TERMINATE_ON_WARNINGS + ): + _log_stack_trace_to_file() + print_stack() + close_logs() + super._warn_message(context, msg) + + ## Error log[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## | Name | Type | Description | Required? |[br] + ## |:-----|:-----|:------------|:----------|[br] + ## |owner|`Object`|caller object (usually, `self`)|yes|[br] + ## |msg|`String`|logged message.|yes|[br] + ## [br] + ## #### Returns[br] + ## [br] + ## Returns nothing. + func error(owner: Object, msg: String): + if _log_level >= ESCLogLevel.LOG_ERROR: + _log_to_file(owner, msg, "E") + if ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.TERMINATE_ON_ERRORS + ): + _log_stack_trace_to_file() + print_stack() + close_logs() + super.error(owner, msg) + + # Static error log (requiring a string for the context) + # + # #### PARAMETERS + # + # - owner: caller as a string value + # - msg: logged message. + func _error_message(context: String, msg: String): + if _log_level >= ESCLogLevel.LOG_ERROR: + _log_to_file_message(context, msg, "E") + if ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.TERMINATE_ON_ERRORS + ): + _log_stack_trace_to_file() + print_stack() + close_logs() + super._error_message(context, msg) + + + ## Close the log file cleanly[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## None. + ## [br] + ## #### Returns[br] + ## [br] + ## Returns nothing. + func close_logs(): + print("Closing logs peacefully.") + _log_line_to_file("Closing logs peacefully.") + log_file.close() + + # Log the log message and context to file + func _log_to_file(owner: Object, msg: String, letter: String): + var context: String + if owner != null: + context = owner.get_script().resource_path.get_file() + _log_to_file_message(context, msg, letter) + + # Log the log message and context as string to file + func _log_to_file_message(context: String, msg: String, letter: String): + if log_file.is_open(): + log_file.store_string(formatted_message(context, msg, letter) + "\n") + + # Log the message line to file + func _log_line_to_file(msg: String): + if log_file.is_open(): + log_file.store_string(msg + "\n") + + # Log the stack trace to file + func _log_stack_trace_to_file(): + var frame_number = 0 + for stack in get_stack().slice(2, get_stack().size()): + _log_line_to_file( + "Frame %s - %s:%s in function '%s'" % [ + str(frame_number), + stack["source"], + stack["line"], + stack["function"], + ] + ) + frame_number += 1 + + +## A simple logger that logs to terminal using debug() function +class ESCLoggerVerbose extends ESCLoggerBase: + # Constructor + func _init(): + pass + + ## Debug log[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## | Name | Type | Description | Required? |[br] + ## |:-----|:-----|:------------|:----------|[br] + ## |owner|`Object`|caller object (usually, `self`)|yes|[br] + ## |msg|`String`|logged message.|yes|[br] + ## [br] + ## #### Returns[br] + ## [br] + ## Returns nothing. + func debug(owner: Object, msg: String): + var context = owner.get_script().resource_path.get_file() + print(context, ": ", msg) diff --git a/addons/escoria-core/tools/logging/esc_logger.gd.uid b/addons/escoria-core/tools/logging/esc_logger.gd.uid new file mode 100644 index 0000000..be8df74 --- /dev/null +++ b/addons/escoria-core/tools/logging/esc_logger.gd.uid @@ -0,0 +1 @@ +uid://3rkr84aiwrdt diff --git a/addons/escoria-core/tools/logging/esc_safe_logging.gd b/addons/escoria-core/tools/logging/esc_safe_logging.gd new file mode 100644 index 0000000..392d71b --- /dev/null +++ b/addons/escoria-core/tools/logging/esc_safe_logging.gd @@ -0,0 +1,60 @@ +# This static class is primarily for situations where there's a possibility that logging to the +# console may be done by a tool script, and so won't have access to the autoloader/singleton that is +# `escoria`. +class_name ESCSafeLogging + + +const COLOUR_RED = "red" +const COLOUR_GREEN = "green" +const COLOUR_YELLOW = "yellow" + + +static func log_level() -> String: + return ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.LOG_LEVEL + ).to_upper() + + +static func log_error(owner: Object, message: String) -> void: + if Engine.is_editor_hint(): + print_rich("[color=%s]%s[/color]" % [COLOUR_RED, message]) + else: + escoria.logger.error(owner, message) + + +static func log_warn(owner: Object, message: String) -> void: + if Engine.is_editor_hint(): + print_rich("[color=%s]%s[/color]" % [COLOUR_YELLOW, message]) + else: + escoria.logger.warn(owner, message) + + +static func log_info(owner: Object, message: String) -> void: + if Engine.is_editor_hint(): + print(message) + else: + escoria.logger.info(owner, message) + + +static func log_debug(owner: Object, message: String) -> void: + if Engine.is_editor_hint(): + print(message) + else: + escoria.logger.debug(owner, message) + + +static func log_trace(owner: Object, message: String) -> void: + if Engine.is_editor_hint(): + print(message) + else: + escoria.logger.trace(owner, message) + + +# Doesn't correpond to a logging level for offline logging; meant more for messages conveying a result +static func log_result(owner: Object, message: String, is_successful_result: bool) -> void: + var colour: String = COLOUR_GREEN if is_successful_result else COLOUR_RED + + if Engine.is_editor_hint(): + print_rich("[color=%s]%s[/color]" % [colour, message]) + else: + escoria.logger.info(owner, message) diff --git a/addons/escoria-core/tools/logging/esc_safe_logging.gd.uid b/addons/escoria-core/tools/logging/esc_safe_logging.gd.uid new file mode 100644 index 0000000..4e08a9a --- /dev/null +++ b/addons/escoria-core/tools/logging/esc_safe_logging.gd.uid @@ -0,0 +1 @@ +uid://cqexq6xpcccac diff --git a/addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd b/addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd new file mode 100644 index 0000000..3ed0025 --- /dev/null +++ b/addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd @@ -0,0 +1,151 @@ +extends ESCScriptAnalyzer +class_name ESCExitSceneScriptAnalyzer + + +const EXIT_SCENE_EVENT_NAME = "exit_scene" +const ACCEPT_INPUT_DISABLE_ARGS = ["none", "skip"] + +const CHANGE_SCENE_MISSING_MESSAGE = "Event ':exit_scene' is missing 'change_scene' command. Scene might not change as expected. Ignore this warning if the command was omitted on purpose." + +const COMMANDS_AFTER_CHANGE_SCENE_MESSAGE = "Event ':exit_scene' may have commands that are expected to be run after a call to 'change_scene'. Such commands will not be executed." + +const MISSING_ACCEPT_INPUT_MESSAGE = \ + "Event ':exit_scene' may allow for the player character to move while exiting and changing the scene." \ + + " To prevent this, ensure a call to 'accept_input' with an argument evaluating to 'NONE' or 'SKIP' is" \ + + " made before any calls to 'transition' and/or 'change_scene'." + +const BULLET_CHARACTER = "- " + + +# These are vars because they have to be, but should be treated as consts +var TRANSITION_COMMAND_NAME = TransitionCommand.new().get_command_name() +var CHANGE_SCENE_COMMAND_NAME = ChangeSceneCommand.new().get_command_name() +var ACCEPT_INPUT_COMMAND_NAME = AcceptInputCommand.new().get_command_name() + +var _has_change_scene_command: bool = false + +var _has_commands_after_change_scene_command: bool = false + +var _change_scene_token: ESCToken = null + +var _accept_input_disable_missing: bool = false + +var _accept_input_token: ESCToken = null + + +func analyze(statements: Array) -> void: + if not statements is Array: + statements = [statements] + + for statement in statements: + if statement.get_event_name().to_lower() == EXIT_SCENE_EVENT_NAME: + _execute(statement) + + _add_warning_messages() + + break + + +func _add_warning_messages() -> void: + var messages: Array[String] = [] + + if not _has_change_scene_command: + messages.append(BULLET_CHARACTER + CHANGE_SCENE_MISSING_MESSAGE) + + if _accept_input_disable_missing: + messages.append(BULLET_CHARACTER + MISSING_ACCEPT_INPUT_MESSAGE) + + if _has_commands_after_change_scene_command: + messages.append(BULLET_CHARACTER + COMMANDS_AFTER_CHANGE_SCENE_MESSAGE) + + if not messages.is_empty(): + _rich_messages.append(ESCSafeLogging.log_warn.bind(self, "\n".join(messages))) + + +func _execute_block(statements: Array, env: ESCEnvironment): + var previous_env = _environment + var ret = null + + _environment = env + + for stmt in statements: + ret = _execute(stmt) + + _environment = previous_env + + return ret + + +# Visitor overriding +func visit_call_expr(expr: ESCGrammarExprs.Call): + var callee = _evaluate(expr.get_callee()) + + var args: Array = [] + + for arg in expr.get_arguments(): + arg = _evaluate(arg) + + # "Adapter" for current ESC commands since they don't currently take + # ESCObject's (or ESCRoom's) as arguments. + if arg is ESCObject or arg is ESCRoom: + arg = arg.global_id + + args.append(arg) + + var command_name: String = _get_command_name(callee) + + _has_commands_after_change_scene_command = _has_change_scene_command_in_scope() + + if command_name == CHANGE_SCENE_COMMAND_NAME: + _has_change_scene_command = true + + _change_scene_token = expr.get_paren_token() + _environment.define(_change_scene_token.get_lexeme(), true) + + # First arg is always the path of the scene to transition to. + # Second arg determines whether the transition is automatic, and defaults to true + if args.size() > 1: + var is_auto_transition = args[1] as bool + + if is_auto_transition != null and not is_auto_transition: + _check_for_missing_accept_input_disable() + elif command_name == TRANSITION_COMMAND_NAME: + _check_for_missing_accept_input_disable() + elif command_name == ACCEPT_INPUT_COMMAND_NAME: + var first_arg = args[0] as String + + if first_arg != null and first_arg.to_lower() in ACCEPT_INPUT_DISABLE_ARGS: + _accept_input_token = expr.get_paren_token() + _environment.define(_accept_input_token.get_lexeme(), true) + + return ESCExecution.RC_OK + + +# as a key in the environment/scope to serve as a marker to check concerning whether `accept_input` +# has previously been called with the appropriate argument +func _check_for_missing_accept_input_disable() -> void: + if _accept_input_token == null: + _accept_input_disable_missing = true + return + + _accept_input_disable_missing = \ + not _environment.is_valid_key(_accept_input_token) \ + or not _environment.get_value(_accept_input_token) + + +func _has_change_scene_command_in_scope() -> bool: + if _change_scene_token == null: + return false + + return \ + _environment.is_valid_key(_change_scene_token) \ + and _environment.get_value(_change_scene_token) + + +func _get_command_name(callee) -> String: + # if we don't have a command to run, check against any built-in functions and, + # if one is found and is executed, we're done + if callee is ESCBaseCommand: + return callee.get_command_name() + + return callee diff --git a/addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd.uid b/addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd.uid new file mode 100644 index 0000000..902e168 --- /dev/null +++ b/addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd.uid @@ -0,0 +1 @@ +uid://0jmaf8d0mjpe diff --git a/addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd b/addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd new file mode 100644 index 0000000..874ffa1 --- /dev/null +++ b/addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd @@ -0,0 +1,387 @@ +extends RefCounted +class_name ESCScriptAnalyzer + + +const CURRENT_PLAYER_KEYWORD = "CURRENT_PLAYER" + + +var _rich_messages: Array[Callable] = [] + +var _globals: ESCEnvironment +var _environment: ESCEnvironment = _globals + +var _locals: Dictionary = {} + +var _builtin_functions: Array = [ + "print" +] + + +# This must be implemented in child class. +func analyze(statements: Array) -> void: + pass + + +func print_messages() -> void: + for print_method in _rich_messages: + print_method.call() + + +func _init(): + var globals: Dictionary = ESCCompiler.load_globals() + + _globals = ESCEnvironment.new() + + for callable in ESCCompiler.load_commands(): + _globals.define(callable.get_command_name(), callable) + + for key in ESCCompiler.load_globals().keys(): + _globals.define(key, globals[key]) + + +# Visitor implementations +func visit_block_stmt(stmt: ESCGrammarStmts.Block): + var env: ESCEnvironment = ESCEnvironment.new() + env.init(_environment) + + return _execute_block(stmt.get_statements(), env) + + +func visit_event_stmt(stmt: ESCGrammarStmts.Event): + _execute(stmt.get_body()) + + +func visit_expression_stmt(stmt: ESCGrammarStmts.ESCExpression): + return _evaluate(stmt.get_expression()) + + +func visit_call_expr(expr: ESCGrammarExprs.Call): + var callee = _evaluate(expr.get_callee()) + + var args: Array = [] + + for arg in expr.get_arguments(): + arg = _evaluate(arg) + + # "Adapter" for current ESC commands since they don't currently take + # ESCObject's (or ESCRoom's) as arguments. + if arg is ESCObject or arg is ESCRoom: + arg = arg.global_id + + args.append(arg) + + # if we don't have a command to run, check against any built-in functions and, + # if one is found and is executed, we're done + if not callee is ESCBaseCommand: + if not callee in _builtin_functions: + return 0 + else: + return _handle_builtin_function(callee, args) + + return ESCExecution.RC_OK + + +func _handle_builtin_function(fn_name: String, args: Array) -> int: + var rc = ESCExecution.RC_ERROR + + match fn_name: + 'print': + if args.size() > 1: + ESCSafeLogging.log_warn( + self, + "'print' only takes one argument" + ) + + rc = ESCExecution.RC_OK + + return rc + + +func visit_if_stmt(stmt: ESCGrammarStmts.If): + _execute(stmt.get_then_branch()) + + for branch in stmt.get_elif_branches(): + _execute(branch) + + if stmt.get_else_branch(): + _execute(stmt.get_else_branch()) + + return null + + +func visit_while_stmt(stmt: ESCGrammarStmts.While): + _execute(stmt.get_body()) + + return null + + +func visit_pass_stmt(stmt: ESCGrammarStmts.Pass): + pass + + +func visit_stop_stmt(stmt: ESCGrammarStmts.Stop): + return stmt + + +func visit_var_stmt(stmt: ESCGrammarStmts.Var): + var value = null + + if stmt.get_initializer(): + value = _evaluate(stmt.get_initializer()) + + _environment.define(stmt.get_name().get_lexeme(), value) + return null + + +func visit_global_stmt(stmt: ESCGrammarStmts.Global): + var value = null + + if stmt.get_initializer(): + value = _evaluate(stmt.get_initializer()) + + # Only define the global if we haven't already done so; otherwise, just + # ignore it + if not _globals.get_values().has(stmt.get_name().get_lexeme()): + _globals.define(stmt.get_name().get_lexeme(), value) + + return null + + +func visit_dialog_stmt(stmt: ESCGrammarStmts.Dialog): + return null + + +func visit_dialog_option_stmt(stmt: ESCGrammarStmts.DialogOption): + pass + + +func visit_break_stmt(stmt: ESCGrammarStmts.Break): + return stmt + + +func visit_done_stmt(stmt: ESCGrammarStmts.Done): + return stmt + + +func visit_assign_expr(expr: ESCGrammarExprs.Assign): + var value = _evaluate(expr.get_value()) + + var distance: int = _locals.get(expr, -1) + + return value + + +func visit_in_inventory_expr(expr: ESCGrammarExprs.InInventory): + var arg = _evaluate(expr.get_identifier()) + + if arg is ESCObject: + arg = arg.global_id + + return null + + +func visit_is_expr(expr: ESCGrammarExprs.Is): + var arg = _evaluate(expr.get_identifier()) + + return true + + +func visit_binary_expr(expr: ESCGrammarExprs.Binary): + var left_part = _evaluate(expr.get_left()) + var right_part = _evaluate(expr.get_right()) + + match expr.get_operator().get_type(): + ESCTokenType.TokenType.EQUAL_EQUAL: + return _is_equal(left_part, right_part) + ESCTokenType.TokenType.BANG_EQUAL: + return not _is_equal(left_part, right_part) + ESCTokenType.TokenType.GREATER: + var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + return left_part > right_part + ESCTokenType.TokenType.GREATER_EQUAL: + var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + return left_part >= right_part + ESCTokenType.TokenType.LESS: + var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + return left_part < right_part + ESCTokenType.TokenType.LESS_EQUAL: + var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + return left_part <= right_part + ESCTokenType.TokenType.MINUS: + var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + return left_part - right_part + ESCTokenType.TokenType.PLUS: + var check = _check_are_numbers(left_part, expr.get_operator(), right_part, false) + + if check: + return left_part + right_part + + check = _check_at_least_one_string(left_part, right_part) + + if check: + return str(left_part) + str(right_part) + + ESCSafeLogging.log_warn( + self, + "%s: Operands must be numbers or strings." % expr.get_operator().get_lexeme() + ) + ESCTokenType.TokenType.SLASH: + var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + return left_part / right_part + ESCTokenType.TokenType.STAR: + var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + return left_part * right_part + + return null + + +func visit_unary_expr(expr: ESCGrammarExprs.Unary): + var right_part = _evaluate(expr.get_right()) + + match expr.get_operator().get_type(): + ESCTokenType.TokenType.BANG, ESCTokenType.TokenType.NOT: + return not _is_truthy(right_part) + ESCTokenType.TokenType.MINUS: + var check = _check_is_number(right_part, expr.get_operator()) + return -right_part + + return null + + +func visit_variable_expr(expr: ESCGrammarExprs.Variable): + if expr.get_name().get_lexeme().begins_with("$"): + return _look_up_object(expr.get_name()) + + if expr.get_name().get_lexeme() in _builtin_functions: + return expr.get_name().get_lexeme() + + return look_up_variable(expr.get_name(), expr) + + +func visit_literal_expr(expr: ESCGrammarExprs.Literal): + return expr.get_value() + + +func visit_logical_expr(expr: ESCGrammarExprs.Logical): + var left = _evaluate(expr.get_left()) + + if expr.get_operator().get_type() == ESCTokenType.TokenType.OR: + if _is_truthy(left): + return left + else: + if not _is_truthy(left): + return left + + return _evaluate(expr.get_right()) + + +func visit_grouping_expr(expr: ESCGrammarExprs.Grouping): + return _evaluate(expr.get_expression()) + + +func resolve(expr: ESCGrammarExpr, depth: int): + _locals[expr] = depth + + +# Private methods +func _look_up_object(name: ESCToken): + var global_id: String = name.get_lexeme().substr(1) + + if global_id.to_upper() == CURRENT_PLAYER_KEYWORD: + pass + + return _look_up_object_by_global_id(global_id) + + +# We have no way of knowing what the value will actually be since this is called during static +# analysis. As such, we'll just return null. +func _look_up_object_by_global_id(global_id: String): + return null + + +# We don't care about the values of variables for static analysis purposes. +func look_up_variable(name: ESCToken, expr: ESCGrammarExpr): + var distance: int = _locals[expr] if _locals.has(expr) else -1 + + if distance == -1: + return _globals.get_value(name) + else: + return _environment.get_at(distance, name.get_lexeme()) + + +func _evaluate(expr: ESCGrammarExpr): + var ret = expr.accept(self) + + # TODO: Error handling + return ret + + +func _execute(stmt: ESCGrammarStmt): + var ret = stmt.accept(self) + + # TODO: Error handling + return ret + + +func _execute_block(statements: Array, env: ESCEnvironment): + var previous_env = _environment + var ret = null + + _environment = env + + for stmt in statements: + ret = _execute(stmt) + + _environment = previous_env + + return ret + + +func _is_truthy(value) -> bool: + if value == null: + return false + + if typeof(value) in [TYPE_INT, TYPE_FLOAT, TYPE_STRING, TYPE_BOOL]: + return bool(value) == true + + return false + + +func _is_equal(left_part, right_part) -> bool: + if not left_part and not right_part: + return true + + if not left_part: + return false + + return left_part == right_part + + +func _check_are_numbers(value_1, operator: ESCToken, value_2, strict: bool = true): + if typeof(value_1) in [TYPE_INT, TYPE_FLOAT] and typeof(value_2) in [TYPE_INT, TYPE_FLOAT]: + return true + + if strict: + ESCSafeLogging.log_warn( + self, + "%s: Operands must be numbers." % operator.get_lexeme() + ) + + return false + + +func _check_is_number(value, operator: ESCToken, strict: bool = true): + if typeof(value) in [TYPE_INT, TYPE_FLOAT]: + return true + + if strict: + ESCSafeLogging.log_warn( + self, + "%s: Operand must be number." % operator.get_lexeme() + ) + + return false + + +func _check_at_least_one_string(value_1, value_2): + return typeof(value_1) == TYPE_STRING || typeof(value_2) == TYPE_STRING diff --git a/addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd.uid b/addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd.uid new file mode 100644 index 0000000..4ee126f --- /dev/null +++ b/addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd.uid @@ -0,0 +1 @@ +uid://cv8ptpeiod6g2 diff --git a/addons/escoria-core/tools/script_analysis/esc_ashes_analyzer.gd b/addons/escoria-core/tools/script_analysis/esc_ashes_analyzer.gd new file mode 100644 index 0000000..2447a87 --- /dev/null +++ b/addons/escoria-core/tools/script_analysis/esc_ashes_analyzer.gd @@ -0,0 +1,52 @@ +@tool +extends Node +class_name ESCAshesAnalyzer + + +# TODO: Update this when we move from the .esc extension for Escoria scripts to .ash +const FILE_EXTENSION_ASHES = "esc" +const DIRECTORIES_TO_EXCLUDE = ["addons"] +const BASE_PROJECT_DIR = "res://" + + +var _compiler: ESCCompiler = ESCCompiler.new() + + +func analyze() -> void: + var files: Array[String] = _get_script_files_recursive(BASE_PROJECT_DIR) + + files.sort() + + for file in files: + _compiler.load_esc_file(file) + + +func _get_script_files_recursive(path: String, files: Array[String] = []) -> Array[String]: + var dir = DirAccess.open(path) + + if dir: + dir.list_dir_begin() + else: + ESCSafeLogging.log_warn(self, "Unable to open '%s'." % path) + return files + + var filename: String = dir.get_next() + + while not filename.is_empty(): + var filename_with_path: String = _make_full_path(dir.get_current_dir(), filename) + + if dir.current_is_dir() and not filename in DIRECTORIES_TO_EXCLUDE: + files = _get_script_files_recursive(filename_with_path, files) + else: + if filename.get_extension() == FILE_EXTENSION_ASHES: + files.append(filename_with_path) + + filename = dir.get_next() + + return files + + +func _make_full_path(directory: String, filename: String) -> String: + var separator: String = "" if directory.ends_with("/") else "/" + + return directory + separator + filename diff --git a/addons/escoria-core/tools/script_analysis/esc_ashes_analyzer.gd.uid b/addons/escoria-core/tools/script_analysis/esc_ashes_analyzer.gd.uid new file mode 100644 index 0000000..207977b --- /dev/null +++ b/addons/escoria-core/tools/script_analysis/esc_ashes_analyzer.gd.uid @@ -0,0 +1 @@ +uid://cs8hmitl4q8ec diff --git a/addons/escoria-core/tools/script_analysis/esc_static_analyzers.gd b/addons/escoria-core/tools/script_analysis/esc_static_analyzers.gd new file mode 100644 index 0000000..53aa7a9 --- /dev/null +++ b/addons/escoria-core/tools/script_analysis/esc_static_analyzers.gd @@ -0,0 +1,19 @@ +extends RefCounted +class_name ESCStaticAnalyzers + + +var _analyzers = [ + ESCExitSceneScriptAnalyzer.new() +] + +var _parsed_statements = [] + + +func _init(parsed_statements: Array) -> void: + _parsed_statements = parsed_statements + + +func run() -> void: + for analyzer in _analyzers: + analyzer.analyze(_parsed_statements) + analyzer.print_messages() diff --git a/addons/escoria-core/tools/script_analysis/esc_static_analyzers.gd.uid b/addons/escoria-core/tools/script_analysis/esc_static_analyzers.gd.uid new file mode 100644 index 0000000..edbcd9e --- /dev/null +++ b/addons/escoria-core/tools/script_analysis/esc_static_analyzers.gd.uid @@ -0,0 +1 @@ +uid://djdivn8o6k6aq -- cgit v1.3