mirror of
https://github.com/isledecomp/isle.git
synced 2025-10-26 18:04:06 +00:00
(Discussion/Proposals) Consistency regarding annotations of header-implemented functions (#316)
* Open discussion * Move annotations of header-implemented functions back to `.h` files * Adjust `README.md` * Relocate annotation * linter * Comment markers in headers only, rename script, update github actions * type hint compat * Rename github action, better argparse for linter * Type hints, working test for byname ignore * Move annotation * CI rename and enable warnfail, enforce mode always on * Two step linting * or one step * continue on error * two jobs instead * Fixes --------- Co-authored-by: disinvite <disinvite@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
4dd0d60dec
commit
3b155bfe38
@@ -1 +1,2 @@
|
||||
from .parser import DecompParser
|
||||
from .linter import DecompLinter
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
# TODO: poorly chosen name, should be AlertType or AlertCode or something
|
||||
class ParserError(Enum):
|
||||
# WARN: Stub function exceeds some line number threshold
|
||||
UNLIKELY_STUB = 100
|
||||
@@ -29,6 +32,16 @@ class ParserError(Enum):
|
||||
# and the start of the function. We can ignore it, but the line shouldn't be there
|
||||
UNEXPECTED_BLANK_LINE = 107
|
||||
|
||||
# WARN: We called the finish() method for the parser but had not reached the starting
|
||||
# state of SEARCH
|
||||
UNEXPECTED_END_OF_FILE = 108
|
||||
|
||||
# WARN: We found a marker to be referenced by name outside of a header file.
|
||||
BYNAME_FUNCTION_IN_CPP = 109
|
||||
|
||||
# This code or higher is an error, not a warning
|
||||
DECOMP_ERROR_START = 200
|
||||
|
||||
# ERROR: We found a marker unexpectedly
|
||||
UNEXPECTED_MARKER = 200
|
||||
|
||||
@@ -39,3 +52,20 @@ class ParserError(Enum):
|
||||
|
||||
# ERROR: The line following a synthetic marker was not a comment
|
||||
BAD_SYNTHETIC = 202
|
||||
|
||||
# ERROR: This function offset comes before the previous offset from the same module
|
||||
# This hopefully gives some hint about which functions need to be rearranged.
|
||||
FUNCTION_OUT_OF_ORDER = 203
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParserAlert:
|
||||
code: ParserError
|
||||
line_number: int
|
||||
line: Optional[str] = None
|
||||
|
||||
def is_warning(self) -> bool:
|
||||
return self.code.value < ParserError.DECOMP_ERROR_START.value
|
||||
|
||||
def is_error(self) -> bool:
|
||||
return self.code.value >= ParserError.DECOMP_ERROR_START.value
|
||||
|
||||
99
tools/isledecomp/isledecomp/parser/linter.py
Normal file
99
tools/isledecomp/isledecomp/parser/linter.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from typing import List, Optional
|
||||
from .parser import DecompParser
|
||||
from .error import ParserAlert, ParserError
|
||||
|
||||
|
||||
def get_checkorder_filter(module):
|
||||
"""Return a filter function on implemented functions in the given module"""
|
||||
return lambda fun: fun.module == module and not fun.lookup_by_name
|
||||
|
||||
|
||||
class DecompLinter:
|
||||
def __init__(self) -> None:
|
||||
self.alerts: List[ParserAlert] = []
|
||||
self._parser = DecompParser()
|
||||
self._filename: str = ""
|
||||
self._module: Optional[str] = None
|
||||
|
||||
def reset(self):
|
||||
self.alerts = []
|
||||
self._parser.reset()
|
||||
self._filename = ""
|
||||
self._module = None
|
||||
|
||||
def file_is_header(self):
|
||||
return self._filename.lower().endswith(".h")
|
||||
|
||||
def _check_function_order(self):
|
||||
"""Rules:
|
||||
1. Only markers that are implemented in the file are considered. This means we
|
||||
only look at markers that are cross-referenced with cvdump output by their line
|
||||
number. Markers with the lookup_by_name flag set are ignored because we cannot
|
||||
directly influence their order.
|
||||
|
||||
2. Order should be considered for a single module only. If we have multiple
|
||||
markers for a single function (i.e. for LEGO1 functions linked statically to
|
||||
ISLE) then the virtual address space will be very different. If we don't check
|
||||
for one module only, we would incorrectly report that the file is out of order.
|
||||
"""
|
||||
|
||||
if self._module is None:
|
||||
return
|
||||
|
||||
checkorder_filter = get_checkorder_filter(self._module)
|
||||
last_offset = None
|
||||
for fun in filter(checkorder_filter, self._parser.functions):
|
||||
if last_offset is not None:
|
||||
if fun.offset < last_offset:
|
||||
self.alerts.append(
|
||||
ParserAlert(
|
||||
code=ParserError.FUNCTION_OUT_OF_ORDER,
|
||||
line_number=fun.line_number,
|
||||
)
|
||||
)
|
||||
|
||||
last_offset = fun.offset
|
||||
|
||||
def _check_offset_uniqueness(self):
|
||||
# TODO
|
||||
pass
|
||||
|
||||
def _check_byname_allowed(self):
|
||||
if self.file_is_header():
|
||||
return
|
||||
|
||||
for fun in self._parser.functions:
|
||||
if fun.lookup_by_name:
|
||||
self.alerts.append(
|
||||
ParserAlert(
|
||||
code=ParserError.BYNAME_FUNCTION_IN_CPP,
|
||||
line_number=fun.line_number,
|
||||
)
|
||||
)
|
||||
|
||||
def check_lines(self, lines, filename, module=None):
|
||||
"""`lines` is a generic iterable to allow for testing with a list of strings.
|
||||
We assume lines has the entire contents of the compilation unit."""
|
||||
|
||||
self.reset()
|
||||
self._filename = filename
|
||||
self._module = module
|
||||
|
||||
self._parser.read_lines(lines)
|
||||
|
||||
self._parser.finish()
|
||||
self.alerts = self._parser.alerts[::]
|
||||
|
||||
if self._module is not None:
|
||||
self._check_byname_allowed()
|
||||
self._check_offset_uniqueness()
|
||||
|
||||
if not self.file_is_header():
|
||||
self._check_function_order()
|
||||
|
||||
return len(self.alerts) == 0
|
||||
|
||||
def check_file(self, filename, module=None):
|
||||
"""Convenience method for decomplint cli tool"""
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
return self.check_lines(f, filename, module)
|
||||
@@ -6,12 +6,6 @@ class ParserNode:
|
||||
line_number: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParserAlert(ParserNode):
|
||||
code: int
|
||||
line: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParserSymbol(ParserNode):
|
||||
module: str
|
||||
|
||||
@@ -12,12 +12,11 @@ from .util import (
|
||||
remove_trailing_comment,
|
||||
)
|
||||
from .node import (
|
||||
ParserAlert,
|
||||
ParserFunction,
|
||||
ParserVariable,
|
||||
ParserVtable,
|
||||
)
|
||||
from .error import ParserError
|
||||
from .error import ParserAlert, ParserError
|
||||
|
||||
|
||||
class ReaderState(Enum):
|
||||
@@ -29,6 +28,7 @@ class ReaderState(Enum):
|
||||
IN_GLOBAL = 5
|
||||
IN_FUNC_GLOBAL = 6
|
||||
IN_VTABLE = 7
|
||||
DONE = 100
|
||||
|
||||
|
||||
def marker_is_stub(marker: DecompMarker) -> bool:
|
||||
@@ -56,7 +56,7 @@ def marker_is_vtable(marker: DecompMarker) -> bool:
|
||||
|
||||
|
||||
class MarkerDict:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.markers: dict = {}
|
||||
|
||||
def insert(self, marker: DecompMarker) -> bool:
|
||||
@@ -80,7 +80,7 @@ class DecompParser:
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
# Could combine output lists into a single list to get under the limit,
|
||||
# but not right now
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
# The lists to be populated as we parse
|
||||
self.functions: List[ParserFunction] = []
|
||||
self.vtables: List[ParserVtable] = []
|
||||
@@ -306,6 +306,9 @@ class DecompParser:
|
||||
self._syntax_warning(ParserError.BOGUS_MARKER)
|
||||
|
||||
def read_line(self, line: str):
|
||||
if self.state == ReaderState.DONE:
|
||||
return
|
||||
|
||||
self.last_line = line # TODO: Useful or hack for error reporting?
|
||||
self.line_number += 1
|
||||
|
||||
@@ -392,3 +395,9 @@ class DecompParser:
|
||||
def read_lines(self, lines: Iterable):
|
||||
for line in lines:
|
||||
self.read_line(line)
|
||||
|
||||
def finish(self):
|
||||
if self.state != ReaderState.SEARCH:
|
||||
self._syntax_warning(ParserError.UNEXPECTED_END_OF_FILE)
|
||||
|
||||
self.state = ReaderState.DONE
|
||||
|
||||
Reference in New Issue
Block a user