Coverage for src / competitive_verifier / oj / languages / cplusplus_bundle.py: 70%

197 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-03 23:40 +0900

1# Python Version: 3.x 

2import contextlib 

3import functools 

4import json 

5import os 

6import pathlib 

7import re 

8import shutil 

9from logging import getLogger 

10from typing import Any 

11 

12from competitive_verifier.exec import command_stdout 

13 

14logger = getLogger(__name__) 

15 

16BITS_STDCXX_H = "bits/stdc++.h" 

17 

18CXX_STANDARD_LIBS = { 

19 "algorithm", 

20 "any", 

21 "array", 

22 "atomic", 

23 "barrier", 

24 "bit", 

25 "bitset", 

26 "charconv", 

27 "chrono", 

28 "codecvt", 

29 "compare", 

30 "complex", 

31 "concepts", 

32 "condition_variable", 

33 "contracts", 

34 "coroutine", 

35 "debugging", 

36 "deque", 

37 "exception", 

38 "expected", 

39 "filesystem", 

40 "flat_map", 

41 "flat_set", 

42 "format", 

43 "forward_list", 

44 "fstream", 

45 "functional", 

46 "future", 

47 "generator", 

48 "initializer_list", 

49 "inplace_vector", 

50 "iomanip", 

51 "ios", 

52 "iosfwd", 

53 "iostream", 

54 "istream", 

55 "iterator", 

56 "latch", 

57 "limits", 

58 "list", 

59 "locale", 

60 "map", 

61 "mdspan", 

62 "memory", 

63 "memory_resource", 

64 "meta", 

65 "mutex", 

66 "new", 

67 "numbers", 

68 "numeric", 

69 "optional", 

70 "ostream", 

71 "print", 

72 "queue", 

73 "random", 

74 "ranges", 

75 "ratio", 

76 "regex", 

77 "scoped_allocator", 

78 "semaphore", 

79 "set", 

80 "shared_mutex", 

81 "source_location", 

82 "span", 

83 "spanstream", 

84 "sstream", 

85 "stack", 

86 "stacktrace", 

87 "stdexcept", 

88 "stdfloat", 

89 "stop_token", 

90 "streambuf", 

91 "string", 

92 "string_view", 

93 "syncstream", 

94 "system_error", 

95 "text_encoding", 

96 "thread", 

97 "tuple", 

98 "type_traits", 

99 "typeindex", 

100 "typeinfo", 

101 "unordered_map", 

102 "unordered_set", 

103 "utility", 

104 "valarray", 

105 "variant", 

106 "vector", 

107 "version", 

108} 

109 

110C_STANDARD_LIBS = { 

111 "assert.h", 

112 "complex.h", 

113 "ctype.h", 

114 "errno.h", 

115 "fenv.h", 

116 "float.h", 

117 "inttypes.h", 

118 "iso646.h", 

119 "limits.h", 

120 "locale.h", 

121 "math.h", 

122 "setjmp.h", 

123 "signal.h", 

124 "stdalign.h", 

125 "stdarg.h", 

126 "stdatomic.h", 

127 "stdbit.h", 

128 "stdbool.h", 

129 "stdckdint.h", 

130 "stddef.h", 

131 "stdint.h", 

132 "stdio.h", 

133 "stdlib.h", 

134 "stdnoreturn.h", 

135 "string.h", 

136 "tgmath.h", 

137 "threads.h", 

138 "time.h", 

139 "uchar.h", 

140 "wchar.h", 

141 "wctype.h", 

142} 

143 

144CXX_C_ORIGIN_LIBS = {"c" + name[: -len(".h")] for name in C_STANDARD_LIBS} 

145 

146BITS_EXTCXX_H = "bits/extc++.h" 

147 

148EXT_LIBS = { 

149 "ext/algorithm", 

150 "ext/array_allocator.h", 

151 "ext/atomicity.h", 

152 "ext/bitmap_allocator.h", 

153 "ext/cast.h", 

154 "ext/concurrence.h", 

155 "ext/debug_allocator.h", 

156 "ext/extptr_allocator.h", 

157 "ext/functional", 

158 "ext/iterator", 

159 "ext/malloc_allocator.h", 

160 "ext/memory", 

161 "ext/mt_allocator.h", 

162 "ext/new_allocator.h", 

163 "ext/numeric", 

164 "ext/pod_char_traits.h", 

165 "ext/pointer.h", 

166 "ext/pool_allocator.h", 

167 "ext/rb_tree", 

168 "ext/rope", 

169 "ext/slist", 

170 "ext/stdio_filebuf.h", 

171 "ext/stdio_sync_filebuf.h", 

172 "ext/throw_allocator.h", 

173 "ext/typelist.h", 

174 "ext/type_traits.h", 

175 "ext/vstring.h", 

176 "ext/pb_ds/assoc_container.hpp", 

177 "ext/pb_ds/priority_queue.hpp", 

178 "ext/pb_ds/exception.hpp", 

179 "ext/pb_ds/hash_policy.hpp", 

180 "ext/pb_ds/list_update_policy.hpp", 

181 "ext/pb_ds/tree_policy.hpp", 

182 "ext/pb_ds/trie_policy.hpp", 

183} 

184 

185BITS_STDTR1CXX_H = "bits/stdtr1c++.h" 

186 

187TR1_LIBS = { 

188 "tr1/array", 

189 "tr1/cctype", 

190 "tr1/cfenv", 

191 "tr1/cfloat", 

192 "tr1/cinttypes", 

193 "tr1/climits", 

194 "tr1/cmath", 

195 "tr1/complex", 

196 "tr1/cstdarg", 

197 "tr1/cstdbool", 

198 "tr1/cstdint", 

199 "tr1/cstdio", 

200 "tr1/cstdlib", 

201 "tr1/ctgmath", 

202 "tr1/ctime", 

203 "tr1/cwchar", 

204 "tr1/cwctype", 

205 "tr1/functional", 

206 "tr1/random", 

207 "tr1/tuple", 

208 "tr1/unordered_map", 

209 "tr1/unordered_set", 

210 "tr1/utility", 

211} 

212 

213 

214@functools.cache 

215def _check_compiler(compiler: str) -> str: 

216 # Executables named "g++" are not always g++, due to the fake g++ of macOS 

217 version = command_stdout([compiler, "--version"]) 

218 if "clang" in version.lower() or "Apple LLVM".lower() in version.lower(): 218 ↛ 219line 218 didn't jump to line 219 because the condition on line 218 was never true

219 return "clang" 

220 if "g++" in version.lower(): 220 ↛ 222line 220 didn't jump to line 222 because the condition on line 220 was always true

221 return "gcc" 

222 return "unknown" # default 

223 

224 

225@functools.cache 

226def _get_uncommented_code( 

227 path: pathlib.Path, *, iquotes_options: tuple[str, ...], compiler: str 

228) -> bytes: 

229 # `iquotes_options` must be a tuple to use `lru_cache` 

230 

231 if shutil.which(compiler) is None: 231 ↛ 232line 231 didn't jump to line 232 because the condition on line 231 was never true

232 raise BundleError(f"command not found: {compiler}") 

233 if _check_compiler(compiler) != "gcc": 233 ↛ 234line 233 didn't jump to line 234 because the condition on line 233 was never true

234 if compiler == "g++": 

235 raise BundleError( 

236 f"A fake g++ is detected. Please install the GNU C++ compiler.: {compiler}" 

237 ) 

238 raise BundleError( 

239 f"It's not g++. Please specify g++ with $CXX envvar.: {compiler}" 

240 ) 

241 command = [ 

242 compiler, 

243 "-x", 

244 "c++", 

245 *iquotes_options, 

246 "-fpreprocessed", 

247 "-dD", 

248 "-E", 

249 str(path), 

250 ] 

251 return command_stdout(command, text=False) 

252 

253 

254def get_uncommented_code( 

255 path: pathlib.Path, *, iquotes: list[pathlib.Path], compiler: str 

256) -> bytes: 

257 iquotes_options: list[str] = [] 

258 for iquote in iquotes: 

259 iquotes_options.extend(["-I", str(iquote.resolve())]) 

260 code = _get_uncommented_code( 

261 path.resolve(), iquotes_options=tuple(iquotes_options), compiler=compiler 

262 ) 

263 lines: list[bytes] = [] 

264 for line in code.splitlines(keepends=True): 

265 m = re.match(rb'# (\d+) ".*"', line.rstrip()) 

266 if m: 

267 lineno = int(m.group(1)) 

268 while len(lines) + 1 < lineno: 268 ↛ 269line 268 didn't jump to line 269 because the condition on line 268 was never true

269 lines.append(b"\n") 

270 else: 

