Skip to content

Commit

Permalink
fix(core): async functions have undefined paths
Browse files Browse the repository at this point in the history
  • Loading branch information
P0lip committed Oct 3, 2022
1 parent 9050785 commit 0cf0d2e
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 29 deletions.
61 changes: 61 additions & 0 deletions packages/core/src/__tests__/linter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1540,4 +1540,65 @@ responses:: !!foo
]);
});
});

test.concurrent('should retain path in async functions', async () => {
jest.useFakeTimers();

const spectral = new Spectral();
const documentUri = path.join(__dirname, './__fixtures__/test.json');
spectral.setRuleset({
rules: {
'valid-type': {
given: '$..type',
then: {
function(_input, _opts, ctx) {
return new Promise(resolve => {
setTimeout(() => {
resolve([
{
path: [...ctx.path, '0'],
message: 'Restricted type',
},
]);
}, 500);
});
},
},
},
},
});

const document = new Document(
JSON.stringify({
oneOf: [
{
type: ['number'],
},
{
type: ['string'],
},
],
}),
Parsers.Json,
documentUri,
);

const results = spectral.run(document);

jest.advanceTimersByTime(500);
jest.useRealTimers();

await expect(results).resolves.toEqual([
expect.objectContaining({
code: 'valid-type',
path: ['oneOf', '0', 'type', '0'],
severity: DiagnosticSeverity.Warning,
}),
expect.objectContaining({
code: 'valid-type',
path: ['oneOf', '1', 'type', '0'],
severity: DiagnosticSeverity.Warning,
}),
]);
});
});
45 changes: 16 additions & 29 deletions packages/core/src/runner/lintNode.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,34 @@
import { JsonPath } from '@stoplight/types';
import { decodeSegmentFragment, getClosestJsonPath, printPath, PrintStyle } from '@stoplight/spectral-runtime';
import { get, isError } from 'lodash';
import { ErrorWithCause } from 'pony-cause';

import { Document } from '../document';
import { IFunctionResult, IGivenNode, RulesetFunctionContext } from '../types';
import { IRunnerInternalContext } from './types';
import type { IFunctionResult, IGivenNode, RulesetFunctionContext } from '../types';
import type { IRunnerInternalContext } from './types';
import { getLintTargets, MessageVars, message } from './utils';
import { Rule } from '../ruleset/rule';
import type { Rule } from '../ruleset/rule';

export const lintNode = (context: IRunnerInternalContext, node: IGivenNode, rule: Rule): void => {
const fnContext: RulesetFunctionContext = {
const givenPath = node.path.length > 0 && node.path[0] === '$' ? node.path.slice(1) : node.path.slice();

const fnContext: RulesetFunctionContext & { rule: Rule } = {
document: context.documentInventory.document,
documentInventory: context.documentInventory,
rule,
path: [],
path: givenPath,
};

const givenPath = node.path.length > 0 && node.path[0] === '$' ? node.path.slice(1) : node.path;

for (const then of rule.then) {
const targets = getLintTargets(node.value, then.field);

for (const target of targets) {
const path = target.path.length > 0 ? [...givenPath, ...target.path] : givenPath;
if (target.path.length > 0) {
fnContext.path = [...givenPath, ...target.path];
}

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

if ('then' in targetResults) {
const _fnContext = { ...fnContext };
context.promises.push(
targetResults.then(results =>
results === void 0
? void 0
: void processTargetResults(
context,
results,
rule,
path, // todo: get rid of it somehow.
),
results === void 0 ? void 0 : processTargetResults(context, _fnContext, results),
),
);
} else {
processTargetResults(
context,
targetResults,
rule,
path, // todo: get rid of it somehow.
);
processTargetResults(context, fnContext, targetResults);
}
}
}
};

function processTargetResults(
context: IRunnerInternalContext,
fnContext: RulesetFunctionContext & { rule: Rule },
results: IFunctionResult[],
rule: Rule,
targetPath: JsonPath,
): void {
const { rule, path: targetPath } = fnContext;
for (const result of results) {
const escapedJsonPath = (result.path ?? targetPath).map(decodeSegmentFragment);
const associatedItem = context.documentInventory.findAssociatedItemForPath(escapedJsonPath, rule.resolved);
Expand Down

0 comments on commit 0cf0d2e

Please sign in to comment.