From 014b96d2bea04b544d356ef4f6f8cecc72aba917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Mon, 3 Oct 2022 12:42:26 +0200 Subject: [PATCH] fix(core): async functions have undefined paths --- packages/core/src/__tests__/linter.test.ts | 61 ++++++++++++++++++++++ packages/core/src/runner/lintNode.ts | 45 ++++++---------- 2 files changed, 77 insertions(+), 29 deletions(-) diff --git a/packages/core/src/__tests__/linter.test.ts b/packages/core/src/__tests__/linter.test.ts index ba4dc148fd..d7868a664c 100644 --- a/packages/core/src/__tests__/linter.test.ts +++ b/packages/core/src/__tests__/linter.test.ts @@ -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, + }), + ]); + }); }); diff --git a/packages/core/src/runner/lintNode.ts b/packages/core/src/runner/lintNode.ts index 6059f5973a..5e232cff42 100644 --- a/packages/core/src/runner/lintNode.ts +++ b/packages/core/src/runner/lintNode.ts @@ -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}` : ''}`, @@ -43,25 +41,14 @@ 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); } } } @@ -69,10 +56,10 @@ export const lintNode = (context: IRunnerInternalContext, node: IGivenNode, rule 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);