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:
Charles Brunet authored and Paolo Bonzini committed 2026-10-23 13:55:51 +02:00
1 parent d3d17647d2
commit 99fe4fef47
20 files changed
+435 -2

No files matched your search

+29
View File
@@ -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
View File
@@ -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]
+10
View File
@@ -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 }
+69
View File
@@ -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)