feat(cli): add option to highlight diagnostics

This commit is contained in:
2026-06-01 11:57:57 +02:00
parent 29f691e38a
commit 86ad348b99
4 changed files with 56 additions and 4 deletions

3
.gitignore vendored
View File

@@ -5,4 +5,5 @@ venv
.venv .venv
*.pyc *.pyc
uv.lock uv.lock
.python-version .python-version
/out

View File

@@ -7,6 +7,7 @@ from typing import Generic, Optional, Protocol, TextIO, TypeVar
import midas.ast.midas as m import midas.ast.midas as m
import midas.ast.python as p import midas.ast.python as p
from midas.ast.location import Location from midas.ast.location import Location
from midas.checker.diagnostic import Diagnostic
H = TypeVar("H", bound="Highlighter", contravariant=True) H = TypeVar("H", bound="Highlighter", contravariant=True)
@@ -71,6 +72,7 @@ class Highlighter(ABC):
openings: list[str] = self.openings.get(pos, []) openings: list[str] = self.openings.get(pos, [])
line_buf += "".join(closings + openings) line_buf += "".join(closings + openings)
line_buf += char line_buf += char
line_buf += "".join(self.closings.get((lineno, len(line)), []))
line_buf += "</div></div>" line_buf += "</div></div>"
lines.append(" " + line_buf) lines.append(" " + line_buf)
lines.extend( lines.extend(
@@ -83,7 +85,7 @@ class Highlighter(ABC):
buf.write("\n".join(lines)) buf.write("\n".join(lines))
def wrap(self, node: Locatable, cls: str): def wrap(self, node: Locatable, cls: str, message: Optional[str] = None):
if node.location is None: if node.location is None:
return return
if node.location.end_lineno is None or node.location.end_col_offset is None: if node.location.end_lineno is None or node.location.end_col_offset is None:
@@ -95,6 +97,10 @@ class Highlighter(ABC):
) )
opening: str = f'<span class="{cls}" title="{cls}">' opening: str = f'<span class="{cls}" title="{cls}">'
closing: str = "</span>" closing: str = "</span>"
if message is not None:
opening = f'<span class="with-msg">{opening}'
closing = f'{closing}<span class="message">{message}</span></span>'
self.openings.setdefault(start_pos, []).append(opening) self.openings.setdefault(start_pos, []).append(opening)
self.closings.setdefault(end_pos, []).insert(0, closing) self.closings.setdefault(end_pos, []).insert(0, closing)
if start_pos[0] != end_pos[0]: if start_pos[0] != end_pos[0]:
@@ -260,3 +266,11 @@ class MidasHighlighter(Highlighter, m.Stmt.Visitor[None], m.Expr.Visitor[None]):
self.wrap(expr, "type") self.wrap(expr, "type")
if expr.template is not None: if expr.template is not None:
expr.template.accept(self) expr.template.accept(self)
class DiagnosticsHighlighter(Highlighter):
EXTRA_CSS_PATH: Optional[Path] = Path(__file__).parent / "hl_diagnostic.css"
def highlight(self, diagnostics: list[Diagnostic]):
for diagnostic in diagnostics:
self.wrap(diagnostic, str(diagnostic.type).lower(), diagnostic.message)

View File

@@ -0,0 +1,32 @@
span {
&.error {
--col: 255, 0, 0;
}
&.warning {
--col: 250, 160, 0;
}
&.info {
--col: 150, 190, 250;
}
&.with-msg {
position: relative;
&:not(:hover) {
.message {
display: none;
}
}
.message {
position: absolute;
top: calc(100% + 0.2em);
left: -.2em;
background-color: black;
color: white;
padding: 0.2em 0.4em;
border-radius: .2em;
z-index: 10;
width: 300%;
}
}
}

View File

@@ -13,7 +13,7 @@ from midas.ast.location import Location
from midas.ast.printer import MidasAstPrinter, PythonAstPrinter from midas.ast.printer import MidasAstPrinter, PythonAstPrinter
from midas.checker.checker import Checker from midas.checker.checker import Checker
from midas.checker.diagnostic import Diagnostic from midas.checker.diagnostic import Diagnostic
from midas.cli.highlighter import Highlighter, MidasHighlighter, PythonHighlighter from midas.cli.highlighter import DiagnosticsHighlighter, Highlighter, MidasHighlighter, PythonHighlighter
from midas.lexer.midas import MidasLexer from midas.lexer.midas import MidasLexer
from midas.lexer.token import Token, TokenType from midas.lexer.token import Token, TokenType
from midas.parser.midas import MidasParser from midas.parser.midas import MidasParser
@@ -28,8 +28,9 @@ def midas():
@midas.command() @midas.command()
@click.option("-l", "--highlight", type=click.File("w"))
@click.argument("file", type=click.File("r")) @click.argument("file", type=click.File("r"))
def compile(file: TextIO): def compile(highlight: Optional[TextIO], file: TextIO):
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
source: str = file.read() source: str = file.read()
tree: ast.Module = ast.parse(source, filename=file.name) tree: ast.Module = ast.parse(source, filename=file.name)
@@ -50,6 +51,10 @@ def compile(file: TextIO):
indent=4, indent=4,
) )
) )
if highlight is not None:
highlighter = DiagnosticsHighlighter(source)
highlighter.highlight(diagnostics)
highlighter.dump(highlight)
@midas.group() @midas.group()