Skip to content

Add first-class callable syntax support to NodeExpressionResolver#193

Draft
Copilot wants to merge 4 commits intomasterfrom
copilot/add-callable-syntax-support
Draft

Add first-class callable syntax support to NodeExpressionResolver#193
Copilot wants to merge 4 commits intomasterfrom
copilot/add-callable-syntax-support

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 29, 2026

NodeExpressionResolver had no handler for PHP 8.1+ first-class callable syntax (strlen(...) / Foo::bar(...)), causing a confusing "Cannot statically resolve a variadic placeholder argument" error instead of a meaningful result or clear failure.

Changes

  • resolveExprFuncCall — detects isFirstClassCallable() early; returns Closure::fromCallable($name) for internal functions; for user-defined functions, sets isConstExpr = true before throwing ReflectionException so callers can catch the exception and reconstruct the FCC expression as code for proxy generation
  • resolveExprStaticCall (new) — handles ClassName::method(...); accepts both built-in and user-defined static methods, sets isConstExpr = true, and returns Closure::fromCallable('ClassName::method') (class loading may occur and can be intercepted by the AOP framework)
  • resolveExprMethodCall (new) — always throws a clear ReflectionException; instance method callables require a runtime object and can never be statically resolved
  • ReflectionParameter::getDefaultValueExpression() (new) — returns the string representation of a FCC or other constant-expression default (e.g. '\strlen(...)'), needed to reconstruct proxied code; constructor now catches ReflectionException from FCC resolution and stores the expression string
  • ReflectionProperty::getDefaultValueExpression() (new) — same as above for properties; __toString() updated to use the const-expression string when available
// Built-in function → returns a live Closure
$expressionSolver->process($parser->parse("<?php strlen(...);")[0]);
$closure = $expressionSolver->getValue(); // instanceof \Closure
$closure('foobar'); // 6

// Built-in static method → returns a live Closure
$expressionSolver->process($parser->parse("<?php \DateTime::createFromFormat(...);")[0]);
$closure = $expressionSolver->getValue(); // instanceof \Closure

// User-defined function → throws ReflectionException, but isConstExpression() is true
// so callers can call getConstExpression() → '\Go\MyNs\myFunc(...)'

// User-defined static method → returns a live Closure (class loading is interceptable by AOP)
$expressionSolver->process($parser->parse("<?php \My\Class::method(...);")[0]);
$closure = $expressionSolver->getValue(); // instanceof \Closure

// Instance method → throws ReflectionException
// "First-class callable syntax for instance methods ($obj->method(...)) cannot be resolved
//  statically, as it requires a runtime object instance."

// ReflectionParameter with FCC default
$param->getDefaultValueExpression(); // '\strlen(...)'
(string) $param; // "Parameter #0 [ <optional> $callable = \strlen(...) ]"

Copilot AI changed the title [WIP] Add support for first-class callable syntax in NodeExpressionResolver Add first-class callable syntax support to NodeExpressionResolver Apr 29, 2026
Copilot AI requested a review from lisachenko April 29, 2026 17:30
Copy link
Copy Markdown
Member

@lisachenko lisachenko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing test stub files and checks for ReflectionParameter and ReflectionProperty if they support FCC

Comment thread src/Resolver/NodeExpressionResolver.php
Comment thread src/Resolver/NodeExpressionResolver.php Outdated
if (!is_callable($callable)) {
throw new ReflectionException("'{$callable}' is not callable and cannot be used as a first-class callable.");
}
$reflectedMethod = new \ReflectionMethod($className, $methodName);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should not use here a reflection, this may cause class-loading and it is against our general way of doing this, so we need to set the isConstExpr = true, set the expression itself and then call the Closure::fromCallable

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in c662388 — removed \ReflectionMethod usage entirely. Now sets $this->isConstExpr = true before calling Closure::fromCallable($callable), which handles both built-in and user-defined static methods (with an is_callable() guard for a clean error message).

Comment thread src/Resolver/NodeExpressionResolver.php Outdated
);
}

return \Closure::fromCallable($callable);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use import for Closure without FQN

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in c662388 — added use Closure; import and replaced all \Closure FQN usages in the resolver.

Comment thread src/Resolver/NodeExpressionResolver.php Outdated
throw new ReflectionException("'{$callable}' is not callable and cannot be used as a first-class callable.");
}
$reflectedMethod = new \ReflectionMethod($className, $methodName);
if ($reflectedMethod->isUserDefined()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can accept here even user-defined methods too, this will cause the class-loading, but we can intercept class loading process, wrap method with advices if needed and return back a first-class-callable to woven method, so should be ok

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in c662388 — user-defined static methods are now accepted. The isUserDefined() check via \ReflectionMethod was removed. Closure::fromCallable($callable) is called directly (class-loading will occur, which is fine for the AOP interception use-case).

Comment thread src/Resolver/NodeExpressionResolver.php Outdated
if (!is_string($functionName)) {
throw new ReflectionException("Could not resolve function name for first-class callable.");
}
$reflectedFunction = new \ReflectionFunction($functionName);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use import for ReflectionFunction

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in c662388 — added use ReflectionFunction; to the imports and replaced all \ReflectionFunction FQN usages.

Comment thread tests/Resolver/NodeExpressionResolverTest.php

// Define a user function to test with
if (!function_exists('Go\ParserReflection\Resolver\testUserDefinedFunction')) {
eval('namespace Go\\ParserReflection\\Resolver; function testUserDefinedFunction() {}');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not use eval here, it is prohibited to use eval in the parser-reflection library, just load one from existing tests

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in c662388 — removed eval(). The test now does require_once __DIR__ . '/../Stub/FileWithFunctions55.php' and uses Go\ParserReflection\Stub\simpleNoArgs (an existing user-defined stub function).

$this->expectExceptionMessageMatches('/user-defined function.*cannot be resolved/');

// Define a user function to test with
if (!function_exists('Go\ParserReflection\Resolver\testUserDefinedFunction')) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, do not check the function_exists, as we operating on parser-reflection level and we don't need to load anything in memory

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in c662388 — removed the function_exists() check. The test uses a real stub function via require_once.

Copilot AI and others added 2 commits May 7, 2026 08:16
…ods, stub, fixed tests

Agent-Logs-Url: https://github.com/goaop/parser-reflection/sessions/ba799072-2236-43cf-8f08-6e63ca00dddc

Co-authored-by: lisachenko <640114+lisachenko@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 7, 2026

missing test stub files and checks for ReflectionParameter and ReflectionProperty if they support FCC

Done in c662388:

  • Added tests/Stub/FileWithFunctionsFcc.php — a parse-only stub (not require_once'd, since PHP runtime rejects FCC in constant-expression positions) with functions bearing FCC parameter defaults
  • ReflectionParameter: try/catch in constructor stores const-expression on FCC, getDefaultValueExpression(): ?string added, tests in ReflectionParameterTest
  • ReflectionProperty: same pattern — $isDefaultValueConstExpr field added, __toString() updated, getDefaultValueExpression(): ?string added, contract test in ReflectionPropertyTest

Copilot AI requested a review from lisachenko May 7, 2026 08:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Add first-class callable syntax support to NodeExpressionResolver

2 participants