@biffo/cli 0.253.9 → 0.253.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -254,13 +254,33 @@ def _template_literals(text: str):
254
254
 
255
255
  _PARAM_SEGMENT = re.compile(r"\{[^}]*\}")
256
256
 
257
- _BLOCK_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL)
258
- # From the first `//` preceded by whitespace or start-of-line, to end of line.
259
- # NOT from the first bare `//`, which would also truncate a `'https://…'`
260
- # string literal — see the module docstring's "Comments are stripped first"
261
- # section for the real false positives (tabsii-crm, tabsii-lms) this exists
262
- # to stop.
263
- _LINE_COMMENT = re.compile(r"(?:^|(?<=\s))//.*$", re.MULTILINE)
257
+ # ONE alternation, scanned left to right, so whichever comment construct opens
258
+ # FIRST consumes the other (#1374). Two separate passes cannot express that,
259
+ # and the order they ran in was a live fail-open:
260
+ #
261
+ # text = _BLOCK_COMMENT.sub(...) # ran first
262
+ # return _LINE_COMMENT.sub(...) # ran second
263
+ #
264
+ # A `/*` appearing as PROSE inside a `//` comment was therefore read as a real
265
+ # block-comment opener, and everything up to the file's next literal `*/` —
266
+ # in JSX, typically a `{/* ... */}` far below — was deleted. `tabsii-geo`'s
267
+ # `MapView.tsx` carried `// exclude everything under basemap/*) from the S3
268
+ # deploy`, which swallowed ~180 lines including BOTH of that repo's real call
269
+ # sites. The guard extracted 0 paths and PASSED.
270
+ #
271
+ # `re.sub` takes the leftmost match, so a single alternation gets this right in
272
+ # both directions: a `//` opening first consumes any `/*` on its line, and a
273
+ # `/*` opening first consumes any `//` inside it.
274
+ #
275
+ # The `//` branch requires whitespace or start-of-line before it, NOT a bare
276
+ # `//`, which would also truncate a `'https://…'` string literal — see the
277
+ # module docstring's "Comments are stripped first" section for the real false
278
+ # positives (tabsii-crm, tabsii-lms) that rule exists to stop.
279
+ #
280
+ # An UNTERMINATED `/*` matches nothing and is left in place, deliberately:
281
+ # deleting to end-of-file on a malformed comment is the very blindness this
282
+ # fix exists to remove.
283
+ _COMMENT = re.compile(r"/\*.*?\*/|(?:^|(?<=\s))//[^\n]*", re.DOTALL | re.MULTILINE)
264
284
 
265
285
  API_PREFIX = "/api/v1/"
266
286
 
@@ -269,14 +289,17 @@ def _strip_comments(text: str) -> str:
269
289
  """Remove `/* ... */` and `// ...` so a comment merely mentioning a path
270
290
  — in prose, as documentation — is never mistaken for a call site.
271
291
 
272
- A block comment is replaced by the same number of newlines it contained,
273
- not deleted outright — this codebase leans heavily on multi-line `/** */`
292
+ A comment is replaced by the same number of newlines it contained, not
293
+ deleted outright — this codebase leans heavily on multi-line `/** */`
274
294
  JSDoc, and dropping those newlines would shift every subsequent line
275
295
  number, misdirecting a failure message at the wrong line for anything
276
- below one.
296
+ below one. A `//` comment contains no newline, so the same substitution
297
+ correctly erases it to nothing.
298
+
299
+ See `_COMMENT` for why this is one pass rather than two, and for the
300
+ fail-open (#1374) that the two-pass version caused.
277
301
  """
278
- text = _BLOCK_COMMENT.sub(lambda m: "\n" * m.group(0).count("\n"), text)
279
- return _LINE_COMMENT.sub("", text)
302
+ return _COMMENT.sub(lambda m: "\n" * m.group(0).count("\n"), text)
280
303
 
281
304
 
282
305
  @dataclass(frozen=True)
@@ -643,6 +666,62 @@ def path_is_registered(fastapi_app: FastAPI, normalized: str) -> bool:
643
666
  return False
644
667
 
645
668
 
