diff --git a/packages/core/src/__tests__/linter.test.ts b/packages/core/src/__tests__/linter.test.ts index 101ac4d19..3d2316329 100644 --- a/packages/core/src/__tests__/linter.test.ts +++ b/packages/core/src/__tests__/linter.test.ts @@ -1,4 +1,4 @@ -import { falsy, pattern, truthy } from '@stoplight/spectral-functions'; +import { enumeration, falsy, pattern, truthy } from '@stoplight/spectral-functions'; import { DiagnosticSeverity } from '@stoplight/types'; import { parse } from '@stoplight/yaml'; import * as Parsers from '@stoplight/spectral-parsers'; @@ -30,6 +30,38 @@ describe('linter', () => { spectral = new Spectral(); }); + test('interpolates {{value}} with the property key for `field: "@key"` (#2922)', async () => { + spectral.setRuleset({ + rules: { + rule1: { + given: '$.responses', + then: { + field: '@key', + function: enumeration, + functionOptions: { values: ['200', '400'] }, + }, + }, + }, + }); + + const document = new Document( + `responses:\n "200":\n description: "ok"\n "404":\n description: "not found"`, + Parsers.Yaml, + ); + + const results = await spectral.run(document); + + expect(results).toEqual([ + expect.objectContaining({ + code: 'rule1', + // before the fix this interpolated to `Object{}` instead of the key + message: '"404" must be equal to one of the allowed values: "200", "400"', + severity: DiagnosticSeverity.Warning, + path: ['responses', '404'], + }), + ]); + }); + test('should demand some result', () => { return expect(spectral.run(new Document('123', Parsers.Json))).rejects.toThrow( 'No ruleset has been defined. Have you called setRuleset()?', diff --git a/packages/core/src/runner/lintNode.ts b/packages/core/src/runner/lintNode.ts index 6070c6811..088ecf2d2 100644 --- a/packages/core/src/runner/lintNode.ts +++ b/packages/core/src/runner/lintNode.ts @@ -46,11 +46,11 @@ export const lintNode = (context: IRunnerInternalContext, node: IGivenNode, rule const _fnContext = { ...fnContext }; context.promises.push( targetResults.then(results => - results === void 0 ? void 0 : processTargetResults(context, _fnContext, results), + results === void 0 ? void 0 : processTargetResults(context, _fnContext, results, then.field), ), ); } else { - processTargetResults(context, fnContext, targetResults); + processTargetResults(context, fnContext, targetResults, then.field); } } } @@ -60,6 +60,7 @@ function processTargetResults( context: IRunnerInternalContext, fnContext: RulesetFunctionContext & { rule: Rule }, results: IFunctionResult[], + field: string | undefined, ): void { const { rule, path: targetPath } = fnContext; for (const result of results) { @@ -70,7 +71,15 @@ function processTargetResults( const document = associatedItem?.document ?? context.documentInventory.document; const range = document.getRangeForJsonPath(path, true) ?? Document.DEFAULT_RANGE; - const value: unknown = path.length === 0 ? document.data : get(document.data, path); + // For `field: '@key'` the linted value is the property key itself, not the + // value stored at that path; reading the document at `path` would yield that + // value (e.g. an object), so `{{value}}` would interpolate to `Object{}`. + const value: unknown = + field === '@key' && result.path === void 0 + ? path[path.length - 1] + : path.length === 0 + ? document.data + : get(document.data, path); const vars: MessageVars = { property: