|
11 | 11 |
|
12 | 12 | __version__ = "0.0.20" |
13 | 13 |
|
14 | | -from typing import Optional |
| 14 | +from typing import Optional, List, Iterable |
15 | 15 |
|
16 | 16 |
|
17 | 17 | class FailedCheck: |
@@ -241,6 +241,157 @@ def implicit_check_line(check_not_check, strict_mode, line): |
241 | 241 | return False |
242 | 242 |
|
243 | 243 |
|
| 244 | +class CheckParserEmptyCheckException(BaseException): |
| 245 | + def __init__(self, check: Check): |
| 246 | + super().__init__() |
| 247 | + self.check = check |
| 248 | + |
| 249 | + |
| 250 | +class CheckParser: |
| 251 | + @staticmethod |
| 252 | + def parse_checks_from_file( |
| 253 | + check_file_path: str, args, strict_mode: bool, check_prefix: str |
| 254 | + ) -> List[Check]: |
| 255 | + with open(check_file_path, encoding="utf-8") as check_file: |
| 256 | + return CheckParser.parse_checks_from_strings( |
| 257 | + check_file, args, strict_mode, check_prefix |
| 258 | + ) |
| 259 | + |
| 260 | + @staticmethod |
| 261 | + def parse_checks_from_strings( |
| 262 | + input_strings: Iterable[str], args, strict_mode: bool, check_prefix: str |
| 263 | + ) -> List[Check]: |
| 264 | + checks = [] |
| 265 | + for line_idx, line in enumerate(input_strings): |
| 266 | + check = CheckParser.parse_check( |
| 267 | + line, line_idx, args, strict_mode, check_prefix |
| 268 | + ) |
| 269 | + if check is None: |
| 270 | + continue |
| 271 | + if check.check_type == CheckType.CHECK_EMPTY and len(checks) == 0: |
| 272 | + raise CheckParserEmptyCheckException(check) |
| 273 | + checks.append(check) |
| 274 | + return checks |
| 275 | + |
| 276 | + @staticmethod |
| 277 | + def parse_check( |
| 278 | + line: str, line_idx, args, strict_mode: bool, check_prefix: str |
| 279 | + ) -> Optional[Check]: |
| 280 | + line = line.rstrip() |
| 281 | + |
| 282 | + if not args.strict_whitespace: |
| 283 | + line = canonicalize_whitespace(line) |
| 284 | + |
| 285 | + # CHECK and CHECK-NEXT |
| 286 | + strict_whitespace_match = "" if strict_mode else " *" |
| 287 | + |
| 288 | + check_regex = ( |
| 289 | + f"{BEFORE_PREFIX}({check_prefix}):{strict_whitespace_match}(.*)" |
| 290 | + ) |
| 291 | + |
| 292 | + check_match = re.search(check_regex, line) |
| 293 | + check_type = CheckType.CHECK |
| 294 | + if not check_match: |
| 295 | + check_regex = ( |
| 296 | + f"{BEFORE_PREFIX}({check_prefix}-NEXT):" |
| 297 | + f"{strict_whitespace_match}(.*)" |
| 298 | + ) |
| 299 | + check_match = re.search(check_regex, line) |
| 300 | + check_type = CheckType.CHECK_NEXT |
| 301 | + |
| 302 | + if check_match: |
| 303 | + check_keyword = check_match.group(2) |
| 304 | + check_expression = check_match.group(3) |
| 305 | + if not strict_mode: |
| 306 | + check_expression = check_expression.strip(" ") |
| 307 | + |
| 308 | + match_type = MatchType.SUBSTRING |
| 309 | + |
| 310 | + if re.search(r"\{\{.*\}\}", check_expression): |
| 311 | + regex_line = escape_non_regex_parts(check_expression) |
| 312 | + regex_line = re.sub(r"\{\{(.*?)\}\}", r"\1", regex_line) |
| 313 | + match_type = MatchType.REGEX |
| 314 | + check_expression = regex_line |
| 315 | + if strict_mode: |
| 316 | + if check_expression[0] != "^": |
| 317 | + check_expression = "^" + check_expression |
| 318 | + if check_expression[-1] != "$": |
| 319 | + check_expression = check_expression + "$" |
| 320 | + |
| 321 | + # Replace line number expressions, e.g. `[[# @LINE + 3 ]]` |
| 322 | + line_var_match = re.search(LINE_NUMBER_REGEX, check_expression) |
| 323 | + while line_var_match is not None: |
| 324 | + offset = int(line_var_match.group(2) or 0) |
| 325 | + if line_var_match.group(1) == "-": |
| 326 | + offset = -offset |
| 327 | + check_expression = re.sub( |
| 328 | + LINE_NUMBER_REGEX, |
| 329 | + str(line_idx + offset + 1), |
| 330 | + check_expression, |
| 331 | + 1, |
| 332 | + ) |
| 333 | + line_var_match = re.search(LINE_NUMBER_REGEX, check_expression) |
| 334 | + |
| 335 | + check = Check( |
| 336 | + check_type=check_type, |
| 337 | + match_type=match_type, |
| 338 | + check_keyword=check_keyword, |
| 339 | + expression=check_expression, |
| 340 | + source_line=line, |
| 341 | + check_line_idx=line_idx, |
| 342 | + start_index=check_match.start(3), |
| 343 | + ) |
| 344 | + return check |
| 345 | + |
| 346 | + check_not_regex = ( |
| 347 | + f"{BEFORE_PREFIX}({check_prefix}-NOT):" |
| 348 | + f"{strict_whitespace_match}(.*)" |
| 349 | + ) |
| 350 | + check_match = re.search(check_not_regex, line) |
| 351 | + if check_match: |
| 352 | + match_type = MatchType.SUBSTRING |
| 353 | + |
| 354 | + check_keyword = check_match.group(2) |
| 355 | + check_expression = check_match.group(3) |
| 356 | + if not strict_mode: |
| 357 | + check_expression = check_expression.strip(" ") |
| 358 | + |
| 359 | + if re.search(r"\{\{.*\}\}", check_expression): |
| 360 | + regex_line = escape_non_regex_parts(check_expression) |
| 361 | + regex_line = re.sub(r"\{\{(.*?)\}\}", r"\1", regex_line) |
| 362 | + match_type = MatchType.REGEX |
| 363 | + check_expression = regex_line |
| 364 | + |
| 365 | + check = Check( |
| 366 | + check_type=CheckType.CHECK_NOT, |
| 367 | + match_type=match_type, |
| 368 | + check_keyword=check_keyword, |
| 369 | + expression=check_expression, |
| 370 | + source_line=line, |
| 371 | + check_line_idx=line_idx, |
| 372 | + start_index=check_match.start(3), |
| 373 | + ) |
| 374 | + return check |
| 375 | + |
| 376 | + check_empty_regex = f"{BEFORE_PREFIX}({check_prefix}-EMPTY):" |
| 377 | + check_match = re.search(check_empty_regex, line) |
| 378 | + if check_match: |
| 379 | + check_keyword = check_match.group(2) |
| 380 | + |
| 381 | + check = Check( |
| 382 | + check_type=CheckType.CHECK_EMPTY, |
| 383 | + match_type=MatchType.SUBSTRING, |
| 384 | + check_keyword=check_keyword, |
| 385 | + expression=None, |
| 386 | + source_line=line, |
| 387 | + check_line_idx=line_idx, |
| 388 | + start_index=check_match.start(2), |
| 389 | + ) |
| 390 | + return check |
| 391 | + |
| 392 | + return None |
| 393 | + |
| 394 | + |
244 | 395 | def main(): |
245 | 396 | # Force UTF-8 to be sent to stdout. |
246 | 397 | # https://stackoverflow.com/a/3597849/598057 |
@@ -335,140 +486,21 @@ def exit_handler(code): |
335 | 486 | print(error_message, file=sys.stderr) |
336 | 487 | exit_handler(2) |
337 | 488 |
|
338 | | - checks = [] |
339 | | - with open(check_file_path, encoding="utf-8") as check_file: |
340 | | - for line_idx, line in enumerate(check_file): |
341 | | - line = line.rstrip() |
342 | | - |
343 | | - if not args.strict_whitespace: |
344 | | - line = canonicalize_whitespace(line) |
345 | | - |
346 | | - # CHECK and CHECK-NEXT |
347 | | - strict_whitespace_match = "" if strict_mode else " *" |
348 | | - |
349 | | - check_regex = ( |
350 | | - f"{BEFORE_PREFIX}({check_prefix}):{strict_whitespace_match}(.*)" |
351 | | - ) |
352 | | - |
353 | | - check_match = re.search(check_regex, line) |
354 | | - check_type = CheckType.CHECK |
355 | | - if not check_match: |
356 | | - check_regex = ( |
357 | | - f"{BEFORE_PREFIX}({check_prefix}-NEXT):" |
358 | | - f"{strict_whitespace_match}(.*)" |
359 | | - ) |
360 | | - check_match = re.search(check_regex, line) |
361 | | - check_type = CheckType.CHECK_NEXT |
362 | | - |
363 | | - if check_match: |
364 | | - check_keyword = check_match.group(2) |
365 | | - check_expression = check_match.group(3) |
366 | | - if not strict_mode: |
367 | | - check_expression = check_expression.strip(" ") |
368 | | - |
369 | | - match_type = MatchType.SUBSTRING |
370 | | - |
371 | | - if re.search(r"\{\{.*\}\}", check_expression): |
372 | | - regex_line = escape_non_regex_parts(check_expression) |
373 | | - regex_line = re.sub(r"\{\{(.*?)\}\}", r"\1", regex_line) |
374 | | - match_type = MatchType.REGEX |
375 | | - check_expression = regex_line |
376 | | - if strict_mode: |
377 | | - if check_expression[0] != "^": |
378 | | - check_expression = "^" + check_expression |
379 | | - if check_expression[-1] != "$": |
380 | | - check_expression = check_expression + "$" |
381 | | - |
382 | | - # Replace line number expressions, e.g. `[[# @LINE + 3 ]]` |
383 | | - line_var_match = re.search(LINE_NUMBER_REGEX, check_expression) |
384 | | - while line_var_match is not None: |
385 | | - offset = int(line_var_match.group(2) or 0) |
386 | | - if line_var_match.group(1) == "-": |
387 | | - offset = -offset |
388 | | - check_expression = re.sub( |
389 | | - LINE_NUMBER_REGEX, |
390 | | - str(line_idx + offset + 1), |
391 | | - check_expression, |
392 | | - 1, |
393 | | - ) |
394 | | - line_var_match = re.search( |
395 | | - LINE_NUMBER_REGEX, check_expression |
396 | | - ) |
397 | | - |
398 | | - check = Check( |
399 | | - check_type=check_type, |
400 | | - match_type=match_type, |
401 | | - check_keyword=check_keyword, |
402 | | - expression=check_expression, |
403 | | - source_line=line, |
404 | | - check_line_idx=line_idx, |
405 | | - start_index=check_match.start(3), |
406 | | - ) |
407 | | - |
408 | | - checks.append(check) |
409 | | - continue |
410 | | - |
411 | | - check_not_regex = ( |
412 | | - f"{BEFORE_PREFIX}({check_prefix}-NOT):" |
413 | | - f"{strict_whitespace_match}(.*)" |
414 | | - ) |
415 | | - check_match = re.search(check_not_regex, line) |
416 | | - if check_match: |
417 | | - match_type = MatchType.SUBSTRING |
418 | | - |
419 | | - check_keyword = check_match.group(2) |
420 | | - check_expression = check_match.group(3) |
421 | | - if not strict_mode: |
422 | | - check_expression = check_expression.strip(" ") |
423 | | - |
424 | | - if re.search(r"\{\{.*\}\}", check_expression): |
425 | | - regex_line = escape_non_regex_parts(check_expression) |
426 | | - regex_line = re.sub(r"\{\{(.*?)\}\}", r"\1", regex_line) |
427 | | - match_type = MatchType.REGEX |
428 | | - check_expression = regex_line |
429 | | - |
430 | | - check = Check( |
431 | | - check_type=CheckType.CHECK_NOT, |
432 | | - match_type=match_type, |
433 | | - check_keyword=check_keyword, |
434 | | - expression=check_expression, |
435 | | - source_line=line, |
436 | | - check_line_idx=line_idx, |
437 | | - start_index=check_match.start(3), |
438 | | - ) |
439 | | - |
440 | | - checks.append(check) |
441 | | - continue |
442 | | - |
443 | | - check_empty_regex = f"{BEFORE_PREFIX}({check_prefix}-EMPTY):" |
444 | | - check_match = re.search(check_empty_regex, line) |
445 | | - if check_match: |
446 | | - check_keyword = check_match.group(2) |
447 | | - |
448 | | - check = Check( |
449 | | - check_type=CheckType.CHECK_EMPTY, |
450 | | - match_type=MatchType.SUBSTRING, |
451 | | - check_keyword=check_keyword, |
452 | | - expression=None, |
453 | | - source_line=line, |
454 | | - check_line_idx=line_idx, |
455 | | - start_index=check_match.start(2), |
456 | | - ) |
457 | | - |
458 | | - if len(checks) == 0: |
459 | | - print( |
460 | | - f"{check_file_path}:" |
461 | | - f"{line_idx + 1}:" |
462 | | - f"{check.start_index + 1}: " |
463 | | - f"error: " |
464 | | - f"found 'CHECK-EMPTY' without previous 'CHECK: line" |
465 | | - ) |
466 | | - print(line) |
467 | | - print("^".rjust(check.start_index + 1, " ")) |
468 | | - exit_handler(2) |
469 | | - |
470 | | - checks.append(check) |
471 | | - continue |
| 489 | + try: |
| 490 | + checks = CheckParser.parse_checks_from_file( |
| 491 | + check_file_path, args, strict_mode, check_prefix |
| 492 | + ) |
| 493 | + except CheckParserEmptyCheckException as exception: |
| 494 | + print( |
| 495 | + f"{check_file_path}:" |
| 496 | + f"{exception.check.check_line_idx + 1}:" |
| 497 | + f"{exception.check.start_index + 1}: " |
| 498 | + f"error: " |
| 499 | + f"found 'CHECK-EMPTY' without previous 'CHECK: line" |
| 500 | + ) |
| 501 | + print(exception.check.source_line) |
| 502 | + print("^".rjust(exception.check.start_index + 1, " ")) |
| 503 | + exit_handler(2) |
472 | 504 |
|
473 | 505 | check_iterator = iter(checks) |
474 | 506 |
|
|
0 commit comments