669
+ def test_the_extractor_is_not_blind() -> None:
670
+ """Zero extracted paths must mean "none there", never "could not see".
671
+
672
+ `test_every_frontend_api_v1_path_is_registered_on_the_bff` cannot tell the
673
+ difference on its own: it iterates whatever the extractor returned, so an
674
+ extractor that returns nothing passes it trivially. Every other safeguard
675
+ in this file — `unresolved` failing rather than skipping, the reported
676
+ denominator, the declared external allowlist — protects against a path
677
+ that is SEEN and cannot be resolved. None of them protects against text
678
+ the extractor never receives.
679
+
680
+ That is not hypothetical. #1374: `_strip_comments` ran its block-comment
681
+ pass before its line-comment pass, so a `/*` in prose inside a `//`
682
+ comment opened a phantom block that deleted ~180 lines of `tabsii-geo`'s
683
+ `MapView.tsx`, including both of its real call sites. The suite reported
684
+ `0 found ... 0 did not match` and passed. It was caught only because the
685
+ sweep that adopted this guard required a raw-grep cross-check of the
686
+ extracted count rather than trusting a green test.
687
+
688
+ So the cross-check is now part of the guard. Comparing against the RAW
689
+ text — before any stripping — is the point: the raw scan is the one thing
690
+ a bug in the stripping cannot affect.
691
+
692
+ **What this does and does not catch.** It catches TOTAL blindness over the
693
+ file set, which is the shape a stripping bug takes when it swallows a
694
+ span. It does not catch partial loss — three call sites eaten while a
695
+ fourth survives still passes here. Stated plainly rather than overclaimed;
696
+ the one-pass `_COMMENT` scanner is what addresses the partial case, and
697
+ this is the backstop for the next bug of a kind nobody has thought of.
698
+
699
+ A repo with genuinely no in-source API calls still passes, because the raw
700
+ scan reads exactly the same file set the extractor does — test files
701
+ excluded. `tabsii-app` is the real example: its only `/api/v1` text lives
702
+ in `lib/api-client.test.ts`, so raw and extracted are both 0 and that is
703
+ an honest clean.
704
+ """
705
+ files = _frontend_source_files()
706
+ raw_hits = [(f, f.read_text(encoding="utf-8").count(API_PREFIX)) for f in files]
707
+ raw_total = sum(n for _, n in raw_hits)
708
+
709
+ extracted = 0
710
+ for file in files:
711
+ extracted += len(extract_api_paths(file.read_text(encoding="utf-8"), file))
712
+
713
+ if raw_total and not extracted:
714
+ offenders = "\n".join(f" {_relative(f)}: {n} raw occurrence(s)" for f, n in raw_hits if n)
715
+ raise AssertionError(
716
+ f"The extractor found 0 `{API_PREFIX}` path(s) across "
717
+ f"{len(files)} frontend source file(s), but the raw text of those "
718
+ f"same files contains {raw_total}. That is blindness, not a clean "
719
+ f"pass — something is consuming the source before extraction "
720
+ f"reaches it (#1374 was exactly this: a `/*` in prose inside a "
721
+ f"`//` comment swallowing the rest of the file).\n{offenders}"
722
+ )
723
+
724
+
646
725
  def test_every_frontend_api_v1_path_is_registered_on_the_bff() -> None:
647
726
  extracted: list[ExtractedPath] = []
648
727
  for file in _frontend_source_files():
@@ -806,6 +885,63 @@ class TestExtractApiPaths:
806
885
  assert len(found) == 1
807
886
  assert found[0].normalized == "/api/v1/whoami"
808
887
 
888
+ def test_a_block_opener_in_prose_inside_a_line_comment_eats_nothing(self) -> None:
889
+ """#1374, reduced from `tabsii-geo`'s `MapView.tsx` verbatim.
890
+
891
+ The `basemap/*)` in that comment is prose — a glob in an English
892
+ sentence about an S3 deploy exclusion. The two-pass stripper read its
893
+ `/*` as a real block-comment opener and deleted everything up to the
894
+ next literal `*/`, which in a `.tsx` file is the JSX comment below.
895
+ Both real call sites vanished and the suite reported a clean pass.
896
+ """
897
+ found = extract_api_paths(
898
+ "// exclude everything under basemap/*) from the S3 deploy\n"
899
+ "api.get('/api/v1/units')\n"
900
+ "api.get('/api/v1/regions')\n"
901
+ "{/* an ordinary JSX comment */}\n"
902
+ "api.get('/api/v1/brands')\n",
903
+ Path("MapView.tsx"),
904
+ )
905
+ assert [p.normalized for p in found] == [
906
+ "/api/v1/units",
907
+ "/api/v1/regions",
908
+ "/api/v1/brands",
909
+ ]
910
+
911
+ def test_a_line_comment_marker_inside_a_block_comment_eats_nothing(self) -> None:
912
+ """The mirror case, which a naive "just swap the two passes" fix
913
+ breaks: a `//` inside a block comment must not end the block early and
914
+ leave its `*/` behind as live source."""
915
+ found = extract_api_paths(
916
+ "/* see //example.com for why\n"
917
+ " this used to call /api/v1/legacy */\n"
918
+ "api.get('/api/v1/whoami')\n",
919
+ Path("x.ts"),
920
+ )
921
+ assert [p.normalized for p in found] == ["/api/v1/whoami"]
922
+
923
+ def test_an_unterminated_block_comment_does_not_eat_the_rest_of_the_file(
924
+ self,
925
+ ) -> None:
926
+ """Deleting to end-of-file on a malformed comment is the same
927
+ blindness in a different costume, so an unterminated `/*` is left in
928
+ place and the code after it is still read."""
929
+ found = extract_api_paths(
930
+ "/* oops, never closed\napi.get('/api/v1/whoami')\n",
931
+ Path("x.ts"),
932
+ )
933
+ assert [p.normalized for p in found] == ["/api/v1/whoami"]
934
+
935
+ def test_comment_stripping_still_preserves_line_numbers(self) -> None:
936
+ """The one-pass scanner must keep the property the two-pass version
937
+ had: a failure message points at the real line."""
938
+ found = extract_api_paths(
939
+ "/* a\n multi\n line\n comment */\napi.get('/api/v1/whoami')\n",
940
+ Path("x.ts"),
941
+ )
942
+ assert len(found) == 1
943
+ assert found[0].line == 5
944
+
809
945
  def test_a_url_containing_double_slash_is_not_mistaken_for_a_comment(self) -> None:
810
946
  found = extract_api_paths(
811
947
  "const base = 'https://example.com'\napi.get('/api/v1/whoami')",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.253.9",
3
+ "version": "0.253.11",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",