Use cmakelang formatter (#52)

This commit is contained in:
Regen
2022-05-01 23:43:22 +09:00
committed by GitHub
parent f0cfa8b13f
commit 1fefcb4cba
8 changed files with 66 additions and 512 deletions

View File

@@ -12,12 +12,11 @@ Alpha Stage, work in progress.
## Features ## Features
- [x] Builtin command completion - [x] Builtin command completion
- [x] Documentation for commands and variables on hover - [x] Documentation for commands and variables on hover
- [x] Formatting - [x] Formatting (by [`cmake-format`](https://github.com/cheshirekow/cmake_format))
## Commands ## Commands
- `cmake-language-server`: LSP server - `cmake-language-server`: LSP server
- `python -m cmake_language_server.formatter`: CLI frontend for formatting
## Installation ## Installation

46
poetry.lock generated
View File

@@ -20,6 +20,21 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
[[package]]
name = "cmakelang"
version = "0.6.13"
description = "Language tools for cmake (format, lint, etc)"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
six = ">=1.13.0"
[package.extras]
yaml = ["pyyaml (>=5.3)"]
html-gen = ["jinja2 (==2.10.3)"]
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.4" version = "0.4.4"
@@ -142,11 +157,14 @@ ws = ["websockets (>=9.0.0,<10.0.0)"]
[[package]] [[package]]
name = "pyparsing" name = "pyparsing"
version = "2.4.7" version = "3.0.7"
description = "Python parsing module" description = "Python parsing module"
category = "main" category = "dev"
optional = false optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = ">=3.6"
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]] [[package]]
name = "pytest" name = "pytest"
@@ -197,6 +215,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies] [package.dependencies]
pytest = ">=2.7.0" pytest = ">=2.7.0"
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.10.2" version = "0.10.2"
@@ -240,7 +266,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.6" python-versions = "^3.6"
content-hash = "8b534309919acadb4c5b088a7a9aff76066ab695b1d39bf507b32db898450b76" content-hash = "1eaad6262e88c28de11d20ac8348f07b2a1b5afea514163be0965ac925588d91"
[metadata.files] [metadata.files]
atomicwrites = [ atomicwrites = [
@@ -251,6 +277,10 @@ attrs = [
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
] ]
cmakelang = [
{file = "cmakelang-0.6.13-py3-none-any.whl", hash = "sha256:764b9467195c7c36453d60a829f30229720d26c7dffd41cb516b99bd9c7daf4e"},
{file = "cmakelang-0.6.13.tar.gz", hash = "sha256:03982e87b00654d024d73ef972d9d9bb0e5726cdb6b8a424a15661fb6278e67f"},
]
colorama = [ colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
@@ -357,8 +387,8 @@ pygls = [
{file = "pygls-0.11.3.tar.gz", hash = "sha256:4d86fc854e6d6613cd42bf7511e9c6aac947fc8d62ff973a705570b036d969f2"}, {file = "pygls-0.11.3.tar.gz", hash = "sha256:4d86fc854e6d6613cd42bf7511e9c6aac947fc8d62ff973a705570b036d969f2"},
] ]
pyparsing = [ pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"},
] ]
pytest = [ pytest = [
{file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
@@ -372,6 +402,10 @@ pytest-datadir = [
{file = "pytest-datadir-1.3.1.tar.gz", hash = "sha256:d3af1e738df87515ee509d6135780f25a15959766d9c2b2dbe02bf4fb979cb18"}, {file = "pytest-datadir-1.3.1.tar.gz", hash = "sha256:d3af1e738df87515ee509d6135780f25a15959766d9c2b2dbe02bf4fb979cb18"},
{file = "pytest_datadir-1.3.1-py2.py3-none-any.whl", hash = "sha256:1847ed0efe0bc54cac40ab3fba6d651c2f03d18dd01f2a582979604d32e7621e"}, {file = "pytest_datadir-1.3.1-py2.py3-none-any.whl", hash = "sha256:1847ed0efe0bc54cac40ab3fba6d651c2f03d18dd01f2a582979604d32e7621e"},
] ]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
toml = [ toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},

View File

@@ -20,13 +20,13 @@ classifiers = [
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.6" python = "^3.6"
pygls = "^0.11" pygls = "^0.11"
pyparsing = "^2.4"
importlib-metadata = {version = "^4.8", python = "<3.8"} importlib-metadata = {version = "^4.8", python = "<3.8"}
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^6.2" pytest = "^6.2"
pytest-datadir = "^1.3" pytest-datadir = "^1.3"
pytest-cov = "^2.11" pytest-cov = "^2.11"
cmakelang = "^0.6.13"
[tool.poetry.scripts] [tool.poetry.scripts]
cmake-language-server = "cmake_language_server.server:main" cmake-language-server = "cmake_language_server.server:main"

View File

@@ -1,160 +0,0 @@
from typing import List, Optional
from .parser import TokenList
class Formatter(object):
indent: str
lower_identifier: bool
def __init__(self, indent: str = " ", lower_identifier: bool = True):
self.indent = indent
self.lower_identifier = lower_identifier
def format(self, tokens: TokenList) -> str:
cmds: List[str] = [""]
indent_level = 0
for token in tokens:
if isinstance(token, tuple):
raw_identifier = token[0]
identifier = raw_identifier.lower()
if identifier in (
"elseif",
"else",
"endif",
"endforeach",
"endwhile",
"endmacro",
"endfunction",
):
if indent_level > 0:
indent_level -= 1
cmds[-1] = self.indent * indent_level
cmds[-1] += identifier if self.lower_identifier else raw_identifier
args = self._format_args(token[1])
if len(args) < 2:
cmds[-1] += "(" + "".join(args) + ")"
else:
cmds[-1] += "(\n"
for arg in args:
cmds[-1] += self.indent * (indent_level + 1) + arg + "\n"
cmds[-1] += self.indent * indent_level + ")"
if identifier in (
"if",
"elseif",
"else",
"foreach",
"while",
"macro",
"function",
):
indent_level += 1
elif token == "\n":
cmds.append("")
elif token[0] == "#":
if cmds[-1]:
cmds[-1] += token
else:
cmds[-1] = self.indent * indent_level + token
elif cmds[-1]:
cmds[-1] += token
cmds = self._strip_line(cmds)
return "\n".join(cmds) + "\n"
def _format_args(self, args: List[str]) -> List[str]:
lines = [""]
for i in range(len(args)):
arg = args[i]
if arg[0] == "#":
lines[-1] += arg
elif arg[0] == "\n":
lines.append("")
elif arg.isspace():
if lines[-1]:
if i + 1 < len(args) and args[i + 1][0] == "#":
lines[-1] += arg
else:
lines[-1] += " "
else:
lines[-1] += arg
return self._strip_line(lines)
def _strip_line(self, lines: List[str]) -> List[str]:
"""Delete empty lines at the start/end of the input"""
ret: List[str] = []
for line in lines:
line = line.rstrip()
if line != "" or len(ret) > 0:
ret.append(line)
while ret and ret[-1] == "":
del ret[-1]
return ret
def main(argss: Optional[List[str]] = None) -> None:
import sys
from argparse import ArgumentParser
from difflib import unified_diff
from pathlib import Path
from . import __version__
from .parser import ListParser
parser = ArgumentParser(
description="Format CMake list files.",
epilog="""
If no arguments are specified, it formats the code from
standard input and writes the result to the standard output.""",
)
parser.add_argument("lists", type=Path, nargs="*", help="CMake list files")
group = parser.add_mutually_exclusive_group()
group.add_argument("-i", "--inplace", action="store_true", help="inplace edit")
group.add_argument("-d", "--diff", action="store_true", help="show diff")
parser.add_argument(
"--version", action="version", version=f"%(prog)s {__version__}"
)
args = parser.parse_args(argss)
if not args.lists and args.inplace:
print("error: cannot use -i when no arguments are specified.", file=sys.stderr)
return
if not args.lists:
args.lists.append(None)
list_parser = ListParser()
formatter = Formatter()
for listpath in args.lists:
if listpath is None:
listpath = "(stdin)"
content = sys.stdin.read()
else:
with listpath.open() as fp:
content = fp.read()
tokens, remain = list_parser.parse(content)
formatted = content if remain else formatter.format(tokens)
if args.inplace:
if not remain:
with listpath.open("w") as fp:
fp.write(formatted)
elif args.diff:
diff = unified_diff(
content.splitlines(True),
formatted.splitlines(True),
str(listpath),
str(listpath),
"(before formatting)",
"(after formatting)",
)
diffstr = "".join(diff)
print(diffstr, end="")
else:
print(formatted, end="")
if __name__ == "__main__":
main()

View File

@@ -1,70 +0,0 @@
from typing import List, Tuple, Union
import pyparsing as pp
CommandTokenType = Tuple[str, List[str]]
TokenType = Union[str, CommandTokenType]
TokenList = List[TokenType]
class ListParser(object):
_parser: pp.ParserElement
def __init__(self) -> None:
newline = "\n"
space_plus = pp.Regex("[ \t]+")
space_star = pp.Optional(space_plus)
quoted_element = pp.Regex(r'[^\\"]|\\[^A-Za-z0-9]|\\[trn]')
quoted_argument = pp.Combine('"' + pp.ZeroOrMore(quoted_element) + '"')
bracket_content = pp.Forward()
def action_bracket_open(tokens: pp.ParseResults) -> None:
nonlocal bracket_content
marker = "]" + "=" * (len(tokens[0]) - 2) + "]"
bracket_content <<= pp.SkipTo(marker, include=True)
bracket_open = pp.Regex(r"\[=*\[").setParseAction(action_bracket_open)
bracket_argument = pp.Combine(bracket_open + bracket_content)
unquoted_element = pp.Regex(r'[^\s()#"\\]|\\[^A-Za-z0-9]|\\[trn]')
unquoted_argument = pp.Combine(pp.OneOrMore(unquoted_element))
argument = bracket_argument | quoted_argument | unquoted_argument
line_comment = pp.Combine("#" + ~bracket_open + pp.SkipTo(pp.LineEnd()))
bracket_comment = pp.Combine("#" + bracket_argument)
line_ending = (
space_star
+ pp.ZeroOrMore(bracket_comment + space_star)
+ pp.Optional(line_comment)
+ (newline | pp.lineEnd)
)
identifier = pp.Word(pp.alphas + "_", pp.alphanums + "_")
arguments = pp.Forward()
(
arguments
<< pp.ZeroOrMore(
argument | line_ending | space_plus | "(" + arguments + ")"
).leaveWhitespace()
)
arguments = pp.Group(arguments)
PAREN_L, PAREN_R = map(pp.Suppress, "()")
command_invocation = (
identifier + space_star.suppress() + PAREN_L + arguments + PAREN_R
).setParseAction(lambda t: (t[0], t[1].asList()))
file_element = (
space_star + command_invocation + line_ending | line_ending
).leaveWhitespace()
file = pp.ZeroOrMore(file_element)
self._parser = file
def parse(self, liststr: str) -> Tuple[TokenList, str]:
for t, s, e in self._parser.scanString(liststr, maxMatches=1):
if s == 0:
return t.asList(), liststr[e:]
return [], liststr

View File

@@ -1,5 +1,7 @@
import logging import logging
import re import re
import shutil
import subprocess
from pathlib import Path from pathlib import Path
from typing import Any, Callable, List, Optional, Tuple from typing import Any, Callable, List, Optional, Tuple
@@ -32,20 +34,16 @@ from pygls.lsp.types import (
from pygls.server import LanguageServer from pygls.server import LanguageServer
from .api import API from .api import API
from .formatter import Formatter
from .parser import ListParser
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CMakeLanguageServer(LanguageServer): class CMakeLanguageServer(LanguageServer):
_parser: ListParser
_api: Optional[API] _api: Optional[API]
def __init__(self, *args: Any) -> None: def __init__(self, *args: Any) -> None:
super().__init__(*args) super().__init__(*args)
self._parser = ListParser()
self._api = None self._api = None
@self.feature(INITIALIZE) @self.feature(INITIALIZE)
@@ -151,26 +149,30 @@ class CMakeLanguageServer(LanguageServer):
return CompletionList(is_incomplete=False, items=items) return CompletionList(is_incomplete=False, items=items)
@self.feature(FORMATTING) if shutil.which("cmake-format") is not None:
def formatting(params: DocumentFormattingParams) -> Optional[List[TextEdit]]:
doc = self.workspace.get_document(params.text_document.uri)
content = doc.source
tokens, remain = self._parser.parse(content)
if remain:
self.show_message("CMake parser failed")
return None
formatted = Formatter().format(tokens) @self.feature(FORMATTING)
lines = content.count("\n") def formatting(
return [ params: DocumentFormattingParams,
TextEdit( ) -> Optional[List[TextEdit]]:
range=Range( doc = self.workspace.get_document(params.text_document.uri)
start=Position(line=0, character=0), content = doc.source
end=Position(line=lines + 1, character=0), formatted = subprocess.check_output(
), ["cmake-format", "-"],
new_text=formatted, cwd=str(Path(doc.path).parent),
input=content,
universal_newlines=True,
) )
] lines = content.count("\n")
return [
TextEdit(
range=Range(
start=Position(line=0, character=0),
end=Position(line=lines + 1, character=0),
),
new_text=formatted,
)
]
@self.feature(HOVER) @self.feature(HOVER)
def hover(params: TextDocumentPositionParams) -> Optional[Hover]: def hover(params: TextDocumentPositionParams) -> Optional[Hover]:

View File

@@ -1,168 +0,0 @@
import sys
from contextlib import contextmanager
from io import StringIO
from pathlib import Path
from typing import Callable, Iterator
from cmake_language_server.formatter import Formatter, main
from cmake_language_server.parser import ListParser
from pytest import CaptureFixture
def make_formatter_test(liststr: str, expect: str) -> Callable[[], None]:
def test() -> None:
tokens, remain = ListParser().parse(liststr)
actual = Formatter().format(tokens)
assert actual == expect
return test
test_command = make_formatter_test("a()", "a()\n")
test_command_tolower = make_formatter_test("A()", "a()\n")
test_remove_space = make_formatter_test(
"""
#a
b ( c ) # d
""",
"""\
#a
b(c) # d
""",
)
test_indent_if = make_formatter_test(
"""
if()
a() # a
else()
# b
b()
endif()
""",
"""\
if()
a() # a
else()
# b
b()
endif()
""",
)
test_indent_if_nested = make_formatter_test(
"""
if()
if()
a()
b()
endif()
endif()
""",
"""\
if()
if()
a()
b()
endif()
endif()
""",
)
test_argument = make_formatter_test("a( b c d)", "a(b c d)\n")
test_argument_multiline = make_formatter_test(
"""
if()
a(b c
d # e
f
# g
) # h
endif()
""",
"""\
if()
a(
b c
d # e
f
# g
) # h
endif()
""",
)
@contextmanager
def mock_stdin(buf: str) -> Iterator[None]:
stdin = sys.stdin
sys.stdin = StringIO(buf)
yield
sys.stdin = stdin
def test_main_stdin(capsys: CaptureFixture[str]) -> None:
with mock_stdin(" a()"):
main([])
captured = capsys.readouterr()
assert captured.out == "a()\n"
assert captured.err == ""
def test_main_stdin_diff(capsys: CaptureFixture[str]) -> None:
with mock_stdin(" a()"):
main(["-d"])
captured = capsys.readouterr()
assert "- a()" in captured.out
assert "+a()" in captured.out
assert captured.err == ""
def test_main_file_1(capsys: CaptureFixture[str], tmp_path: Path) -> None:
testfile1 = tmp_path / "list1.cmake"
with testfile1.open("w") as fp:
fp.write(" a()")
main([str(testfile1)])
captured = capsys.readouterr()
assert captured.out == "a()\n"
assert captured.err == ""
def test_main_file_2(capsys: CaptureFixture[str], tmp_path: Path) -> None:
testfile1 = tmp_path / "list1.cmake"
with testfile1.open("w") as fp:
fp.write(" a()")
testfile2 = tmp_path / "list2.cmake"
with testfile2.open("w") as fp:
fp.write(" b()")
main([str(testfile1), str(testfile2)])
captured = capsys.readouterr()
assert captured.out == "a()\nb()\n"
assert captured.err == ""
def test_main_inplace(capsys: CaptureFixture[str], tmp_path: Path) -> None:
testfile1 = tmp_path / "list1.cmake"
with testfile1.open("w") as fp:
fp.write(" a()")
main(["-i", str(testfile1)])
captured = capsys.readouterr()
assert captured.out == ""
assert captured.err == ""
with testfile1.open() as fp:
content = fp.read()
assert content == "a()\n"
def test_main_diff(capsys: CaptureFixture[str], tmp_path: Path) -> None:
testfile1 = tmp_path / "list1.cmake"
with testfile1.open("w") as fp:
fp.write(" a()")
main(["-d", str(testfile1)])
captured = capsys.readouterr()
assert str(testfile1) in captured.out
assert "- a()" in captured.out
assert "+a()" in captured.out
assert captured.err == ""

View File

@@ -1,83 +0,0 @@
from typing import Callable, List
from cmake_language_server.parser import ListParser, TokenType
def make_parser_test(
liststr: str, expect_token: List[TokenType], expect_remain: str = ""
) -> Callable[[], None]:
def test() -> None:
actual_token, actual_remain = ListParser().parse(liststr)
assert actual_token == expect_token
assert actual_remain == expect_remain
return test
test_command_no_args = make_parser_test("a()", [("a", [])])
test_command_space = make_parser_test(" a ()", [" ", ("a", [])])
test_command_arg = make_parser_test("a(b)", [("a", ["b"])])
test_command_arg_space = make_parser_test("a ( b )", [("a", ["b"])])
test_command_arg_escape = make_parser_test(r"a(\n\")", [("a", [r"\n\""])])
test_command_arg_paren = make_parser_test("a((b))", [("a", ["(", "b", ")"])])
test_command_arg_paren_paren = make_parser_test(
"a(((b)))", [("a", ["(", "(", "b", ")", ")"])]
)
test_command_arg_quote = make_parser_test(r'a("b\"")', [("a", [r'"b\""'])])
test_command_arg_quote_cont = make_parser_test('a("\\\n")', [("a", ['"\\\n"'])])
test_command_arg_quo_multiline = make_parser_test(
"""a("b
c
")""",
[("a", ['"b\nc\n"'])],
)
test_command_arg_bracket_0 = make_parser_test("a([[b]])", [("a", ["[[b]]"])])
test_command_arg_bracket_1 = make_parser_test("a([=[b]=])", [("a", ["[=[b]=]"])])
test_command_arg_space = make_parser_test("a ( b )", [("a", [" ", "b", " "])])
test_command_arg_multi = make_parser_test("a(b c)", [("a", ["b", " ", "c"])])
test_command_multielement = make_parser_test(
"""a(
b
c # c
)""",
[("a", ["\n", " ", "b", "\n", " ", "c", " ", "# c", "\n"])],
)
test_line_comment = make_parser_test("a() # b # c", [("a", []), " ", "# b # c"])
test_bracket_comment = make_parser_test("#[[a]]#[[b]]", ["#[[a]]", "#[[b]]"])
test_bracket_comment_nested = make_parser_test("#[=[[[a]]]=]", ["#[=[[[a]]]=]"])
test_bracket_comment_multiline = make_parser_test(
"#[[\na\nb\nc\n]]", ["#[[\na\nb\nc\n]]"]
)
test_if_block = make_parser_test(
"""if()
a()
else()
b()
endif()""",
[
("if", []),
"\n",
" ",
("a", []),
"\n",
("else", []),
"\n",
" ",
("b", []),
"\n",
("endif", []),
],
)
test_comment_multi_linecomment = make_parser_test(
"""a()# a
b() # b
c() # c""",
[("a", []), "# a", "\n", ("b", []), " ", "# b", "\n", ("c", []), " ", "# c"],
)
test_incomplete_id = make_parser_test("a", [], "a")
test_incomplete_command = make_parser_test("a(", [], "a(")
test_incomplete_id_after_command = make_parser_test("a()\nb", [("a", []), "\n"], "b")
test_incomplete_command_after_command = make_parser_test(
"a()\nb(c", [("a", []), "\n"], "b(c"
)