Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/debug preproc #396

Merged
merged 3 commits into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion fortls/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))

Expand Down
144 changes: 99 additions & 45 deletions fortls/debug.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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):
Expand All @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions fortls/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down