Skip to content

Commit df257b3

Browse files
authored
fix(core): async functions have undefined paths (#2304)
1 parent 8dde6f3 commit df257b3

File tree

2 files changed

+67
-29
lines changed

2 files changed

+67
-29
lines changed

packages/core/src/__tests__/linter.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,4 +1540,55 @@ responses:: !!foo
15401540
]);
15411541
});
15421542
});
1543+
1544+
test.concurrent('should retain path in async functions', async () => {
1545+
const spectral = new Spectral();
1546+
const documentUri = path.join(__dirname, './__fixtures__/test.json');
1547+
spectral.setRuleset({
1548+
rules: {
1549+
'valid-type': {
1550+
given: '$..type',
1551+
then: {
1552+
async function() {
1553+
return [
1554+
{
1555+
message: 'Restricted type',
1556+
},
1557+
];
1558+
},
1559+
},
1560+
},
1561+
},
1562+
});
1563+
1564+
const document = new Document(
1565+
JSON.stringify({
1566+
oneOf: [
1567+
{
1568+
type: ['number'],
1569+
},
1570+
{
1571+
type: ['string'],
1572+
},
1573+
],
1574+
}),
1575+
Parsers.Json,
1576+
documentUri,
1577+
);
1578+
1579+
const results = spectral.run(document);
1580+
1581+
await expect(results).resolves.toEqual([
1582+
expect.objectContaining({
1583+
code: 'valid-type',
1584+
path: ['oneOf', '0', 'type'],
1585+
severity: DiagnosticSeverity.Warning,
1586+
}),
1587+
expect.objectContaining({
1588+
code: 'valid-type',
1589+
path: ['oneOf', '1', 'type'],
1590+
severity: DiagnosticSeverity.Warning,
1591+
}),
1592+
]);
1593+
});
15431594
});

packages/core/src/runner/lintNode.ts

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,34 @@
1-
import { JsonPath } from '@stoplight/types';
21
import { decodeSegmentFragment, getClosestJsonPath, printPath, PrintStyle } from '@stoplight/spectral-runtime';
32
import { get, isError } from 'lodash';
43
import { ErrorWithCause } from 'pony-cause';
54

65
import { Document } from '../document';
7-
import { IFunctionResult, IGivenNode, RulesetFunctionContext } from '../types';
8-
import { IRunnerInternalContext } from './types';
6+
import type { IFunctionResult, IGivenNode, RulesetFunctionContext } from '../types';
7+
import type { IRunnerInternalContext } from './types';
98
import { getLintTargets, MessageVars, message } from './utils';
10-
import { Rule } from '../ruleset/rule';
9+
import type { Rule } from '../ruleset/rule';
1110

1211
export const lintNode = (context: IRunnerInternalContext, node: IGivenNode, rule: Rule): void => {
13-
const fnContext: RulesetFunctionContext = {
12+
const givenPath = node.path.length > 0 && node.path[0] === '$' ? node.path.slice(1) : node.path.slice();
13+
14+
const fnContext: RulesetFunctionContext & { rule: Rule } = {
1415
document: context.documentInventory.document,
1516
documentInventory: context.documentInventory,
1617
rule,
17-
path: [],
18+
path: givenPath,
1819
};
1920

20-
const givenPath = node.path.length > 0 && node.path[0] === '$' ? node.path.slice(1) : node.path;
21-
2221
for (const then of rule.then) {
2322
const targets = getLintTargets(node.value, then.field);
2423

2524
for (const target of targets) {
26-
const path = target.path.length > 0 ? [...givenPath, ...target.path] : givenPath;
25+
if (target.path.length > 0) {
26+
fnContext.path = [...givenPath, ...target.path];
27+
}
2728

2829
let targetResults;
2930
try {
30-
targetResults = then.function(target.value, then.functionOptions ?? null, {
31-
...fnContext,
32-
path,
33-
});
31+
targetResults = then.function(target.value, then.functionOptions ?? null, fnContext);
3432
} catch (e) {
3533
throw new ErrorWithCause(
3634
`Function "${then.function.name}" threw an exception${isError(e) ? `: ${e.message}` : ''}`,
@@ -43,36 +41,25 @@ export const lintNode = (context: IRunnerInternalContext, node: IGivenNode, rule
4341
if (targetResults === void 0) continue;
4442

4543
if ('then' in targetResults) {
44+
const _fnContext = { ...fnContext };
4645
context.promises.push(
4746
targetResults.then(results =>
48-
results === void 0
49-
? void 0
50-
: void processTargetResults(
51-
context,
52-
results,
53-
rule,
54-
path, // todo: get rid of it somehow.
55-
),
47+
results === void 0 ? void 0 : processTargetResults(context, _fnContext, results),
5648
),
5749
);
5850
} else {
59-
processTargetResults(
60-
context,
61-
targetResults,
62-
rule,
63-
path, // todo: get rid of it somehow.
64-
);
51+
processTargetResults(context, fnContext, targetResults);
6552
}
6653
}
6754
}
6855
};
6956

7057
function processTargetResults(
7158
context: IRunnerInternalContext,
59+
fnContext: RulesetFunctionContext & { rule: Rule },
7260
results: IFunctionResult[],
73-
rule: Rule,
74-
targetPath: JsonPath,
7561
): void {
62+
const { rule, path: targetPath } = fnContext;
7663
for (const result of results) {
7764
const escapedJsonPath = (result.path ?? targetPath).map(decodeSegmentFragment);
7865
const associatedItem = context.documentInventory.findAssociatedItemForPath(escapedJsonPath, rule.resolved);

0 commit comments

Comments
 (0)