note description: "Gobo Eiffel Documentation Format" copyright: "Copyright (c) 2017-2021, Eric Bezault and others" license: "MIT License" date: "$Date$" revision: "$Revision$" deferred class GEDOC_FORMAT inherit ANY KL_SHARED_FILE_SYSTEM export {NONE} all end KL_SHARED_STANDARD_FILES export {NONE} all end KL_SHARED_EXECUTION_ENVIRONMENT export {NONE} all end UC_SHARED_STRING_EQUALITY_TESTER export {NONE} all end UT_SHARED_ISE_VERSIONS export {NONE} all end ET_SHARED_TOKEN_CONSTANTS export {NONE} all end KL_IMPORTED_STRING_ROUTINES export {NONE} all end feature {NONE} -- Initialization make (a_input_filename: STRING; a_system_processor: like system_processor) -- Create a new documentation format with `a_input_filename'. require a_input_filename_not_void: a_input_filename /= Void a_system_processor_not_void: a_system_processor /= Void do input_filename := a_input_filename system_processor := a_system_processor ise_version := ise_latest input_classes := tokens.empty_classes create class_name_buffer.make (80) create universe_name_buffer.make (80) create filename_buffer.make (200) create concat_buffer.make (100) create interactive_mutex.make ensure input_filename_set: input_filename = a_input_filename system_processor_set: system_processor = a_system_processor end make_from_format (a_format: like Current; a_system_processor: like system_processor) -- Create a new documentation format using the settings of `a_format'. require a_format_not_void: a_format /= Void a_system_processor_not_void: a_system_processor /= Void do make (a_format.input_filename, a_system_processor) set_target_name (a_format.target_name) set_override_settings (a_format.override_settings) set_override_capabilities (a_format.override_capabilities) set_override_variables (a_format.override_variables) set_ise_version (a_format.ise_version) set_ecf_version (a_format.ecf_version) set_class_filters (a_format.class_filters) set_output_directory (a_format.output_directory) set_library_prefix_flag (a_format.library_prefix_flag) set_force_flag (a_format.force_flag) set_interactive_flag (a_format.interactive_flag) set_verbose_flag (a_format.verbose_flag) set_benchmark_flag (a_format.benchmark_flag) set_nested_benchmark_flag (a_format.nested_benchmark_flag) set_metrics_flag (a_format.metrics_flag) set_silent_flag (a_format.silent_flag) last_system := a_format.last_system input_classes := a_format.input_classes ensure input_filename_set: input_filename = a_format.input_filename system_processor_set: system_processor = a_system_processor target_name_set: target_name = a_format.target_name override_settings_set: override_settings = a_format.override_settings override_capabilities_set: override_capabilities = a_format.override_capabilities override_variables_set: override_variables = a_format.override_variables ise_version_set: ise_version = a_format.ise_version ecf_version_set: ecf_version = a_format.ecf_version class_fiters_set: class_filters = a_format.class_filters output_directory_set: output_directory = a_format.output_directory library_prefix_flag_set: library_prefix_flag = a_format.library_prefix_flag force_flag_set: force_flag = a_format.force_flag interactive_flag_set: interactive_flag = a_format.interactive_flag verbose_flag_set: verbose_flag = a_format.verbose_flag benchmark_flag_set: benchmark_flag = a_format.benchmark_flag metrics_flag: metrics_flag = a_format.metrics_flag silent_flag_set: silent_flag = a_format.silent_flag last_system_set: last_system = a_format.last_system input_classes_set: input_classes = a_format.input_classes end feature -- Execution execute -- Execute documentation format. local dt1: detachable DT_DATE_TIME do system_processor.set_benchmark_shown (benchmark_flag and not silent_flag) dt1 := system_processor.benchmark_start_time parse_input_file (input_filename) if not has_error and attached last_system as l_last_system then prepare_system (l_last_system) if not has_error then build_input_classes (l_last_system) end if not has_error then process_system (l_last_system) end if system_processor.error_handler.has_error then has_error := True end end system_processor.record_end_time (dt1, "Total Time") end feature -- Access input_filename: STRING -- Name of the input file. -- It can be an ECF file or an Eiffel class file. target_name: detachable STRING -- Name of target to be used in ECF file. -- Use last target in ECF file if not specified. override_settings: detachable ET_ECF_SETTINGS -- Settings overriding those specified for the selected ECF target override_capabilities: detachable ET_ECF_CAPABILITIES -- Capabilities overriding those specified for the selected ECF target override_variables: detachable ET_ECF_VARIABLES -- Variables overriding those specified for the selected ECF target ise_version: UT_VERSION -- ISE version, whose semantics should be -- used by the Eiffel code analysis process ecf_version: detachable UT_VERSION -- ECF version to be used when converting ECF files class_filters: detachable DS_ARRAYED_LIST [LX_DFA_WILDCARD] -- Name of classes to be processed output_directory: detachable STRING -- Directory for the generated files. -- Use directory containing the given class if Void. library_prefix_flag: BOOLEAN -- Should a subfolder with library name of the given class -- be added to the output directory? force_flag: BOOLEAN -- Should existing files be overwritten without asking? interactive_flag: BOOLEAN -- Should ask before overwriting a file, unless `force_flag' is True? verbose_flag: BOOLEAN -- Should detailed informative messages be displayed? benchmark_flag: BOOLEAN -- Should benchmark information be displayed? nested_benchmark_flag: BOOLEAN -- Should nested benchmark information be displayed? metrics_flag: BOOLEAN -- Should metrics information be displayed? silent_flag: BOOLEAN -- Should no informative messages be displayed? system_processor: ET_SYSTEM_PROCESSOR -- System processor currently used feature -- Setting set_target_name (a_target_name: like target_name) -- Set `target_name' to `a_target_name'. do target_name := a_target_name ensure target_name_set: target_name = a_target_name end set_override_settings (a_settings: like override_settings) -- Set `override_settings' to `a_settings'. do override_settings := a_settings ensure override_settings_set: override_settings = a_settings end set_override_capabilities (a_capabilities: like override_capabilities) -- Set `override_capabilities' to `a_capabilities'. do override_capabilities := a_capabilities ensure override_capabilities_set: override_capabilities = a_capabilities end set_override_variables (a_variables: like override_variables) -- Set `override_variables' to `a_variables'. do override_variables := a_variables ensure override_variables_set: override_variables = a_variables end set_ise_version (a_version: like ise_version) -- Set `ise_version' to `a_version'. require a_version_not_void: a_version /= Void do ise_version := a_version ensure ise_version_set: ise_version = a_version end set_ecf_version (a_version: like ecf_version) -- Set `ecf_version' to `a_version'. require a_version_not_void: a_version /= Void do ecf_version := a_version ensure ecf_version_set: ecf_version = a_version end set_class_filters (a_class_filters: like class_filters) -- Set `class_filters' to `a_class_filters'. require a_class_filters_compiled: a_class_filters /= Void implies a_class_filters.for_all (agent {LX_DFA_WILDCARD}.is_compiled) do class_filters := a_class_filters ensure class_fiters_set: class_filters = a_class_filters end set_output_directory (a_directory: like output_directory) -- Set `output_directory' to `a_directory'. do output_directory := a_directory ensure output_directory_set: output_directory = a_directory end set_library_prefix_flag (b: BOOLEAN) -- Set `library_prefix_flag' to `b'. do library_prefix_flag := b ensure library_prefix_flag_set: library_prefix_flag = b end set_force_flag (b: BOOLEAN) -- Set `force_flag' to `b'. do force_flag := b ensure force_flag_set: force_flag = b end set_interactive_flag (b: BOOLEAN) -- Set `interactive_flag' to `b'. do interactive_flag := b ensure interactive_flag_set: interactive_flag = b end set_verbose_flag (b: BOOLEAN) -- Set `verbose_flag' to `b'. do verbose_flag := b ensure verbose_flag_set: verbose_flag = b end set_benchmark_flag (b: BOOLEAN) -- Set `benchmark_flag' to `b'. do benchmark_flag := b ensure benchmark_flag_set: benchmark_flag = b end set_nested_benchmark_flag (b: BOOLEAN) -- Set `nested_benchmark_flag' to `b'. do nested_benchmark_flag := b ensure nested_benchmark_flag_set: nested_benchmark_flag = b end set_metrics_flag (b: BOOLEAN) -- Set `metrics_flag' to `b'. do metrics_flag := b ensure metrics_flag_set: metrics_flag = b end set_silent_flag (b: BOOLEAN) -- Set `silent_flag' to `b'. do silent_flag := b ensure silent_flag_set: silent_flag = b end feature -- Status report is_output_directory_required: BOOLEAN -- Is `output_directory' required by the current format? do Result := False end feature {NONE} -- Eiffel config file parsing parse_input_file (a_input_filename: STRING) -- Read `a_input_filename'. It can be -- an ECF file or an Eiffel class file. -- Put result in `last_system' if no error occurred. require a_input_filename_not_void: a_input_filename /= Void local l_file: KL_TEXT_INPUT_FILE nb: INTEGER do last_system := Void create l_file.make (a_input_filename) l_file.open_read if l_file.is_open_read then nb := a_input_filename.count if nb > 2 and then STRING_.same_string (a_input_filename.substring (nb - 1, nb), ".e") then parse_eiffel_file (l_file) else parse_ecf_file (l_file) end l_file.close else report_cannot_read_error (a_input_filename) end ensure has_error_if_void: last_system = Void implies has_error end parse_ecf_file (a_file: KI_CHARACTER_INPUT_STREAM) -- Read ECF file `a_file'. -- Put result in `last_system' if no error occurred. require a_file_not_void: a_file /= Void a_file_open_read: a_file.is_open_read local l_ecf_parser: ET_ECF_SYSTEM_PARSER l_ecf_error_handler: ET_ECF_ERROR_HANDLER do last_system := Void if silent_flag then create l_ecf_error_handler.make_null else create l_ecf_error_handler.make_standard end create l_ecf_parser.make (l_ecf_error_handler) l_ecf_parser.set_ise_version (ise_version) l_ecf_parser.set_override_settings (override_settings) l_ecf_parser.set_override_capabilities (override_capabilities) l_ecf_parser.set_override_variables (override_variables) l_ecf_parser.parse_file (a_file, target_name) if l_ecf_error_handler.has_error then has_error := True elseif not attached l_ecf_parser.last_system as l_last_system then report_no_system_found_error (a_file.name) else last_system := l_last_system end ensure has_error_if_void: last_system = Void implies has_error end parse_eiffel_file (a_file: KI_CHARACTER_INPUT_STREAM) -- Read Eiffel file `a_file' and keep track of it in -- newly created `last_system' if no error occurred. -- If there is no class filters, then add classes found -- in this file to `input_classes'. require a_file_not_void: a_file /= Void a_file_open_read: a_file.is_open_read local l_system: ET_SYSTEM l_cluster: ET_CLUSTER l_eiffel_preparser: ET_EIFFEL_PREPARSER l_input_classes: like input_classes do last_system := Void create l_system.make ("gedoc_system") create l_cluster.make ("gedoc_cluster", ".", l_system) if attached {ET_EIFFEL_PREPARSER} system_processor.eiffel_preparser as l_system_eiffel_preparser then l_eiffel_preparser := l_system_eiffel_preparser else create l_eiffel_preparser.make (system_processor) end l_eiffel_preparser.preparse_file (a_file.name, l_cluster) if l_eiffel_preparser.error_handler.has_error then has_error := True l_system := Void elseif not attached class_filters as l_class_filters or else l_class_filters.is_empty then create l_input_classes.make (1) input_classes := l_input_classes l_system.classes_do_if (agent l_input_classes.force_last, agent {ET_CLASS}.is_in_group (l_cluster)) if l_input_classes.is_empty then report_no_class_found_in_file_error (a_file.name) l_system := Void else input_classes := l_input_classes end end last_system := l_system ensure has_error_if_void: last_system = Void implies has_error end feature {NONE} -- Processing prepare_system (a_system: ET_SYSTEM) -- Prepare `a_system' before being processed. require a_system_not_void: a_system /= Void do system_processor.error_handler.set_ise system_processor.error_handler.set_verbose (verbose_flag) system_processor.set_benchmark_shown (benchmark_flag and not silent_flag) system_processor.set_nested_benchmark_shown (nested_benchmark_flag and benchmark_flag and not silent_flag) system_processor.set_metrics_shown (metrics_flag and not silent_flag) system_processor.set_ise_version (ise_version) system_processor.set_unknown_builtin_reported (False) a_system.set_unique_universe_names a_system.set_attachment_type_conformance_mode (False) a_system.set_target_type_attachment_mode (False) set_ast_factory if input_classes.is_empty then -- If `input_classes' is not empty, it means that we got them -- from an Eiffel file as input (`parse_eiffel_file'). In that -- case we do not run Degree 6 to get the list of classes. system_processor.compile_degree_6 (a_system) end end set_ast_factory -- Configure the AST factory as needed. local l_ast_factory: ET_DECORATED_AST_FACTORY do create l_ast_factory.make l_ast_factory.set_keep_all_comments (True) system_processor.set_ast_factory (l_ast_factory) end build_input_classes (a_system: ET_SYSTEM) -- Build `input_classes' using `class_filters' if not done yet. local l_last_wildcard: detachable READABLE_STRING_GENERAL l_input_classes: DS_HASH_SET [ET_CLASS] do if not input_classes.is_empty then -- Nothing to be done. elseif attached class_filters as l_class_filters and then not l_class_filters.is_empty then create l_input_classes.make (500) across l_class_filters as i_class_wildcard loop l_last_wildcard := i_class_wildcard.pattern a_system.add_classes_by_wildcarded_name_recursive (i_class_wildcard, l_input_classes) end l_input_classes.remove (a_system.none_type.base_class) if l_input_classes.is_empty and l_last_wildcard /= Void then report_no_class_matching_wildcard_error (l_last_wildcard) else create input_classes.make_from_linear (l_input_classes) end else create input_classes.make (a_system.class_count_recursive) a_system.classes_do_unless_recursive (agent input_classes.force_last, agent {ET_CLASS}.is_none) end end process_system (a_system: ET_SYSTEM) -- Process `input_classes' from `a_system'. require a_system_not_void: a_system /= Void deferred end feature {GEDOC_FORMAT} -- Processing last_system: detachable ET_SYSTEM -- Last system parsed, if any input_classes: DS_ARRAYED_LIST [ET_CLASS] -- Classes to be processed feature {NONE} -- Output class_output_directory (a_class: ET_CLASS): detachable STRING -- Where to generate files for `a_class'; -- Void if this directory could not be determined -- (e.g. `output_directory' was not specified and -- `a_class' is in a .NET assembly). require a_class_not_void: a_class /= Void do if attached output_directory as l_output_directory then Result := l_output_directory if library_prefix_flag then Result := file_system.pathname (Result, universe_lower_name (a_class.universe)) end elseif a_class.is_in_cluster then Result := a_class.group.full_pathname end end universe_output_directory (a_universe: ET_UNIVERSE): detachable STRING -- Where to generate files for `a_universe'. -- Void if this directory could not be determined. require a_universe_not_void: a_universe /= Void do if attached output_directory as l_output_directory then Result := l_output_directory if library_prefix_flag then Result := file_system.pathname (Result, universe_lower_name (a_universe)) end end end is_file_overwritable (a_filename: STRING): BOOLEAN -- Is it allowed to write to file `a_filename'? require a_filename_not_void: a_filename /= Void do if force_flag then Result := True elseif not file_system.file_exists (a_filename) then Result := True elseif interactive_flag then interactive_mutex.lock std.output.put_string ("File '") std.output.put_string (a_filename) std.output.put_line ("' already exists. Overwrite it (y/n)?") std.input.read_line Result := std.input.last_string.starts_with ("y") interactive_mutex.unlock end end create_class_output_directories (a_classes: DS_ARRAYED_LIST [ET_CLASS]) -- Create output directories for all classes in `a_classes'. -- It's useful to create them beforehand in a multi-threaded -- environment, to avoid having two threads trying to create -- the same directory at the same time. require a_classes_not_void: a_classes /= Void no_void_class: not a_classes.has_void local i, nb: INTEGER do if output_directory /= Void then nb := a_classes.count from i := 1 until i > nb loop if attached class_output_directory (a_classes.item (i)) as l_directory then file_system.recursive_create_directory (l_directory) end i := i + 1 end end end feature {NONE} -- Implementation new_output_file (a_filename: STRING): KL_BUFFERED_TEXT_OUTPUT_FILE -- File named `a_filename' -- -- Note that this routine always returns the same object. require a_filename_not_void: a_filename /= Void do if attached cached_output_file as l_output_file then Result := l_output_file Result.reset (a_filename) else create Result.make_with_buffer_size (a_filename, 1_000_000) cached_output_file := Result end ensure new_output_file_not_void: Result /= Void end cached_output_file: detachable KL_BUFFERED_TEXT_OUTPUT_FILE -- Cached file object used to write generated HTML text class_lower_name (a_class: ET_CLASS): STRING -- Name of `a_class' in lower-case -- -- Note that this routine always returns the same object. do Result := class_name_buffer Result.wipe_out Result.append_string (a_class.name.name) Result.to_lower ensure class_lower_name_not_void: Result /= Void definition: Result ~ a_class.lower_name end class_name_buffer: STRING -- Buffer for class names universe_lower_name (a_universe: ET_UNIVERSE): STRING -- Name of `a_universe' in lower-case -- -- Note that this routine always returns the same object. do Result := universe_name_buffer Result.wipe_out Result.append_string (a_universe.name) Result.to_lower ensure universe_lower_name_not_void: Result /= Void definition: Result ~ a_universe.lower_name end universe_name_buffer: STRING -- Buffer for universe names filename (a_dirname, a_pathname: STRING): STRING -- Pathname made up of relative pathname -- `a_pathname' in directory `a_dirname' -- (`a_dirname' and `a_pathname' should follow the pathname convention -- of `file_system.'. The result also follows this pathname convention.) -- -- Note that this routine always returns the same object. require a_dirname_not_void: a_dirname /= Void a_pathname_not_void: a_pathname /= Void a_pathname_relative: file_system.is_relative_pathname (a_pathname) do Result := filename_buffer Result.wipe_out file_system.append_pathname_to_string (a_dirname, a_pathname, Result) ensure pathname_not_void: Result /= Void same_relative: file_system.is_relative_pathname (Result) = file_system.is_relative_pathname (a_dirname) same_absolute: file_system.is_absolute_pathname (Result) = file_system.is_absolute_pathname (a_dirname) end filename_buffer: STRING -- Buffer for filenames concat (a_first_part, a_second_part: STRING): STRING -- String made up of `a_first_part' and `a_second_part' -- -- Note that this routine always returns the same object. require a_first_part_not_void: a_first_part /= Void a_second_part_not_void: a_second_part /= Void do Result := concat_buffer Result.wipe_out Result.append_string (a_first_part) Result.append_string (a_second_part) ensure concat_not_void: Result /= Void definition: Result ~ a_first_part + a_second_part end concat_buffer: STRING -- Buffer for concat feature -- Error handling error_handler: UT_ERROR_HANDLER -- Error handler do Result := system_processor.error_handler ensure error_handler_not_void: Result /= Void end has_error: BOOLEAN -- Have some errors been reported? report_error (a_error: UT_ERROR) -- Report `a_error'. require a_error_not_void: a_error /= Void do error_handler.report_error (a_error) has_error := True ensure has_error: has_error end report_cannot_read_error (a_filename: STRING) -- Report that `a_filename' cannot be -- opened in read mode. require a_filename_not_void: a_filename /= Void local l_error: UT_CANNOT_READ_FILE_ERROR do create l_error.make (a_filename) report_error (l_error) ensure has_error: has_error end report_cannot_write_error (a_filename: STRING) -- Report that `a_filename' cannot be -- opened in write mode. require a_filename_not_void: a_filename /= Void local l_error: UT_CANNOT_WRITE_TO_FILE_ERROR do create l_error.make (a_filename) report_error (l_error) ensure has_error: has_error end report_no_system_found_error (a_filename: STRING) -- Report that no Eiffel system was found in file `a_filename'. require a_filename_not_void: a_filename /= Void local l_error: UT_MESSAGE do create l_error.make ("No Eiffel system found in file '" + a_filename + "'.") report_error (l_error) ensure has_error: has_error end report_file_already_exists_error (a_filename: STRING) -- Report that `a_filename' already exists an dwill not be overwritten. require a_filename_not_void: a_filename /= Void local l_error: UT_MESSAGE do create l_error.make ("File '" + a_filename + "' already exists. Not overwritten.") report_error (l_error) ensure has_error: has_error end report_no_output_directory_for_class_error (a_class: ET_CLASS) -- Report that it was not possible to find an output directory -- for `a_class'. require a_class_not_void: a_class /= Void local l_error: UT_MESSAGE do create l_error.make ("Cannot find an output directory for class '" + a_class.upper_name + "'. Option --output must be specified.") report_error (l_error) ensure has_error: has_error end report_no_class_found_in_file_error (a_filename: STRING) -- Report that no class could be found in file `a_filename'. require a_filename_not_void: a_filename /= Void local l_error: UT_MESSAGE do create l_error.make ("No class was found in file '" + a_filename + "'.") report_error (l_error) ensure has_error: has_error end report_no_class_matching_wildcard_error (a_wildcard: READABLE_STRING_GENERAL) -- Report that no class matches `a_wildcard'. require a_wildcard_not_void: a_wildcard /= Void local l_error: UT_MESSAGE l_message: STRING do create l_message.make (50) l_message.append_string_general ("No class matches wildcard '") l_message.append_string_general (a_wildcard) l_message.append_string_general ("'.") create l_error.make (l_message) report_error (l_error) ensure has_error: has_error end report_target_not_found_error (a_target_name: STRING) -- Report that no ECF target `a_target_name' was found. require a_target_name_not_void: a_target_name /= Void local l_error: UT_MESSAGE do create l_error.make ("Target '" + a_target_name + "' not found.") report_error (l_error) ensure has_error: has_error end feature {NONE} -- Concurrency interactive_mutex: MUTEX -- Mutex to be used in interactive mode in a multi-threaded environment invariant system_processor_not_void: system_processor /= Void input_filename_not_void: input_filename /= Void ise_version_not_void: ise_version /= Void class_filters_compiled: attached class_filters as l_class_filters implies l_class_filters.for_all (agent {LX_DFA_WILDCARD}.is_compiled) input_classes_not_void: input_classes /= Void no_void_imput_class: not input_classes.has_void class_name_buffer_not_void: class_name_buffer /= Void universe_name_buffer_not_void: universe_name_buffer /= Void filename_buffer_not_void: filename_buffer /= Void concat_buffer_not_void: concat_buffer /= Void interactive_mutex_not_void: interactive_mutex /= Void end