mirror of
https://github.com/mesonbuild/meson.git
synced 2026-11-06 05:16:42 +00:00
format: add suppression directives
`meson format` now leaves untouched the lines between `# meson-format off` and `# meson-format on` comments. When `# meson-format off` is not followed by `# meson-format on`, formatting is disabled until the end of the file. ```meson sources = files( 'a.c', 'b.c', 'c.c', 'd.c', ) ``` Fixes #15922.
This commit is contained in:
1 parent
d3d17647d2
commit
99fe4fef47
20 files changed
+435
-2
No files matched your search
@@ -506,6 +506,31 @@ or `--inline` arguments.
|
||||
*Since 1.10.0* When `--check-diff` is specified, instead of silently exiting
|
||||
with an error code, `meson format` will print a diff of the formatting changes.
|
||||
|
||||
#### Disabling formatting
|
||||
|
||||
*Since 1.13.0* Formatting can be disabled for part of a file using
|
||||
`# meson-format off` and `# meson-format on` comments. Lines between the two
|
||||
comments are left untouched by the formatter:
|
||||
|
||||
```meson
|
||||
# meson-format off
|
||||
sources = files(
|
||||
'a.c', 'b.c',
|
||||
'c.c', 'd.c',
|
||||
)
|
||||
# meson-format on
|
||||
```
|
||||
|
||||
Each directive must be alone on its own line, and must enclose complete
|
||||
statements: a directive following code on the same line, or inside
|
||||
parentheses, brackets or braces, is an error. When `# meson-format off` is not followed by `# meson-format on`,
|
||||
formatting stays disabled until the end of the file. Directives only apply to
|
||||
the file they appear in; with `--recursive`, files from `subdir` are formatted
|
||||
normally.
|
||||
|
||||
For compatibility with `muon fmt`, `# fmt: off` and `# fmt: on` are also
|
||||
recognized as directives.
|
||||
|
||||
|
||||
#### Differences with `muon fmt`
|
||||
|
||||
@@ -520,3 +545,7 @@ However, it has more features, and some differences:
|
||||
`meson format` also recognizes `max_line_length`, `end_of_line`,
|
||||
`insert_final_newline` and `tab_width` options.
|
||||
- `meson format` has many additional format rules (see option list above).
|
||||
- In addition to `# fmt: off` and `# fmt: on`, `meson format` recognizes
|
||||
`# meson-format off` and `# meson-format on` directives. Unlike `muon fmt`,
|
||||
`meson format` requires those directives to be on their own line and to
|
||||
enclose whole statements.
|
||||
@@ -0,0 +1,16 @@
|
||||
## `meson format` suppression directives
|
||||
|
||||
`meson format` now leaves untouched the lines between
|
||||
`# meson-format off` and `# meson-format on` comments. When
|
||||
`# meson-format off` is not followed by `# meson-format on`, formatting
|
||||
is disabled until the end of the file. `muon fmt`'s `# fmt: off` and
|
||||
`# fmt: on` directives are also recognized.
|
||||
|
||||
```meson
|
||||
# meson-format off
|
||||
sources = files(
|
||||
'a.c', 'b.c',
|
||||
'c.c', 'd.c',
|
||||
)
|
||||
# meson-format on
|
||||
```
|
||||
+80
-2
@@ -44,6 +44,78 @@ class DefaultConfigParser(ConfigParser):
|
||||
return value
|
||||
|
||||
|
||||
# "# fmt: off|on" is accepted for compatibility with muon
|
||||
FORMAT_DIRECTIVE = re.compile(r'^\s*#\s*(?:meson-format\s+|fmt:\s*)(off|on)\s*$')
|
||||
|
||||
|
||||
class FormatDirectiveFinder(FullAstVisitor):
|
||||
'''Collect "# meson-format off|on" and "# fmt: off|on" comments standing on their own line'''
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.depth = 0
|
||||
self.directives: list[tuple[int, str]] = []
|
||||
|
||||
def visit_SymbolNode(self, node: mparser.SymbolNode) -> None:
|
||||
if node.value in {')', ']', '}'}:
|
||||
self.depth -= 1
|
||||
elif node.value in {'(', '[', '{'}:
|
||||
self.depth += 1
|
||||
super().visit_SymbolNode(node)
|
||||
|
||||
def visit_WhitespaceNode(self, node: mparser.WhitespaceNode) -> None:
|
||||
for i, line in enumerate(node.value.split('\n')):
|
||||
m = FORMAT_DIRECTIVE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
lineno = node.lineno + i
|
||||
colno = line.index('#') + (node.colno if i == 0 else 0)
|
||||
if i == 0 and node.colno != 0:
|
||||
raise MesonException(f'"{line.strip()}" directive must be on its own line',
|
||||
file=node.filename, lineno=lineno, colno=colno)
|
||||
if self.depth:
|
||||
raise MesonException(f'"{line.strip()}" directive must enclose whole statements, '
|
||||
'it cannot be inside parentheses, brackets or braces',
|
||||
file=node.filename, lineno=lineno, colno=colno)
|
||||
self.directives.append((lineno - 1, m.group(1)))
|
||||
|
||||
|
||||
def find_format_directives(ast: mparser.BaseNode) -> list[tuple[int, str]]:
|
||||
finder = FormatDirectiveFinder()
|
||||
ast.accept(finder)
|
||||
return sorted(finder.directives)
|
||||
|
||||
|
||||
def restore_disabled_regions(code: str, orig_directives: list[tuple[int, str]],
|
||||
formatted: str, fmt_directives: list[tuple[int, str]]) -> str:
|
||||
'''Replace regions between "# meson-format off" and "# meson-format on"
|
||||
directives in the formatted code with the original code.'''
|
||||
orig_lines = code.splitlines(keepends=True)
|
||||
fmt_lines = formatted.splitlines(keepends=True)
|
||||
if [k for _, k in orig_directives] != [k for _, k in fmt_directives]:
|
||||
raise MesonException('format directives were moved by the formatter; '
|
||||
'they must be on their own line and enclose whole statements')
|
||||
|
||||
result: list[str] = []
|
||||
enabled = True
|
||||
fmt_pos = 0
|
||||
off_pos = 0
|
||||
for (orig_idx, kind), (fmt_idx, _) in zip(orig_directives, fmt_directives):
|
||||
if kind == 'off' and enabled:
|
||||
result += fmt_lines[fmt_pos:fmt_idx + 1]
|
||||
off_pos = orig_idx + 1
|
||||
enabled = False
|
||||
elif kind == 'on' and not enabled:
|
||||
result += orig_lines[off_pos:orig_idx]
|
||||
fmt_pos = fmt_idx
|
||||
enabled = True
|
||||
|
||||
if enabled:
|
||||
result += fmt_lines[fmt_pos:]
|
||||
else:
|
||||
result += orig_lines[off_pos:]
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
def match_path(filename: str, pattern: str) -> bool:
|
||||
'''recursive glob match for editorconfig sections'''
|
||||
index = 0
|
||||
@@ -981,6 +1053,7 @@ class Formatter:
|
||||
self.current_config = self.current_config.update(self.config)
|
||||
|
||||
ast = mparser.Parser(code, source_file.as_posix()).parse()
|
||||
directives = find_format_directives(ast)
|
||||
if self.fetch_subdirs:
|
||||
subdir_fetcher = SubdirFetcher(self.current_dir, self.fetch_subprojects)
|
||||
ast.accept(subdir_fetcher)
|
||||
@@ -998,7 +1071,11 @@ class Formatter:
|
||||
|
||||
printer = RawPrinter()
|
||||
ast.accept(printer)
|
||||
return printer.result
|
||||
if not directives:
|
||||
return printer.result
|
||||
|
||||
formatted_ast = mparser.Parser(printer.result, source_file.as_posix()).parse()
|
||||
return restore_disabled_regions(code, directives, printer.result, find_format_directives(formatted_ast))
|
||||
|
||||
|
||||
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
||||
@@ -1139,7 +1216,6 @@ def run(options: argparse.Namespace) -> int:
|
||||
return err
|
||||
|
||||
# TODO: remove empty newlines when more than N (2...)
|
||||
# TODO: magic comment to prevent formatting
|
||||
# TODO: split long lines on binary operators
|
||||
# TODO: align series of assignments
|
||||
# TODO: align comments
|
||||
@@ -1155,3 +1231,5 @@ def run(options: argparse.Namespace) -> int:
|
||||
# - Correctly compute line length when using tabs
|
||||
# - By default, arguments in files() are sorted naturally
|
||||
# - Option to group '--arg', 'value' on same line in multiline arguments
|
||||
# - '# meson-format off|on' directives, in addition to muon's '# fmt: off|on';
|
||||
# they must be on their own line and enclose whole statements
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2026 The Meson development team
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
import difflib
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('expected', help='the diff we expected')
|
||||
parser.add_argument('meson_cmd_path', nargs='+')
|
||||
args = parser.parse_args()
|
||||
|
||||
result = subprocess.run([
|
||||
*args.meson_cmd_path,
|
||||
'format',
|
||||
'--recursive',
|
||||
'--check-diff',
|
||||
'recurse/meson.build',
|
||||
], capture_output=True, text=True)
|
||||
|
||||
actual = result.stdout.replace('\\', '/').splitlines(keepends=True)
|
||||
with open(args.expected, 'r', encoding='utf-8') as f:
|
||||
expected = f.readlines()
|
||||
|
||||
if actual == expected:
|
||||
return 0
|
||||
|
||||
diff = difflib.ndiff(expected, actual)
|
||||
for line in diff:
|
||||
print(line, file=sys.stderr, end='')
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright © 2024 Intel Corporation
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import difflib
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('actual', help='The transformed contents')
|
||||
parser.add_argument('expected', help='the contents we expected')
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.actual, 'r', encoding='utf-8') as f:
|
||||
actual = f.readlines()
|
||||
with open(args.expected, 'r', encoding='utf-8') as f:
|
||||
expected = f.readlines()
|
||||
|
||||
if actual == expected:
|
||||
return 0
|
||||
|
||||
diff = difflib.ndiff(expected, actual)
|
||||
for line in diff:
|
||||
print(line, file=sys.stderr, end='')
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,33 @@
|
||||
project('format directives')
|
||||
|
||||
python = import('python')
|
||||
meson_cmd = find_program('meson')
|
||||
file_compare = find_program(files('file_compare.py'))
|
||||
check_recurse = find_program(files('check_recurse.py'))
|
||||
|
||||
foreach name : ['nested', 'strings', 'unpaired', 'multiple', 'muon']
|
||||
expected = files(name + '.expected.meson')
|
||||
transform = custom_target(
|
||||
input: files(name + '.meson'),
|
||||
output: name + '.transformed.meson',
|
||||
command: [meson_cmd, 'format', '--output', '@OUTPUT@', '@INPUT@'],
|
||||
)
|
||||
test(name, file_compare, args: [transform, expected])
|
||||
test(
|
||||
name + ' idempotent',
|
||||
meson_cmd,
|
||||
args: ['format', '--check-only', expected],
|
||||
)
|
||||
endforeach
|
||||
|
||||
# formatting disabled in recurse/meson.build must not apply to recurse/sub/meson.build
|
||||
test(
|
||||
'recurse',
|
||||
check_recurse,
|
||||
args: [
|
||||
files('recurse.expected.diff'),
|
||||
python.find_installation(),
|
||||
meson_cmd.full_path(),
|
||||
],
|
||||
workdir: meson.current_source_dir(),
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
# meson-format on
|
||||
# meson-format on
|
||||
a = [1, 2]
|
||||
# meson-format off
|
||||
b = [1,2]
|
||||
# meson-format off
|
||||
c = [1,2]
|
||||
# meson-format on
|
||||
d = [1, 2]
|
||||
# meson-format on
|
||||
e = [1, 2]
|
||||
@@ -0,0 +1,11 @@
|
||||
# meson-format on
|
||||
# meson-format on
|
||||
a = [1,2]
|
||||
# meson-format off
|
||||
b = [1,2]
|
||||
# meson-format off
|
||||
c = [1,2]
|
||||
# meson-format on
|
||||
d = [1,2]
|
||||
# meson-format on
|
||||
e = [1,2]
|
||||
@@ -0,0 +1,10 @@
|
||||
# muon's directives are also recognized, in any spacing,
|
||||
# and can be mixed with meson-format directives
|
||||
# fmt: off
|
||||
a = [1,2]
|
||||
#fmt:on
|
||||
b = [1, 2]
|
||||
# fmt:off
|
||||
c = [1,2]
|
||||
# meson-format on
|
||||
d = [1, 2]
|
||||
@@ -0,0 +1,10 @@
|
||||
# muon's directives are also recognized, in any spacing,
|
||||
# and can be mixed with meson-format directives
|
||||
# fmt: off
|
||||
a = [1,2]
|
||||
#fmt:on
|
||||
b = [1,2]
|
||||
# fmt:off
|
||||
c = [1,2]
|
||||
# meson-format on
|
||||
d = [1,2]
|
||||
@@ -0,0 +1,29 @@
|
||||
project('nested')
|
||||
|
||||
# disabled region inside a block
|
||||
if true
|
||||
# meson-format off
|
||||
x = [1,2,
|
||||
3]
|
||||
# meson-format on
|
||||
y = [1, 2]
|
||||
endif
|
||||
|
||||
# disabled region inside nested blocks
|
||||
foreach i : [1, 2]
|
||||
if i == 1
|
||||
# meson-format off
|
||||
message( 'one' )
|
||||
# meson-format on
|
||||
else
|
||||
message('two')
|
||||
endif
|
||||
endforeach
|
||||
|
||||
# disabled region enclosing a whole block
|
||||
# meson-format off
|
||||
if false
|
||||
z = { 'a' : 1 }
|
||||
endif
|
||||
# meson-format on
|
||||
w = {'a': 1}
|
||||
@@ -0,0 +1,29 @@
|
||||
project('nested')
|
||||
|
||||
# disabled region inside a block
|
||||
if true
|
||||
# meson-format off
|
||||
x = [1,2,
|
||||
3]
|
||||
# meson-format on
|
||||
y = [1,2]
|
||||
endif
|
||||
|
||||
# disabled region inside nested blocks
|
||||
foreach i : [1,2]
|
||||
if i == 1
|
||||
# meson-format off
|
||||
message( 'one' )
|
||||
# meson-format on
|
||||
else
|
||||
message( 'two' )
|
||||
endif
|
||||
endforeach
|
||||
|
||||
# disabled region enclosing a whole block
|
||||
# meson-format off
|
||||
if false
|
||||
z = { 'a' : 1 }
|
||||
endif
|
||||
# meson-format on
|
||||
w = { 'a' : 1 }
|
||||
@@ -0,0 +1,5 @@
|
||||
--- recurse/sub/meson.build (original)
|
||||
+++ recurse/sub/meson.build (reformatted)
|
||||
@@ -1 +1 @@
|
||||
-y = [1,2]
|
||||
+y = [1, 2]
|
||||
@@ -0,0 +1,6 @@
|
||||
project('recurse')
|
||||
|
||||
# formatting is disabled until the end of this file, but not in subdirs
|
||||
# meson-format off
|
||||
subdir( 'sub' )
|
||||
x = [1,2]
|
||||
@@ -0,0 +1 @@
|
||||
y = [1,2]
|
||||
@@ -0,0 +1,6 @@
|
||||
# directives inside strings are not directives
|
||||
s1 = '# meson-format off'
|
||||
s2 = '''
|
||||
# meson-format off
|
||||
'''
|
||||
x = [1, 2]
|
||||
@@ -0,0 +1,6 @@
|
||||
# directives inside strings are not directives
|
||||
s1 = '# meson-format off'
|
||||
s2 = '''
|
||||
# meson-format off
|
||||
'''
|
||||
x = [1,2]
|
||||
@@ -0,0 +1,7 @@
|
||||
# a lone on directive does nothing
|
||||
# meson-format on
|
||||
a = [1, 2]
|
||||
# a lone off directive disables formatting until the end of file
|
||||
# meson-format off
|
||||
b = [1,2]
|
||||
c = { 'k' : 1 }
|
||||
@@ -0,0 +1,7 @@
|
||||
# a lone on directive does nothing
|
||||
# meson-format on
|
||||
a = [1,2]
|
||||
# a lone off directive disables formatting until the end of file
|
||||
# meson-format off
|
||||
b = [1,2]
|
||||
c = { 'k' : 1 }
|
||||
@@ -414,6 +414,75 @@ class PlatformAgnosticTests(BasePlatformTests):
|
||||
formatted = formatter.format(code, Path())
|
||||
self.assertEqual(code, formatted)
|
||||
|
||||
def test_format_directives(self) -> None:
|
||||
formatter = Formatter(None, use_editor_config=False, fetch_subdirs=False)
|
||||
cases = [
|
||||
# off/on pair
|
||||
('x=[1,2]\n# meson-format off\ny = [1,\n 2]\n# meson-format on\nz=[1,2]\n',
|
||||
'x = [1, 2]\n# meson-format off\ny = [1,\n 2]\n# meson-format on\nz = [1, 2]\n'),
|
||||
# lone off disables formatting until the end of file
|
||||
('x=[1,2]\n# meson-format off\ny = [1,\n 2]',
|
||||
'x = [1, 2]\n# meson-format off\ny = [1,\n 2]'),
|
||||
# lone on and duplicated off are no-ops
|
||||
('# meson-format on\nx=[1,2]\n',
|
||||
'# meson-format on\nx = [1, 2]\n'),
|
||||
('# meson-format off\nx=[1,2]\n# meson-format off\ny=[1,2]\n# meson-format on\nz=[1,2]\n',
|
||||
'# meson-format off\nx=[1,2]\n# meson-format off\ny=[1,2]\n# meson-format on\nz = [1, 2]\n'),
|
||||
# indented and loosely spelled directives
|
||||
('if true\n#meson-format off\n y = [1,\n 2]\n # meson-format on\nz=1\nendif\n',
|
||||
'if true\n #meson-format off\n y = [1,\n 2]\n # meson-format on\n z = 1\nendif\n'),
|
||||
# muon compatible directives
|
||||
('# fmt: off\nx = [1,2]\n#fmt:on\ny=[1,2]\n',
|
||||
'# fmt: off\nx = [1,2]\n#fmt:on\ny = [1, 2]\n'),
|
||||
('# fmt:off\nx = [1,2]\n# meson-format on\ny=[1,2]\n',
|
||||
'# fmt:off\nx = [1,2]\n# meson-format on\ny = [1, 2]\n'),
|
||||
# not a directive
|
||||
('# meson-format offset\nx=[1,2]\n',
|
||||
'# meson-format offset\nx = [1, 2]\n'),
|
||||
('# fmt off\nx=[1,2]\n',
|
||||
'# fmt off\nx = [1, 2]\n'),
|
||||
# directives inside strings are ignored
|
||||
("s='''\n# meson-format off\n'''\ny=[1,2]\n",
|
||||
"s = '''\n# meson-format off\n'''\ny = [1, 2]\n"),
|
||||
("s='# meson-format off'\ny=[1,2]\n",
|
||||
"s = '# meson-format off'\ny = [1, 2]\n"),
|
||||
]
|
||||
for code, expected in cases:
|
||||
with self.subTest(code=code):
|
||||
formatted = formatter.format(code, Path())
|
||||
self.assertEqual(expected, formatted)
|
||||
self.assertEqual(formatted, formatter.format(formatted, Path()))
|
||||
|
||||
errors = [
|
||||
('x = [1, 2] # meson-format off\n', 1, 12, 'must be on its own line'),
|
||||
('if true\n x = 1 # meson-format on\nendif\n', 2, 10, 'must be on its own line'),
|
||||
("x = files(\n 'b.c',\n # meson-format off\n 'a.c',\n)\n", 3, 4, 'must enclose whole statements'),
|
||||
('x = [\n # meson-format on\n]\n', 2, 4, 'must enclose whole statements'),
|
||||
('if (true and\n# meson-format off\nfalse)\nendif\n', 2, 0, 'must enclose whole statements'),
|
||||
('x = 1 # fmt: off\n', 1, 7, '"# fmt: off" directive must be on its own line'),
|
||||
('x = [\n # fmt:on\n]\n', 2, 4, '"# fmt:on" directive must enclose whole statements'),
|
||||
]
|
||||
for code, lineno, colno, message in errors:
|
||||
with self.subTest(code=code):
|
||||
with self.assertRaisesRegex(MesonException, message) as cm:
|
||||
formatter.format(code, Path())
|
||||
self.assertEqual((cm.exception.lineno, cm.exception.colno), (lineno, colno))
|
||||
|
||||
def test_format_directives_recursive(self) -> None:
|
||||
dirpath = tempfile.mkdtemp()
|
||||
self.addCleanup(windows_proof_rmtree, dirpath)
|
||||
|
||||
root = Path(dirpath, 'meson.build')
|
||||
root.write_text("project('a')\nsubdir('sub')\n# meson-format off\nx = [1,2]\n", encoding='utf-8')
|
||||
sub = Path(dirpath, 'sub', 'meson.build')
|
||||
sub.parent.mkdir()
|
||||
sub.write_text('y = [1,2]\n', encoding='utf-8')
|
||||
|
||||
self._run(self.meson_command + ['format', '-r', '-i'], workdir=dirpath)
|
||||
self.assertEqual("project('a')\nsubdir('sub')\n# meson-format off\nx = [1,2]\n",
|
||||
root.read_text(encoding='utf-8'))
|
||||
self.assertEqual('y = [1, 2]\n', sub.read_text(encoding='utf-8'))
|
||||
|
||||
def test_error_configuring_subdir(self):
|
||||
testdir = os.path.join(self.common_test_dir, '152 index customtarget')
|
||||
out = self.init(os.path.join(testdir, 'subdir'), allow_fail=True)
|
||||
|
||||
Reference in new issue
Block a user