Skip to content

Commit 4054c05

Browse files
committed
feat: add subscript access expression support for dataset static analysis
Support $v[0] string/array index access in data provider evaluation, enabling conditional yield patterns like `if ($v[0] === 'a')` in foreach loops.
1 parent f4c8050 commit 4054c05

6 files changed

Lines changed: 66 additions & 1 deletion

File tree

packages/phpunit/src/Interpreter/AstParser/AstNode.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,13 @@ export interface ConstDeclarationNode {
257257
loc?: AstNodeLoc;
258258
}
259259

260+
export interface SubscriptAccessNode {
261+
kind: 'subscript_access_expression';
262+
object: AstNode;
263+
index: AstNode;
264+
loc?: AstNodeLoc;
265+
}
266+
260267
export interface ArrayCreationNode {
261268
kind: 'array_creation_expression';
262269
entries: ArrayEntryNode[];
@@ -317,4 +324,5 @@ export type AstNode =
317324
| VariableNode
318325
| NumberNode
319326
| ClassConstantAccessNode
320-
| ConstDeclarationNode;
327+
| ConstDeclarationNode
328+
| SubscriptAccessNode;

packages/phpunit/src/Interpreter/AstParser/PhpParser/PhpParserAdapter.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ function adaptNode(raw: RawNode): AstNode | undefined {
135135
alternate: adaptNode(raw.falseExpr) ?? EMPTY_STRING_NODE,
136136
loc: convertLoc(raw.loc),
137137
};
138+
case 'offsetlookup':
139+
return {
140+
kind: 'subscript_access_expression',
141+
object: adaptNode(raw.what) ?? EMPTY_STRING_NODE,
142+
index: adaptNode(raw.offset) ?? EMPTY_STRING_NODE,
143+
loc: convertLoc(raw.loc),
144+
};
138145
case 'parenthesis':
139146
return raw.inner ? adaptNode(raw.inner) : undefined;
140147
default:

packages/phpunit/src/Interpreter/AstParser/TreeSitter/TreeSitterAdapter.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,16 @@ const expressionAdapters: Record<string, (node: SyntaxNode) => AstNode> = {
789789
name: node.namedChildren[1]?.text ?? '',
790790
loc: locOf(node),
791791
}),
792+
subscript_expression: (node) => {
793+
const object = node.namedChildren[0];
794+
const index = node.namedChildren[1];
795+
return {
796+
kind: 'subscript_access_expression',
797+
object: object ? adaptExpression(object) : EMPTY_STRING_NODE,
798+
index: index ? adaptExpression(index) : EMPTY_STRING_NODE,
799+
loc: locOf(node),
800+
};
801+
},
792802
anonymous_function_creation_expression: adaptClosure,
793803
anonymous_function: adaptClosure,
794804
arrow_function: adaptArrowFunction,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { AstNode, SubscriptAccessNode } from '../../AstParser/AstNode';
2+
import type { Context, Expression } from '../Expression';
3+
4+
class SubscriptAccessExpression implements Expression<unknown> {
5+
supports(node: AstNode): boolean {
6+
return node.kind === 'subscript_access_expression';
7+
}
8+
9+
resolve(node: AstNode, context: Context): unknown {
10+
const { object, index } = node as SubscriptAccessNode;
11+
const obj = context.resolve(object);
12+
const idx = context.resolve(index);
13+
14+
if ((typeof obj === 'string' || Array.isArray(obj)) && typeof idx === 'number') {
15+
return obj[idx];
16+
}
17+
18+
return undefined;
19+
}
20+
}
21+
22+
export const subscriptAccessExpression = new SubscriptAccessExpression();

packages/phpunit/src/Interpreter/Expressions/PhpExpression.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { classConstantAccessExpression } from './Iterables/ClassConstantAccessEx
2121
import { encapsedStringExpression } from './Literals/EncapsedStringExpression';
2222
import { numberLiteralExpression } from './Literals/NumberLiteralExpression';
2323
import { stringLiteralExpression } from './Literals/StringLiteralExpression';
24+
import { subscriptAccessExpression } from './Literals/SubscriptAccessExpression';
2425
import { variableExpression } from './Literals/VariableExpression';
2526
import { binaryExpr } from './Operators/BinaryExpression';
2627
import { conditionalExpression } from './Operators/ConditionalExpression';
@@ -41,6 +42,7 @@ const allExpressions: Expression<unknown>[] = [
4142
variableExpression,
4243
stringLiteralExpression,
4344
numberLiteralExpression,
45+
subscriptAccessExpression,
4446
encapsedStringExpression,
4547
binaryExpr,
4648
conditionalExpression,

packages/phpunit/src/Interpreter/evaluate.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,22 @@ describe.each(parsers)('DataProviderParser (%s)', (_name, createParser) => {
515515
]);
516516
});
517517

518+
it('foreach loop with conditional yield using subscript access', () => {
519+
const method = givenMethod(
520+
`<?php class FooTest extends \\PHPUnit\\Framework\\TestCase {
521+
public static function provider() {
522+
foreach (['apple', 'banana', 'avocado', 'cherry'] as $v) {
523+
if ($v[0] === 'a') {
524+
yield $v => [$v];
525+
}
526+
}
527+
}
528+
}`,
529+
'provider',
530+
);
531+
expect(parseDataProvider(method)).toEqual(['data set "apple"', 'data set "avocado"']);
532+
});
533+
518534
it('yield key with strtoupper', () => {
519535
const method = givenMethod(
520536
`<?php class FooTest extends \\PHPUnit\\Framework\\TestCase {

0 commit comments

Comments
 (0)