@@ -89,6 +89,86 @@ def start_workers(max_tasks, tidy_caller, task_queue, lock, timeout):
8989 t .daemon = True
9090 t .start ()
9191
92+ _HEADER_EXTS = frozenset (('.h' , '.hh' , '.hpp' , '.hxx' , '.h++' , '.inc' ))
93+
94+ # Matches -include/-include-pch and its path argument in a command string,
95+ # including the -Xclang wrapped form used for precompiled header binaries.
96+ _PCH_INCLUDE_RE = re .compile (
97+ r'-Xclang\s+-include-pch\s+-Xclang\s+(?:"[^"]*"|\S+)'
98+ r'|-include(?:-pch)?(?:\s+(?:"[^"]*"|\S+)|\S+)'
99+ )
100+
101+
102+ def _is_header (filename ):
103+ _ , ext = os .path .splitext (filename )
104+ return ext .lower () in _HEADER_EXTS
105+
106+
107+ def _strip_pch_from_command (command_str ):
108+ result = _PCH_INCLUDE_RE .sub ('' , command_str )
109+ return re .sub (r' +' , ' ' , result ).strip ()
110+
111+
112+ def _strip_pch_from_arguments (arguments ):
113+ result = []
114+ i = 0
115+ while i < len (arguments ):
116+ # -Xclang -include-pch -Xclang <path> (4 args)
117+ if (arguments [i ] == '-Xclang' and
118+ i + 3 < len (arguments ) and
119+ arguments [i + 1 ] == '-include-pch' and
120+ arguments [i + 2 ] == '-Xclang' ):
121+ i += 4
122+ continue
123+ # -include <path> or -include-pch <path> (2 args)
124+ if arguments [i ] in ('-include' , '-include-pch' ) and i + 1 < len (arguments ):
125+ i += 2
126+ continue
127+ # -include<path> or -include-pch<path> (concatenated, 1 arg)
128+ if arguments [i ].startswith ('-include' ):
129+ i += 1
130+ continue
131+ result .append (arguments [i ])
132+ i += 1
133+ return result
134+
135+
136+ def _create_sanitized_compile_commands (build_path ):
137+ """Create a copy of compile_commands.json with forced-include flags removed.
138+
139+ When clang-tidy analyses a header file as the main source file, any
140+ -include (PCH) flag causes the header to be parsed twice in the same
141+ translation unit: once via the forced include and once as the main file.
142+ #pragma once cannot prevent this because the main file is not an #include.
143+ Stripping these flags for header-file analysis avoids the resulting
144+ redefinition errors.
145+ """
146+ src = os .path .join (build_path , 'compile_commands.json' )
147+ if not os .path .isfile (src ):
148+ return None
149+ try :
150+ with open (src , 'r' ) as f :
151+ db = json .load (f )
152+ except (ValueError , OSError ):
153+ return None
154+
155+ for entry in db :
156+ if 'command' in entry :
157+ entry ['command' ] = _strip_pch_from_command (entry ['command' ])
158+ if 'arguments' in entry :
159+ entry ['arguments' ] = _strip_pch_from_arguments (entry ['arguments' ])
160+
161+ sanitized_dir = tempfile .mkdtemp (prefix = 'clang-tidy-no-pch-' )
162+ dst = os .path .join (sanitized_dir , 'compile_commands.json' )
163+ try :
164+ with open (dst , 'w' ) as f :
165+ json .dump (db , f )
166+ except OSError :
167+ shutil .rmtree (sanitized_dir , ignore_errors = True )
168+ return None
169+ return sanitized_dir
170+
171+
92172def merge_replacement_files (tmpdir , mergefile ):
93173 """Merge all replacement files in a directory into a single file"""
94174 # The fixes suggested by clang-tidy >= 4.0.0 are given under
@@ -222,13 +302,18 @@ def main():
222302 common_clang_tidy_args .append ('-checks=' + args .checks )
223303 if args .quiet :
224304 common_clang_tidy_args .append ('-quiet' )
225- if args .build_path is not None :
226- common_clang_tidy_args .append ('-p=%s' % args .build_path )
227305 for arg in args .extra_arg :
228306 common_clang_tidy_args .append ('-extra-arg=%s' % arg )
229307 for arg in args .extra_arg_before :
230308 common_clang_tidy_args .append ('-extra-arg-before=%s' % arg )
231309
310+ # When headers are in the changed-file list, create a sanitized compile DB
311+ # with forced-include flags stripped so they don't cause redefinitions.
312+ sanitized_build_path = None
313+ if args .build_path is not None :
314+ if any (_is_header (name ) for name in lines_by_file ):
315+ sanitized_build_path = _create_sanitized_compile_commands (args .build_path )
316+
232317 for name in lines_by_file :
233318 line_filter_json = json .dumps (
234319 [{"name" : name , "lines" : lines_by_file [name ]}],
@@ -244,6 +329,11 @@ def main():
244329 os .close (handle )
245330 command .append ('-export-fixes=' + tmp_name )
246331 command .extend (common_clang_tidy_args )
332+ if args .build_path is not None :
333+ if _is_header (name ) and sanitized_build_path is not None :
334+ command .append ('-p=%s' % sanitized_build_path )
335+ else :
336+ command .append ('-p=%s' % args .build_path )
247337 command .append (name )
248338 command .extend (clang_tidy_args )
249339
@@ -252,6 +342,9 @@ def main():
252342 # Wait for all threads to be done.
253343 task_queue .join ()
254344
345+ if sanitized_build_path is not None :
346+ shutil .rmtree (sanitized_build_path , ignore_errors = True )
347+
255348 if yaml and args .export_fixes :
256349 print ('Writing fixes to ' + args .export_fixes + ' ...' )
257350 try :
0 commit comments