From 18937012f3c3368b357f63f6d6d5bff170ba506d Mon Sep 17 00:00:00 2001 From: Conduction Release Bot Date: Thu, 6 Aug 2026 09:52:34 +0200 Subject: [PATCH] =?UTF-8?q?fix(gate-64):=20a=20named=20constant=20is=20the?= =?UTF-8?q?=20prelude,=20spelled=20better=20=E2=80=94=20not=20a=20missing?= =?UTF-8?q?=20one?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gate-64 recognised the autoload prelude only as a QUOTED LITERAL inside the call parentheses: \OC_App::registerAutoloading('openregister', $path); accepted \OC_App::registerAutoloading(self::OPENREGISTER_APP_ID, $path); REJECTED The second is the same call with the app id given a name, four lines below `private const OPENREGISTER_APP_ID = 'openregister';`. doriath writes it that way, and gate-64 reported it as an AppHost adoption with NO prelude — with the prelude present, correct, and unit-tested. Measured on doriath#163: "FAIL — 2 AppHost adoption(s) with no OpenRegister autoload prelude". A gate that fails the tidier spelling of the thing it is asking for is not reporting a defect; it is teaching people to write the untidy spelling, and it spends the reader's attention on a finding that has nothing behind it. Resolution is deliberately SHALLOW and LITERAL. A name counts only when the same source blob binds it to the exact string 'openregister' — via `const`, `define()` or a plain variable. Imports are not followed, and a name that cannot be seen bound is NOT accepted. That last point is the whole design. "Accept any argument" would clear doriath and simultaneously blind the gate, so two of the four new tests exist to fail in that direction: test_named_constant_bound_to_openregister_is_GREEN doriath's shape passes test_variable_and_define_spellings_are_accepted the other two bindings test_constant_bound_to_another_app_still_FAILS registerAutoloading() for a DIFFERENT app is not a prelude test_unknown_constant_still_FAILS an unresolvable name is not taken on trust Mutation-checked both ways, and the two mutants turn DISJOINT sets red: accept any argument name -> the 2 discriminators fail resolve nothing (the bug) -> the 2 acceptance tests fail Measured against the real trees, which is what settles it: repo gate-64 @main with this fix doriath FAIL (rc 1) PASS (rc 0) false positive cleared scholiq FAIL (rc 1) FAIL (rc 1) real defect, still caught openbuild FAIL (rc 1) FAIL (rc 1) real defect, still caught scholiq and openbuild genuinely have no prelude. openbuild is the acute one: its `class_exists(Bootstrap::class)` guard answers FALSE on a healthy instance because `openbuild` sorts before `openregister`, so Bootstrap::register() has apparently never run there and its generic controllers, install repair steps and deep-link listener are silently absent. Those are fixed in their own repos, not here. 18 gate-64 tests pass. --- .../lib/check_apphost_autoload_prelude.py | 66 +++++++++++++- .../test_check_apphost_autoload_prelude.py | 87 +++++++++++++++++++ 2 files changed, 151 insertions(+), 2 deletions(-) diff --git a/hydra-gates/scripts/lib/check_apphost_autoload_prelude.py b/hydra-gates/scripts/lib/check_apphost_autoload_prelude.py index f05669d..8ec8225 100644 --- a/hydra-gates/scripts/lib/check_apphost_autoload_prelude.py +++ b/hydra-gates/scripts/lib/check_apphost_autoload_prelude.py @@ -93,6 +93,34 @@ PRELUDE = re.compile(r"registerAutoloading\s*\(([^)]*)\)", re.S) OPENREGISTER_LITERAL = re.compile(r"""['"]openregister['"]""") +# A name BOUND to the literal 'openregister' in the same blob. Three spellings: +# const OPENREGISTER_APP_ID = 'openregister'; +# define('OPENREGISTER_APP_ID', 'openregister'); +# $openRegisterAppId = 'openregister'; +# The binding must be to the EXACT literal — this resolves names, it does not +# guess at them, and a name bound to anything else is not a match. +CONST_BINDING = re.compile( + r"""(?: + const\s+(?P[A-Za-z_][A-Za-z0-9_]*)\s*=\s*['"]openregister['"] + | define\s*\(\s*['"](?P[A-Za-z_][A-Za-z0-9_]*)['"]\s*,\s*['"]openregister['"] + | \$(?P[A-Za-z_][A-Za-z0-9_]*)\s*=\s*['"]openregister['"] + )""", + re.X, +) + + +def _binding_name(match: "re.Match[str]") -> str: + return match.group("name1") or match.group("name2") or match.group("name3") + + +# A name USED as a call argument: `self::NAME`, `static::NAME`, `Cls::NAME`, +# a bare `NAME`, or `$var`. Matched against the bound names above, never +# accepted on its own. +ARG_NAME = re.compile( + r"""(?:(?:self|static|parent|[A-Za-z_][A-Za-z0-9_\\]*)\s*::\s*)?""" + r"""\$?(?P[A-Za-z_][A-Za-z0-9_]*)""", +) + # loadApp('openregister') is a WRONG fix, not a prelude — it boots OpenRegister # before its own register() has run. Named so the reader is told why. LOAD_APP = re.compile(r"""loadApp\s*\(\s*['"]openregister['"]\s*\)""") @@ -135,11 +163,45 @@ def _sources(app_dir: str) -> dict[str, str]: return out +def openregister_aliases(text: str) -> set[str]: + """Names bound to the literal 'openregister' somewhere in this text. + + WHY THIS EXISTS. The prelude used to be recognised only as a QUOTED literal + inside the call parentheses: + + \\OC_App::registerAutoloading('openregister', $path); accepted + \\OC_App::registerAutoloading(self::OPENREGISTER_APP_ID, $path); REJECTED + + The second form is the same call with the app id given a name, which is the + better of the two — and doriath, which does exactly that, was reported as + having NO prelude while its prelude sat four lines above the call. A gate + that fails the tidier spelling of the thing it is asking for teaches people + to write the untidy one, and its finding is not about the defect at all. + + Resolution is deliberately SHALLOW and LITERAL: a name counts only when + this same source blob binds it to the exact string 'openregister'. We do + not follow imports, and we do not accept a name we cannot see bound. An + unresolvable name is still a finding — see test_unknown_constant_still_FAILS + and test_constant_bound_to_another_app_still_FAILS, which are what stop this + from becoming "any argument at all satisfies the gate". + """ + return {_binding_name(m) for m in CONST_BINDING.finditer(text)} - {""} + + def has_prelude(text: str) -> bool: - """True when text contains registerAutoloading(... 'openregister' ...).""" + """True when text calls registerAutoloading() for the openregister app. + + Accepts the app id as a quoted literal, or as any name this same blob binds + to that literal (a class constant, a define(), or a plain variable). + """ + aliases = openregister_aliases(text) for m in PRELUDE.finditer(text): - if OPENREGISTER_LITERAL.search(m.group(1)): + args = m.group(1) + if OPENREGISTER_LITERAL.search(args): return True + for ref in ARG_NAME.finditer(args): + if ref.group("name") in aliases: + return True return False diff --git a/hydra-gates/scripts/lib/test_check_apphost_autoload_prelude.py b/hydra-gates/scripts/lib/test_check_apphost_autoload_prelude.py index 54414b6..820f973 100644 --- a/hydra-gates/scripts/lib/test_check_apphost_autoload_prelude.py +++ b/hydra-gates/scripts/lib/test_check_apphost_autoload_prelude.py @@ -124,6 +124,93 @@ def test_same_app_with_the_prelude_is_GREEN(self): self.assertIn("apphost-autoload-prelude: OK", out) +class NamedConstantPreludeTest(GateCase): + """The app id may be given a NAME, and that must not be a finding. + + doriath writes the prelude as + + \\OC_App::registerAutoloading(self::OPENREGISTER_APP_ID, $path); + + with `private const OPENREGISTER_APP_ID = 'openregister';` four lines + above. That is the same call as the documented one, spelled better — and + the gate reported it as having NO prelude, because the matcher required a + QUOTED literal inside the parentheses. + + Measured on doriath#163: gate-64 FAIL, "2 AppHost adoption(s) with no + OpenRegister autoload prelude", with the prelude present and correct. + + The three tests below are one unit. The first is the fix; the second and + third are what stop the fix from being "accept any argument", which would + make the gate blind to the defect it exists to catch. + """ + + CONST_FORM = """getAppPath('openregister'); + \\OC_App::registerAutoloading(self::%s, $path); + Bootstrap::register($context, 'leaf', []); + } +} +""" + + def test_named_constant_bound_to_openregister_is_GREEN(self): + self.app("Application.php", self.CONST_FORM % ( + "OPENREGISTER_APP_ID", "openregister", "OPENREGISTER_APP_ID")) + rc, out = _run(self.dir) + self.assertEqual( + rc, 0, + "a const bound to 'openregister' IS the prelude — this is doriath's shape") + self.assertIn("apphost-autoload-prelude: OK", out) + + def test_constant_bound_to_another_app_still_FAILS(self): + """The discriminator. Same syntax, different binding.""" + self.app("Application.php", self.CONST_FORM % ( + "SOME_OTHER_APP_ID", "opencatalogi", "SOME_OTHER_APP_ID")) + rc, out = _run(self.dir) + self.assertEqual( + rc, 1, + "registerAutoloading() for a DIFFERENT app is not an OpenRegister prelude") + self.assertIn("FAIL", out) + + def test_unknown_constant_still_FAILS(self): + """A name the blob never binds must not be taken on trust.""" + self.app("Application.php", """getAppPath($openRegisterAppId); + \\OC_App::registerAutoloading($openRegisterAppId, $path); + Bootstrap::register($context, 'leaf', []); + } +} +""") + rc, out = _run(self.dir) + self.assertEqual(rc, 0, "a variable bound to the literal is the same prelude") + + class DetectionShapesTest(GateCase): def test_class_exists_probe_without_prelude_is_RED(self): self.app("Application.php", """