271 lines.append(line) 

272 return b"".join(lines) 

273 

274 

275class BundleError(Exception): 

276 pass 

277 

278 

279class BundleErrorAt(BundleError): # noqa: N818 

280 def __init__( 

281 self, path: pathlib.Path, line: int, message: str, *args: Any, **kwargs: Any 

282 ): 

283 with contextlib.suppress(ValueError): 

284 path = path.resolve().relative_to(pathlib.Path.cwd()) 

285 message = f"{path!s}: line {line}: {message}" 

286 super().__init__(message, *args, **kwargs) 

287 

288 

289class Bundler: 

290 iquotes: list[pathlib.Path] 

291 pragma_once: set[pathlib.Path] 

292 pragma_once_system: set[str] 

293 result_lines: list[bytes] 

294 path_stack: set[pathlib.Path] 

295 compiler: str 

296 

297 def __init__( 

298 self, 

299 *, 

300 iquotes: list[pathlib.Path] | None = None, 

301 compiler: str = os.environ.get("CXX", "g++"), 

302 ) -> None: 

303 if iquotes is None: 303 ↛ 304line 303 didn't jump to line 304 because the condition on line 303 was never true

304 iquotes = [] 

305 self.iquotes = iquotes 

306 self.pragma_once = set() 

307 self.pragma_once_system = set() 

308 self.result_lines = [] 

309 self.path_stack = set() 

310 self.compiler = compiler 

311 

312 # これをしないと __FILE__ や __LINE__ が壊れる 

313 def _line(self, line: int, path: pathlib.Path) -> None: 

314 while self.result_lines and self.result_lines[-1].startswith(b"#line "): 

315 self.result_lines.pop() 

316 with contextlib.suppress(ValueError): 

317 path = path.relative_to(pathlib.Path.cwd()) 

318 # パス中の特殊文字を JSON style にエスケープしてから生成コードに記述 

319 # quick solution to this: https://github.com/online-judge-tools/verification-helper/issues/280 

320 self.result_lines.append(f"#line {line} {json.dumps(str(path))}\n".encode()) 

321 

322 # path を解決する 

323 # see: https://gcc.gnu.org/onlinedocs/gcc/Directory-Options.html#Directory-Options 

324 def _resolve( 

325 self, path: pathlib.Path, *, included_from: pathlib.Path 

326 ) -> pathlib.Path: 

327 if (included_from.parent / path).exists(): 327 ↛ 329line 327 didn't jump to line 329 because the condition on line 327 was always true

328 return (included_from.parent / path).resolve() 

329 for dir_ in self.iquotes: 

330 if (dir_ / path).exists(): 

331 return (dir_ / path).resolve() 

332 raise BundleErrorAt(path, -1, "no such header") 

333 

334 def update(self, path: pathlib.Path) -> None: 

335 if path.resolve() in self.pragma_once: 335 ↛ 336line 335 didn't jump to line 336 because the condition on line 335 was never true

336 logger.debug( 

337 "%s: skipped since this file is included once with include guard", 

338 path, 

339 ) 

340 return 

341 

342 # 再帰的に自分自身を #include してたら諦める 

343 if path in self.path_stack: 343 ↛ 344line 343 didn't jump to line 344 because the condition on line 343 was never true

344 raise BundleErrorAt(path, -1, "cycle found in inclusion relations") 

345 self.path_stack.add(path) 

346 try: 

347 code = path.read_bytes() 

348 if not code.endswith(b"\n"): 348 ↛ 350line 348 didn't jump to line 350 because the condition on line 348 was never true

349 # ファイルの末尾に改行がなかったら足す 

350 code += b"\n" 

351 

352 # include guard のまわりの変数 

353 # NOTE: include guard に使われたマクロがそれ以外の用途にも使われたり #undef されたりすると壊れるけど、無視します 

354 non_guard_line_found = False 

355 pragma_once_found = False 

356 include_guard_macro = None 

357 include_guard_define_found = False 

358 include_guard_endif_found = False 

359 preprocess_if_nest = 0 

360 

361 lines = code.splitlines(keepends=True) 

362 uncommented_lines = get_uncommented_code( 

363 path, iquotes=self.iquotes, compiler=self.compiler 

364 ).splitlines(keepends=True) 

365 uncommented_lines.extend( 

366 [b""] * (len(lines) - len(uncommented_lines)) 

367 ) # trailing comment lines are removed 

