Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve dependency resolution, XPath static analysis; support current() in computations, repeat-based itemsets, relative body references #166

Merged
merged 32 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d9f5783
Warn on missing repeats, add explicit creation when missing
eyelidlessness Jul 4, 2024
840937b
XPath static analysis overhaul: improved function call analysis, path…
eyelidlessness Jul 8, 2024
722d06d
Fix (alignment with Collect): handle relative nodeset references in f…
eyelidlessness Jul 8, 2024
f33e22a
Fix: ensure that itemset nodeset expression is resolved in select con…
eyelidlessness Jul 8, 2024
4b24332
Update all DependentExpressions to use improved dependency analysis l…
eyelidlessness Jul 8, 2024
e41b24f
Remove old, now unused XPath analysis logic
eyelidlessness Jul 8, 2024
fd82774
Update passing secondary-instances test
eyelidlessness Jul 8, 2024
746baf8
Scenario: implement `canCreateNewRepeat`
eyelidlessness Jul 8, 2024
c49bc82
Update now-passing test checking that creation of count-controlled re…
eyelidlessness Jul 8, 2024
c83a049
Remove scenario test accommodations for relative body references
eyelidlessness Jul 8, 2024
71250dc
Update several repeat tests now passing
eyelidlessness Jul 8, 2024
717ac4c
Update select tests now passing
eyelidlessness Jul 8, 2024
7ef5ad5
Update select tests now passing with explicit repeat creation
eyelidlessness Jul 8, 2024
d0dd2ec
Update current tests now passing
eyelidlessness Jul 8, 2024
ce44512
Update current test now passing with explicit repeat creation
eyelidlessness Jul 8, 2024
0c1534d
Add changeset
eyelidlessness Jul 15, 2024
7cb2a5c
Correct typo `resovledPathList` -> `resolvedPathList`
eyelidlessness Jul 19, 2024
9d72a32
Correct dependency analysis null context
eyelidlessness Jul 19, 2024
413819d
Remove `null`-as-keyword special case
eyelidlessness Jul 19, 2024
c5b8ac7
Split `resolveDependencyNodesets` set-like behavior tests
eyelidlessness Jul 19, 2024
1d4de28
JSDoc todos on current/instance expression predicates reference grammar
eyelidlessness Jul 19, 2024
d415510
Add tests for @sadiqkhoja’s questions 1-5
eyelidlessness Jul 23, 2024
7c3f513
Address @sadiqkhoja’s question 6
eyelidlessness Jul 23, 2024
918062d
Add tests demonstrating `ignoreReferenceToContextPath` option
eyelidlessness Jul 23, 2024
24a30ec
Test same path sub-expression as argument of FilterExpr path, closer …
eyelidlessness Jul 23, 2024
057748b
Fix: resolve paths in Arguments of also-resolved paths beginning with…
eyelidlessness Jul 23, 2024
b58ac5b
Capture findings of timeboxed spike on `child::text()`…
eyelidlessness Jul 23, 2024
fe91584
Simplify conditional `contextNode` lookup in dependency analysis
eyelidlessness Jul 25, 2024
47b4fbc
Correct JSDoc for `isCompleteSubExpression`
eyelidlessness Jul 25, 2024
16719b4
Correct non-empty tuples in all cases where that was intended
eyelidlessness Jul 25, 2024
0c0adc4
Fix: principal expression lookup short-circuit
eyelidlessness Jul 25, 2024
b07023e
Fix: `isTranslationFunctionCall` doesn’t care about `jr:itext` Argume…
eyelidlessness Jul 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/itchy-icons-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@getodk/scenario": minor
"@getodk/xforms-engine": minor
---

- Support tracking `current()` references in computations
- Support tracking references in computations where path references include predicates
- Improve support for repeat-based itemsets
- Improve relative path resolution across the board, fixing many computation update edge cases where expressions include complex path expressions
- Support relative `ref`/`nodeset` body attributes, as well as those with a `current()/` prefix
- Improve function call analysis in XPath expressions, particularly identification of functions called with no arguments
- Lay more mature foundation for general syntax analysis of XPath expressions
128 changes: 123 additions & 5 deletions packages/scenario/src/jr/Scenario.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { XFormsElement } from '@getodk/common/test/fixtures/xform-dsl/XFormsElement.ts';
import type { AnyNode, RootNode, SelectNode } from '@getodk/xforms-engine';
import type { AnyNode, RepeatRangeNode, RootNode, SelectNode } from '@getodk/xforms-engine';
import type { Accessor, Setter } from 'solid-js';
import { createMemo, createSignal, runWithOwner } from 'solid-js';
import { afterEach, expect } from 'vitest';
Expand Down Expand Up @@ -63,6 +63,10 @@ interface CreateNewRepeatAssertedReferenceOptions {
readonly assertCurrentReference: string;
}

