diff --git a/docs/options.rst b/docs/options.rst index b0c4d228..e68c4a0d 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -219,6 +219,7 @@ Options for debugging language server - ``--debug_filepath DEBUG_FILEPATH`` File path for language server tests - ``--debug_rootpath DEBUG_ROOTPATH`` Root path for language server tests - ``--debug_parser`` Test source code parser on specified file +- ``--debug_preproc`` Test preprocessor on specified file - ``--debug_hover`` Test `textDocument/hover` request for specified file and position - ``--debug_rename RENAME_STRING`` Test `textDocument/rename` request for specified file and position - ``--debug_actions`` Test `textDocument/codeAction` request for specified file and position diff --git a/fortls/__init__.py b/fortls/__init__.py index 4405fd3f..e7e6c1f8 100644 --- a/fortls/__init__.py +++ b/fortls/__init__.py @@ -3,7 +3,13 @@ import sys from multiprocessing import freeze_support -from .debug import DebugError, debug_lsp, debug_parser, is_debug_mode +from .debug import ( + DebugError, + debug_lsp, + debug_parser, + debug_preprocessor, + is_debug_mode, +) from .interface import cli from .jsonrpc import JSONRPC2Connection, ReadWriter from .langserver import LangServer @@ -20,6 +26,9 @@ def main(): if args.debug_parser: debug_parser(args) + elif args.debug_preproc: + debug_preprocessor(args) + elif is_debug_mode(args): debug_lsp(args, vars(args)) diff --git a/fortls/debug.py b/fortls/debug.py index 59803806..4a3c8c97 100644 --- a/fortls/debug.py +++ b/fortls/debug.py @@ -1,14 +1,16 @@ from __future__ import annotations +import logging import os import pprint +import sys import json5 from .helper_functions import only_dirs, resolve_globs from .jsonrpc import JSONRPC2Connection, ReadWriter, path_from_uri from .langserver import LangServer -from .parsers.internal.parser import FortranFile +from .parsers.internal.parser import FortranFile, preprocess_file class DebugError(Exception): @@ -415,54 +417,11 @@ def debug_parser(args): The arguments parsed from the `ArgumentParser` """ - def locate_config(root: str) -> str | None: - default_conf_files = [args.config, ".fortlsrc", ".fortls.json5", ".fortls"] - present_conf_files = [ - os.path.isfile(os.path.join(root, f)) for f in default_conf_files - ] - if not any(present_conf_files): - return None - - # Load the first config file found - for f, present in zip(default_conf_files, present_conf_files): - if not present: - continue - config_path = os.path.join(root, f) - return config_path - - def read_config(root: str | None): - pp_suffixes = None - pp_defs = {} - include_dirs = set() - if root is None: - return pp_suffixes, pp_defs, include_dirs - - # Check for config files - config_path = locate_config(root) - print(f" Config file = {config_path}") - if config_path is None or not os.path.isfile(config_path): - return pp_suffixes, pp_defs, include_dirs - - try: - with open(config_path, encoding="utf-8") as fhandle: - config_dict = json5.load(fhandle) - pp_suffixes = config_dict.get("pp_suffixes", None) - pp_defs = config_dict.get("pp_defs", {}) - for path in config_dict.get("include_dirs", set()): - include_dirs.update(only_dirs(resolve_globs(path, root))) - - if isinstance(pp_defs, list): - pp_defs = {key: "" for key in pp_defs} - except ValueError as e: - print(f"Error {e} while parsing '{config_path}' settings file") - - return pp_suffixes, pp_defs, include_dirs - print("\nTesting parser") separator() ensure_file_accessible(args.debug_filepath) - pp_suffixes, pp_defs, include_dirs = read_config(args.debug_rootpath) + pp_suffixes, pp_defs, include_dirs = read_config(args.debug_rootpath, args.config) print(f' File = "{args.debug_filepath}"') file_obj = FortranFile(args.debug_filepath, pp_suffixes) @@ -482,6 +441,56 @@ def read_config(root: str | None): separator() +def debug_preprocessor(args): + """Debug the preprocessor of the Language Server + Triggered by `--debug_preprocessor` option. + + Parameters + ---------- + args : Namespace + The arguments parsed from the `ArgumentParser` + """ + + def sep_lvl2(heading: str): + print("\n" + "=" * 75 + f"\n{heading}\n" + "=" * 75) + + print("\nTesting preprocessor") + separator() + + logging.basicConfig(level=logging.DEBUG, stream=sys.stdout, format="%(message)s") + + file = args.debug_filepath + ensure_file_accessible(file) + with open(file, encoding="utf-8") as f: + lines = f.readlines() + + root = args.debug_rootpath if args.debug_rootpath else os.path.dirname(file) + _, pp_defs, include_dirs = read_config(root, args.config) + + sep_lvl2("Preprocessor Pass:") + output, skips, defines, defs = preprocess_file( + lines, file, pp_defs, include_dirs, debug=True + ) + + sep_lvl2("Preprocessor Skipped Lines:") + for line in skips: + print(f" {line}") + + sep_lvl2("Preprocessor Macros:") + for key, value in defs.items(): + print(f" {key} = {value}") + + sep_lvl2("Preprocessor Defines (#define):") + for line in defines: + print(f" {line}") + + sep_lvl2("Preprocessor Final Output:") + for line in output: + print(rf" {line.rstrip()}") + + separator() + + def ensure_file_accessible(filepath: str): """Ensure the file exists and is accessible, raising an error if not.""" if not os.path.isfile(filepath): @@ -500,6 +509,51 @@ def check_request_params(args, loc_needed=True): print(f" Char = {args.debug_char}\n") +def locate_config(root: str, input_config: str) -> str | None: + default_conf_files = [input_config, ".fortlsrc", ".fortls.json5", ".fortls"] + present_conf_files = [ + os.path.isfile(os.path.join(root, f)) for f in default_conf_files + ] + if not any(present_conf_files): + return None + + # Load the first config file found + for f, present in zip(default_conf_files, present_conf_files): + if not present: + continue + config_path = os.path.join(root, f) + return config_path + + +def read_config(root: str | None, input_config: str): + pp_suffixes = None + pp_defs = {} + include_dirs = set() + if root is None: + return pp_suffixes, pp_defs, include_dirs + + # Check for config files + config_path = locate_config(root, input_config) + print(f" Config file = {config_path}") + if config_path is None or not os.path.isfile(config_path): + return pp_suffixes, pp_defs, include_dirs + + try: + with open(config_path, encoding="utf-8") as fhandle: + config_dict = json5.load(fhandle) + pp_suffixes = config_dict.get("pp_suffixes", None) + pp_defs = config_dict.get("pp_defs", {}) + for path in config_dict.get("include_dirs", set()): + include_dirs.update(only_dirs(resolve_globs(path, root))) + + if isinstance(pp_defs, list): + pp_defs = {key: "" for key in pp_defs} + except ValueError as e: + print(f"Error {e} while parsing '{config_path}' settings file") + + return pp_suffixes, pp_defs, include_dirs + + def debug_generic(args, test_label, lsp_request, format_results, loc_needed=True): print(f'\nTesting "{test_label}" request:') check_request_params(args, loc_needed) diff --git a/fortls/interface.py b/fortls/interface.py index facc99a7..e16d0641 100644 --- a/fortls/interface.py +++ b/fortls/interface.py @@ -333,6 +333,11 @@ def hide_opt(help: str) -> str: action="store_true", help=hide_opt("Test source code parser on specified file"), ) + group.add_argument( + "--debug_preproc", + action="store_true", + help=hide_opt("Test source code preprocessor parser on specified file"), + ) group.add_argument( "--debug_hover", action="store_true",