368 assert len(lines) == len(uncommented_lines) 

369 self._line(1, path) 

370 for i, (line, uncommented_line) in enumerate( 

371 zip(lines, uncommented_lines, strict=False) 

372 ): 

373 # nest の処理 

374 if re.match(rb"\s*#\s*(if|ifdef|ifndef)\s.*", uncommented_line): 

375 preprocess_if_nest += 1 

376 if ( 376 ↛ 380line 376 didn't jump to line 380 because the condition on line 376 was never true

377 re.match(rb"\s*#\s*(else\s*|elif\s.*)", uncommented_line) 

378 and preprocess_if_nest == 0 

379 ): 

380 raise BundleErrorAt(path, i + 1, "unmatched #else / #elif") 

381 if re.match(rb"\s*#\s*endif\s*", uncommented_line): 

382 preprocess_if_nest -= 1 

383 if preprocess_if_nest < 0: 383 ↛ 384line 383 didn't jump to line 384 because the condition on line 383 was never true

384 raise BundleErrorAt(path, i + 1, "unmatched #endif") 

385 is_toplevel = preprocess_if_nest == 0 or ( 

386 preprocess_if_nest == 1 and include_guard_macro is not None 

387 ) 

388 

389 # #pragma once 

390 if re.match( 

391 rb"\s*#\s*pragma\s+once\s*", line 

392 ): # #pragma once は comment 扱いで消されてしまう 

393 logger.debug("%s: line %s: #pragma once", path, i + 1) 

394 if non_guard_line_found: 394 ↛ 396line 394 didn't jump to line 396 because the condition on line 394 was never true

395 # 先頭以外で #pragma once されてた場合は諦める 

396 raise BundleErrorAt( 

397 path, i + 1, "#pragma once found in a non-first line" 

398 ) 

399 if include_guard_macro is not None: 399 ↛ 400line 399 didn't jump to line 400 because the condition on line 399 was never true

400 raise BundleErrorAt( 

401 path, 

402 i + 1, 

403 "#pragma once found in an include guard with #ifndef", 

404 ) 

405 if path.resolve() in self.pragma_once: 405 ↛ 406line 405 didn't jump to line 406 because the condition on line 405 was never true

406 return 

407 pragma_once_found = True 

408 self.pragma_once.add(path.resolve()) 

409 self._line(i + 2, path) 

410 continue 

411 

412 matched: re.Match[bytes] | None 

413 # #ifndef HOGE_H as guard 

414 if ( 

415 not pragma_once_found 

416 and not non_guard_line_found 

417 and include_guard_macro is None 

418 ): 

419 matched = re.match(rb"\s*#\s*ifndef\s+(\w+)\s*", uncommented_line) 

420 if matched: 

421 include_guard_macro = matched.group(1).decode() 

422 logger.debug( 

423 "%s: line %s: #ifndef %s", 

424 path, 

425 i + 1, 

426 include_guard_macro, 

427 ) 

428 self.result_lines.append(b"\n") 

429 continue 

430 

431 # #define HOGE_H as guard 

432 if include_guard_macro is not None and not include_guard_define_found: 

433 matched = re.match(rb"\s*#\s*define\s+(\w+)\s*", uncommented_line) 

434 if matched and matched.group(1).decode() == include_guard_macro: 434 ↛ 447line 434 didn't jump to line 447 because the condition on line 434 was always true

435 self.pragma_once.add(path.resolve()) 

436 logger.debug( 

437 "%s: line %s: #define %s", 

438 path, 

439 i + 1, 

440 include_guard_macro, 

441 ) 

442 include_guard_define_found = True 

443 self.result_lines.append(b"\n") 

444 continue 

445 

446 # #endif as guard 

447 if ( 

448 include_guard_define_found 

449 and preprocess_if_nest == 0 

450 and not include_guard_endif_found 

451 ) and re.match(rb"\s*#\s*endif\s*", uncommented_line): 

452 include_guard_endif_found = True 

453 self.result_lines.append(b"\n") 

454 continue 

455 

456 if uncommented_line and not re.match(rb"^\s*$", uncommented_line): 

457 non_guard_line_found = True 

458 if ( 458 ↛ 463line 458 didn't jump to line 463 because the condition on line 458 was never true

459 include_guard_macro is not None 

460 and not include_guard_define_found 

461 ): 

462 # 先頭に #ifndef が見付かっても #define が続かないならそれは include guard ではない 

