From 4e81d854f60fc6d4e6ffeec70f73c222d926d75d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:48:21 +0000 Subject: [PATCH 1/2] chore: bump js-yaml from 4.3.2 to 5.5.0 Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.3.2 to 5.5.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.3.2...5.5.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 5.5.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index bc837cf85..3694f365e 100644 --- a/package.json +++ b/package.json @@ -2645,7 +2645,7 @@ "cheerio": "1.2.0", "ejs": "^7.0.2", "highlight.js": "^11.12.0", - "js-yaml": "^4.3.2", + "js-yaml": "^5.5.0", "vscode-languageclient": "^10.1.2", "winreg": "^1.2.5" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b6b74d40..29a800392 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: specifier: ^11.12.0 version: 11.12.0 js-yaml: - specifier: ^4.3.2 - version: 4.3.2 + specifier: ^5.5.0 + version: 5.5.0 vscode-languageclient: specifier: ^10.1.2 version: 10.1.2 From b89f42dc8315446e1b1a29bd0d3cb6a37731534c Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Sat, 7 Nov 2026 01:10:10 +0800 Subject: [PATCH 2/2] fix: preserve R Markdown YAML behavior with js-yaml 5 --- package.json | 1 - pnpm-lock.yaml | 8 ---- src/rTerminal.ts | 19 ++++---- src/rmarkdown/knit.ts | 4 +- src/rmarkdown/yaml.ts | 31 ++++++++++++ src/test/suite/rmarkdown.test.ts | 42 +++++++++++++++++ src/test/suite/rmdParams.test.ts | 81 ++++++++++++++++++++++++++++++++ 7 files changed, 164 insertions(+), 22 deletions(-) create mode 100644 src/rmarkdown/yaml.ts diff --git a/package.json b/package.json index 3694f365e..5dca935ff 100644 --- a/package.json +++ b/package.json @@ -2619,7 +2619,6 @@ }, "devDependencies": { "@types/ejs": "^3.1.5", - "@types/js-yaml": "^4.0.9", "@types/mocha": "^8.2.3", "@types/node": "^22.20.5", "@types/sinon": "^10.0.20", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29a800392..18d739100 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,9 +39,6 @@ importers: '@types/ejs': specifier: ^3.1.5 version: 3.1.5 - '@types/js-yaml': - specifier: ^4.0.9 - version: 4.0.9 '@types/mocha': specifier: ^8.2.3 version: 8.2.3 @@ -1026,9 +1023,6 @@ packages: '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} - '@types/js-yaml@4.0.9': - resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} - '@types/mocha@10.0.10': resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} @@ -3546,8 +3540,6 @@ snapshots: '@types/istanbul-lib-coverage@2.0.6': {} - '@types/js-yaml@4.0.9': {} - '@types/mocha@10.0.10': {} '@types/mocha@8.2.3': {} diff --git a/src/rTerminal.ts b/src/rTerminal.ts index 3208ea2e7..d5484c86e 100644 --- a/src/rTerminal.ts +++ b/src/rTerminal.ts @@ -30,7 +30,7 @@ import { config, delay, getRterm, getCurrentWorkspaceFolder, getRPathConfigEntry import { resolveBackend, jgdEnabled, CommonPlotManager } from './plotViewer'; import { tryInteractiveExecution } from './interactive/executionTarget'; import * as fs from 'fs'; -import * as yaml from 'js-yaml'; +import { loadRMarkdownYaml } from './rmarkdown/yaml'; export let rTerm: vscode.Terminal | undefined = undefined; let rTermResource: vscode.Uri | undefined; @@ -96,12 +96,6 @@ const terminalSends = new WeakMap>(); let lastParamsRmdPath: string | undefined; let lastParamsRmdVersion: number | undefined; -const rExprType = new yaml.Type('!r', { - kind: 'scalar', - construct: (data: string) => ({ __rExpr: data }), -}); -const RMARKDOWN_SCHEMA = yaml.DEFAULT_SCHEMA.extend([rExprType]); - function valueToR(val: unknown): string { if (val === null || val === undefined) { return 'NULL'; @@ -118,6 +112,12 @@ function valueToR(val: unknown): string { if (typeof val === 'object' && val !== null && '__rExpr' in (val as Record)) { return (val as { __rExpr: string }).__rExpr; } + if (val instanceof Set) { + // js-yaml 5 represents !!set with Set instead of an object of nulls. + return valueToR( + Object.fromEntries(Array.from(val as Set, (key) => [String(key), null])), + ); + } if (Array.isArray(val)) { return `c(${val.map(valueToR).join(', ')})`; } @@ -145,10 +145,7 @@ export function getRmdParamsCommand(document: vscode.TextDocument): string | und lastParamsRmdPath = filePath; lastParamsRmdVersion = document.version; try { - const frontmatter = yaml.load(match[1], { schema: RMARKDOWN_SCHEMA }) as Record< - string, - unknown - >; + const frontmatter = loadRMarkdownYaml(match[1]) as Record; const params = frontmatter?.['params'] as Record | undefined; if (!params || typeof params !== 'object') { return undefined; diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts index 97d3caf53..1351888c6 100644 --- a/src/rmarkdown/knit.ts +++ b/src/rmarkdown/knit.ts @@ -2,7 +2,7 @@ import * as util from '../util'; import * as vscode from 'vscode'; import * as fs from 'node:fs'; import * as path from 'path'; -import * as yaml from 'js-yaml'; +import { loadRMarkdownYaml } from './yaml'; import { RMarkdownManager, KnitWorkingDirectory } from './manager'; import { runTextInTerm } from '../rTerminal'; @@ -125,7 +125,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { let paramObj: IYamlFrontmatter = {}; if (yamlText) { try { - const parsed: unknown = yaml.load(yamlText); + const parsed: unknown = loadRMarkdownYaml(yamlText); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { paramObj = parsed as IYamlFrontmatter; } diff --git a/src/rmarkdown/yaml.ts b/src/rmarkdown/yaml.ts new file mode 100644 index 000000000..0c65b96ef --- /dev/null +++ b/src/rmarkdown/yaml.ts @@ -0,0 +1,31 @@ +import * as yaml from 'js-yaml'; + +const rExpressionTag = yaml.defineScalarTag('!r', { + resolve: (source: string) => ({ __rExpr: source }), + identify: () => false, +}); + +const rMarkdownIntegerTag = { + ...yaml.intCoreTag, + // v4 also accepted binary integers and signed octal/hexadecimal integers + // without an explicit !!int tag. v5 restricts those to explicit tags. + resolve: (source: string, _isExplicit: boolean, tagName: string) => + yaml.intCoreTag.resolve(source, true, tagName), +}; + +// Keep the extra tags supported by js-yaml 4's DEFAULT_SCHEMA (including +// timestamps and merge keys), with YAML 1.2 boolean and number resolution. +const rMarkdownSchema = yaml.YAML11_SCHEMA.withTags( + yaml.CORE_SCHEMA.tags, + rMarkdownIntegerTag, + rExpressionTag, +); + +export function loadRMarkdownYaml(source: string): unknown { + // loadAll retains v4's undefined result for empty/comment-only headers. + const documents = yaml.loadAll(source, { schema: rMarkdownSchema }); + if (documents.length > 1) { + throw new yaml.YAMLException('expected a single YAML frontmatter document'); + } + return documents[0]; +} diff --git a/src/test/suite/rmarkdown.test.ts b/src/test/suite/rmarkdown.test.ts index 5eb50ff40..8ea039395 100644 --- a/src/test/suite/rmarkdown.test.ts +++ b/src/test/suite/rmarkdown.test.ts @@ -151,6 +151,46 @@ suite('R Markdown execution', () => { ); }); + test('knit frontmatter preserves merged commands alongside !r parameters', async () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-r-knit-yaml-')); + try { + const file = path.join(directory, 'test.Rmd'); + const knit = 'function(input, ...) rmarkdown::render(input, ...)'; + fs.writeFileSync( + file, + `--- +defaults: &defaults + knit: ${knit} + site: rmarkdown::render_site +<<: *defaults +date: !!timestamp 2026-11-07 +params: + count: !r 1+1 +---\n`, + ); + const manager = new RMarkdownKnitManager() as unknown as { + getYamlFrontmatter(file: string): Record; + getKnitCommand( + params: Record, + file: string, + format?: string, + ): Promise; + }; + const frontmatter = manager.getYamlFrontmatter(file); + assert.strictEqual(frontmatter['knit'], knit); + assert.strictEqual(frontmatter['site'], 'rmarkdown::render_site'); + assert.deepStrictEqual(frontmatter['params'], { count: { __rExpr: '1+1' } }); + assert.deepStrictEqual(frontmatter['date'], new Date('2026-11-07T00:00:00Z')); + const literal = util.ToRStringLiteral(file, '"'); + assert.strictEqual( + await manager.getKnitCommand(frontmatter, literal, 'html_document'), + `(${knit})(${literal}, output_format = "html_document")`, + ); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } + }); + test('non-object YAML frontmatter cannot leave knitting permanently busy', async () => { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-r-knit-')); try { @@ -178,7 +218,9 @@ suite('R Markdown execution', () => { ) => Promise; getYamlFrontmatter(file: string): unknown; }; + const parseError = sandbox.stub(console, 'error'); assert.deepStrictEqual(manager.getYamlFrontmatter(file), {}); + sinon.assert.notCalled(parseError); const render = sandbox .stub(manager, 'renderDocument') .rejects(new Error('transient failure')); diff --git a/src/test/suite/rmdParams.test.ts b/src/test/suite/rmdParams.test.ts index 7dfe703a4..ad17e9cd6 100644 --- a/src/test/suite/rmdParams.test.ts +++ b/src/test/suite/rmdParams.test.ts @@ -63,6 +63,87 @@ suite('Rmd Params Test Suite', () => { assert.strictEqual(cmd, 'params <- list(a = 1+1)'); }); + test('getRmdParamsCommand preserves merged parameter defaults and nested !r values', () => { + const mockDoc = { + languageId: 'rmd', + getText: () => `--- +defaults: &defaults + count: 2 + label: default +params: + <<: *defaults + label: overridden + expression: + value: !r | + sum(c(1, 2)) + values: [true, null, !r 1+1] +---`, + uri: vscode.Uri.file('/test_merged_params.Rmd'), + version: 1, + } as vscode.TextDocument; + + assert.strictEqual( + rTerminal.getRmdParamsCommand(mockDoc), + 'params <- list(count = 2, label = "overridden", expression = sum(c(1, 2))\n, values = c(TRUE, NULL, 1+1))', + ); + }); + + test('getRmdParamsCommand keeps YAML 1.2 scalars with legacy metadata tags', () => { + const mockDoc = { + languageId: 'rmd', + getText: () => `--- +date: !!timestamp 2026-11-07 +params: + enabled: true + label: yes + mode: on + count: 012 + binary: 0b101 + hex: -0xF + nothing: null +---`, + uri: vscode.Uri.file('/test_scalar_params.Rmd'), + version: 1, + } as vscode.TextDocument; + + assert.strictEqual( + rTerminal.getRmdParamsCommand(mockDoc), + 'params <- list(enabled = TRUE, label = "yes", mode = "on", count = 12, binary = 5, hex = -15, nothing = NULL)', + ); + }); + + test('getRmdParamsCommand preserves set parameters as named null values', () => { + const mockDoc = { + languageId: 'rmd', + getText: () => '---\nparams:\n choices: !!set {one: null, two: null}\n---', + uri: vscode.Uri.file('/test_set_params.Rmd'), + version: 1, + } as vscode.TextDocument; + + assert.strictEqual( + rTerminal.getRmdParamsCommand(mockDoc), + 'params <- list(choices = list(one = NULL, two = NULL))', + ); + }); + + test('getRmdParamsCommand rejects malformed YAML and non-scalar !r tags', () => { + for (const [index, value] of [ + '[1,', + '!unknown value', + '!r [1, 2]', + '!r {a: 1}', + ].entries()) { + const mockDoc = { + languageId: 'rmd', + getText: () => `---\nparams:\n a: ${value}\n---`, + uri: vscode.Uri.file(`/test_invalid_params_${index}.Rmd`), + version: 1, + } as vscode.TextDocument; + + assert.strictEqual(rTerminal.getRmdParamsCommand(mockDoc), undefined); + } + }); + test('getRmdParamsCommand respects cache invalidation by version', () => { const mockDoc = { languageId: 'rmd',