export interface ExplicitRepeatCreationOptions {
readonly explicitRepeatCreation: boolean;
}

// prettier-ignore
type GetQuestionAtIndexParameters<
ExpectedQuestionType extends QuestionNodeType
Expand Down Expand Up @@ -304,6 +308,8 @@ export class Scenario {
});

if (index === -1) {
this.logMissingRepeatAncestor(reference);

throw new Error(
`Setting answer to ${reference} failed: could not locate question/positional event with that reference.`
);
Expand Down Expand Up @@ -457,6 +463,16 @@ export class Scenario {
}

const { node } = event;

// TODO: we should probably remove this when we add support for `jr:count`
// and `jr:noAddRemove`. There will likely be other, engine/client API-level
// restrictions which address this. For now, this is intended to ensure that
// we don't mistakenly introduce new explicit repeat creation calls where
// the repeat under test is count/fixed.
if (!this.proposed_canCreateNewRepeat(repeatReference)) {
throw new Error(`Repeat is/will be engine controlled: ${repeatReference}`);
}

const { reference } = node.currentState;
const repeatRange = getClosestRepeatRange(node);

Expand Down Expand Up @@ -521,6 +537,10 @@ export class Scenario {
throw new Error('Cannot remove repeat instance, failed to find its parent repeat range');
}

if (!this.isClientControlled(repeatRange)) {
throw new Error('Cannot remove repeat instance: repeat range is engine controlled');
}

const repeatIndex = repeatRange.currentState.children.indexOf(event.node);

if (repeatIndex === -1) {
Expand Down Expand Up @@ -655,6 +675,102 @@ export class Scenario {
return event.eventType === 'END_OF_FORM';
}

private suppressMissingRepeatAncestorLogs = false;

private logMissingRepeatAncestor(reference: string): void {
if (this.suppressMissingRepeatAncestorLogs) {
return;
}

const [, positionPredicatedReference, positionExpression] =
reference.match(/^(.*\/[^/[]+)\[(\d+)\]\/[^[]+$/) ?? [];

if (positionPredicatedReference == null || positionExpression == null) {
return;
}

if (/\[\d+\]/.test(positionPredicatedReference)) {
this.logMissingRepeatAncestor(positionPredicatedReference);
}

const position = parseInt(positionExpression, 10);

if (Number.isNaN(position) || position < 1) {
throw new Error(
`Cannot log missing repeat ancestor for reference (invalid position predicate): ${reference} (repeatRangeReference: ${positionPredicatedReference}, positionExpression: ${positionExpression})`
);
}

try {
const ancestorNode = this.getInstanceNode(positionPredicatedReference);

if (ancestorNode.nodeType !== 'repeat-range') {
// eslint-disable-next-line no-console
console.trace(
'Unexpected position predicate for ancestor reference:',
positionPredicatedReference,
'position:',
position
);

return;
}

if (!this.proposed_canCreateNewRepeat(positionPredicatedReference)) {
return;
}

const index = position - 1;
const repeatInstances = ancestorNode.currentState.children;

if (repeatInstances[index] == null) {
// eslint-disable-next-line no-console
console.trace(
'Missing repeat in range:',
positionPredicatedReference,
'position:',
position,
'index:',
index,
'actual instances present:',
repeatInstances.length
);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);

return;
}
}

proposed_addExplicitCreateNewRepeatCallHere(
reference: string,
options: ExplicitRepeatCreationOptions
): unknown {
if (options.explicitRepeatCreation) {
return this.createNewRepeat(reference);
}

this.suppressMissingRepeatAncestorLogs = true;

return;
}

/** @todo this should change when we support `jr:count` */
private isCountControlled(node: RepeatRangeNode): boolean {
return node.definition.bodyElement.countExpression != null;
}

/** @todo this should change when we support `jr:noAddRemove` */
private isFixedCount(node: RepeatRangeNode): boolean {
return node.definition.bodyElement.isFixedCount;
}

private isClientControlled(node: RepeatRangeNode): boolean {
return !this.isCountControlled(node) && !this.isFixedCount(node);
}

/**
* **PORTING NOTES**
*
Expand All @@ -668,15 +784,17 @@ export class Scenario {
proposed_canCreateNewRepeat(repeatRangeReference: string): boolean {
const node = getNodeForReference(this.instanceRoot, repeatRangeReference);

if (node != null && node.nodeType !== 'repeat-range') {
if (node == null) {
return false;
}

if (node.nodeType !== 'repeat-range') {
throw new Error(
`Expected a repeat range with reference ${repeatRangeReference}, found a node of type ${node.nodeType}`
);
}

throw new ImplementationPendingError(
'determining whether or not a repeat range supports client-invoked addition of repeat instances'
);
return this.isClientControlled(node);
}

/**
Expand Down
Loading