diff --git a/Lib/test/test_tools/test_c_analyzer.py b/Lib/test/test_tools/test_c_analyzer.py new file mode 100644 index 000000000000000..2df7c4ea97fc38f --- /dev/null +++ b/Lib/test/test_tools/test_c_analyzer.py @@ -0,0 +1,78 @@ +import os.path +import shutil +import subprocess +import tempfile +import unittest + +from test.test_tools import imports_under_tool, skip_if_missing + + +skip_if_missing('c-analyzer') + +with imports_under_tool('c-analyzer'): + from c_parser.info import FileInfo + from c_parser.parser import parse + from c_parser.preprocessor import gcc + + +class StaticAssertTests(unittest.TestCase): + def parse(self, text): + return list(parse( + (FileInfo('test.c', lno), line) + for lno, line in enumerate(text.splitlines(), 1) + )) + + def test_global(self): + for keyword in ('_Static_assert', 'static_assert'): + with self.subTest(keyword=keyword): + items = self.parse(f''' + int before; + {keyword}( + 256 > 0 && !(256 & (256 - 1)), + "buffer size; (" + ) + ; int after; + ''') + self.assertEqual([item.name for item in items], + ['before', 'after']) + + def test_struct(self): + for keyword in ('_Static_assert', 'static_assert'): + with self.subTest(keyword=keyword): + items = self.parse(f''' + struct example {{ + int before; + {keyword}(sizeof(int) > 0, "int size"); + int after; + }}; + int global_after; + ''') + struct = next(item for item in items + if item.name == 'example' and item.data) + self.assertEqual([field.name for field in struct.data], + ['before', 'after']) + self.assertEqual(items[-1].name, 'global_after') + + @unittest.skipUnless(shutil.which('gcc'), 'requires gcc') + @unittest.skipIf(os.name == 'nt', 'GCC backend does not handle Windows paths') + def test_preprocess(self): + version = subprocess.check_output(['gcc', '--version'], text=True) + if 'clang' in version.lower(): + self.skipTest('requires GNU GCC, not Clang') + with tempfile.TemporaryDirectory() as tmpdir: + filename = os.path.join(tmpdir, 'test.c') + with open(filename, 'w', encoding='utf-8') as source: + source.write(''' + #define _GNU_SOURCE 1 + #include + static_assert(256 > 0 && !(256 & 255), "buffer size"); + int global_after; + ''') + lines = gcc.preprocess(filename, samefiles=(), cwd=tmpdir) + items = list(parse((line.file, line.data) for line in lines + if line.kind == 'source')) + self.assertEqual([item.name for item in items], ['global_after']) + + +if __name__ == '__main__': + unittest.main() diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-09-05-00-00-00.gh-issue-136952.Jx7mQp.rst b/Misc/NEWS.d/next/Tools-Demos/2026-09-05-00-00-00.gh-issue-136952.Jx7mQp.rst new file mode 100644 index 000000000000000..9953418e263c146 --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2026-09-05-00-00-00.gh-issue-136952.Jx7mQp.rst @@ -0,0 +1,2 @@ +Fix the C analyzer failing on static assertions in free-threaded builds by +using C11 preprocessing and handling static assertion declarations. diff --git a/Tools/c-analyzer/c_parser/parser/_common.py b/Tools/c-analyzer/c_parser/parser/_common.py index 2eacace2c001df9..206581e26bfb560 100644 --- a/Tools/c-analyzer/c_parser/parser/_common.py +++ b/Tools/c-analyzer/c_parser/parser/_common.py @@ -78,6 +78,21 @@ def match_paren(text, depth=0): raise ValueError(f'could not find matching parens for {text!r}') +def skip_static_assert(srcinfo): + m = re.match(r'^\s*(?:_Static_assert|static_assert)\b', srcinfo.text) + if not m: + return False + text = srcinfo.text[m.end():] + try: + end = match_paren(text) + except ValueError: + return True + remainder = text[end:].lstrip() + if remainder.startswith(';'): + srcinfo.advance(remainder[1:]) + return True + + VAR_DECL = set_capture_groups(_VAR_DECL, ( 'STORAGE', 'TYPE_QUAL', diff --git a/Tools/c-analyzer/c_parser/parser/_compound_decl_body.py b/Tools/c-analyzer/c_parser/parser/_compound_decl_body.py index 67528d227989627..4f623f2908a9e6a 100644 --- a/Tools/c-analyzer/c_parser/parser/_compound_decl_body.py +++ b/Tools/c-analyzer/c_parser/parser/_compound_decl_body.py @@ -8,6 +8,7 @@ log_match, parse_var_decl, set_capture_groups, + skip_static_assert, ) @@ -31,6 +32,8 @@ def parse_struct_body(source, anon_name, parent): while not done: done = True for srcinfo in source: + if skip_static_assert(srcinfo): + continue m = STRUCT_MEMBER_RE.match(srcinfo.text) if m: break diff --git a/Tools/c-analyzer/c_parser/parser/_global.py b/Tools/c-analyzer/c_parser/parser/_global.py index b1ac9f5db034e1f..c6705e9feb49d58 100644 --- a/Tools/c-analyzer/c_parser/parser/_global.py +++ b/Tools/c-analyzer/c_parser/parser/_global.py @@ -7,6 +7,7 @@ log_match, parse_var_decl, set_capture_groups, + skip_static_assert, ) from ._compound_decl_body import DECL_BODY_PARSERS from ._func_body import parse_function_statics as parse_function_body @@ -36,6 +37,8 @@ def parse_globals(source, anon_name): for srcinfo in source: + if skip_static_assert(srcinfo): + continue m = GLOBAL_RE.match(srcinfo.text) if not m: # We need more text. diff --git a/Tools/c-analyzer/c_parser/preprocessor/gcc.py b/Tools/c-analyzer/c_parser/preprocessor/gcc.py index 92134bc1321e1b5..cb43a2d957a3d67 100644 --- a/Tools/c-analyzer/c_parser/preprocessor/gcc.py +++ b/Tools/c-analyzer/c_parser/preprocessor/gcc.py @@ -56,7 +56,7 @@ POST_ARGS = ( '-pthread', - '-std=c99', + '-std=c11', #'-g', #'-Og', #'-Wno-unused-result', diff --git a/Tools/c-analyzer/cpython/_parser.py b/Tools/c-analyzer/cpython/_parser.py index 489043103aa9b5b..ef29a749bb3c9e4 100644 --- a/Tools/c-analyzer/cpython/_parser.py +++ b/Tools/c-analyzer/cpython/_parser.py @@ -178,6 +178,9 @@ def format_tsv_lines(lines): MACROS = format_tsv_lines([ # (glob, name, value) + # Alignment does not affect global-state classification. + ('*', '_Py_ALIGNED_DEF(N, T)', 'T'), + ('Include/internal/*.h', 'Py_BUILD_CORE', '1'), ('Python/**/*.c', 'Py_BUILD_CORE', '1'), ('Python/**/*.h', 'Py_BUILD_CORE', '1'),