463 include_guard_macro = None 

464 if include_guard_endif_found: 464 ↛ 466line 464 didn't jump to line 466 because the condition on line 464 was never true

465 # include guard の外側にコードが書かれているとまずいので検出する 

466 raise BundleErrorAt( 

467 path, i + 1, "found codes out of include guard" 

468 ) 

469 

470 # #include <...> 

471 matched = re.match(rb"\s*#\s*include\s*<(.*)>\s*", uncommented_line) 

472 if matched: 

473 included = matched.group(1).decode() 

474 logger.debug( 

475 "%s: line %s: #include <%s>", path, i + 1, str(included) 

476 ) 

477 if included in self.pragma_once_system: 477 ↛ 478line 477 didn't jump to line 478 because the condition on line 477 was never true

478 self._line(i + 2, path) 

479 elif not is_toplevel: 479 ↛ 481line 479 didn't jump to line 481 because the condition on line 479 was never true

480 # #pragma once 系の判断ができない場合はそっとしておく 

481 self.result_lines.append(line) 

482 elif ( 482 ↛ 492line 482 didn't jump to line 492 because the condition on line 482 was always true

483 included in C_STANDARD_LIBS 

484 or included in CXX_STANDARD_LIBS 

485 or included in CXX_C_ORIGIN_LIBS 

486 ): 

487 if BITS_STDCXX_H in self.pragma_once_system: 487 ↛ 488line 487 didn't jump to line 488 because the condition on line 487 was never true

488 self._line(i + 2, path) 

489 else: 

490 self.pragma_once_system.add(included) 

491 self.result_lines.append(line) 

492 elif included in EXT_LIBS: 

493 if BITS_EXTCXX_H in self.pragma_once_system: 

494 self._line(i + 2, path) 

495 else: 

496 self.pragma_once_system.add(included) 

497 self.result_lines.append(line) 

498 elif included in TR1_LIBS: 

499 if BITS_STDTR1CXX_H in self.pragma_once_system: 

500 self._line(i + 2, path) 

501 else: 

502 self.pragma_once_system.add(included) 

503 self.result_lines.append(line) 

504 else: 

505 # possibly: bits/*, tr2/* boost/*, c-posix library, etc. 

506 self.pragma_once_system.add(included) 

507 self.result_lines.append(line) 

508 if included in [BITS_EXTCXX_H, BITS_STDTR1CXX_H]: 

509 self.pragma_once_system.add(BITS_STDCXX_H) 

510 continue 

511 

512 # #include "..." 

513 matched = re.match(rb'\s*#\s*include\s*"(.*)"\s*', uncommented_line) 

514 if matched: 

515 included = matched.group(1).decode() 

516 logger.debug('%s: line %s: #include "%s"', path, i + 1, included) 

517 if not is_toplevel: 517 ↛ 519line 517 didn't jump to line 519 because the condition on line 517 was never true

518 # #if の中から #include されると #pragma once 系の判断が不可能になるので諦める 

519 raise BundleErrorAt( 

520 path, 

521 i + 1, 

522 "unable to process #include in #if / #ifdef / #ifndef other than include guards", 

523 ) 

524 self.update( 

525 self._resolve(pathlib.Path(included), included_from=path) 

526 ) 

527 self._line(i + 2, path) 

528 # #include "iostream" みたいに書いたときの挙動をはっきりさせる 

529 # #include <iostream> /* とかをやられた場合を落とす 

530 continue 

531 

532 # otherwise 

533 self.result_lines.append(line) 

534 

535 # #if #endif の対応が壊れてたら諦める 

536 last_index = i + 1 # pyright: ignore[reportPossiblyUnboundVariable] 

537 

538 if preprocess_if_nest != 0: 538 ↛ 539line 538 didn't jump to line 539 because the condition on line 538 was never true

539 raise BundleErrorAt( 

540 path, last_index, "unmatched #if / #ifdef / #ifndef" 

541 ) 

542 if include_guard_macro is not None and not include_guard_endif_found: 542 ↛ 543line 542 didn't jump to line 543 because the condition on line 542 was never true

543 raise BundleErrorAt(path, last_index, "unmatched #ifndef") 

544 

545 finally: 

546 # 中で return することがあるので finally 節に入れておく 

547 self.path_stack.remove(path) 

548 

549 def get(self) -> bytes: 

550 return b"".join(self.result_lines)