From 30308112199a5cc5274eef0c018149203e83ab8d Mon Sep 17 00:00:00 2001 From: Morgan Chang Date: Fri, 1 May 2026 12:27:35 -0400 Subject: [PATCH 1/2] fix to handle inline regex modifiers and invalid schema patterns for hover Signed-off-by: Morgan Chang --- l10n/bundle.l10n.de.json | 3 +- l10n/bundle.l10n.fr.json | 3 +- l10n/bundle.l10n.ja.json | 3 +- l10n/bundle.l10n.json | 3 +- l10n/bundle.l10n.ko.json | 3 +- l10n/bundle.l10n.zh-cn.json | 3 +- l10n/bundle.l10n.zh-tw.json | 3 +- .../parser/schemaValidation/baseValidator.ts | 11 ++- src/languageservice/utils/strings.ts | 20 ++++- test/hover.test.ts | 54 ++++++++++++ test/schemaValidation.test.ts | 87 +++++++++++++++++++ 11 files changed, 182 insertions(+), 11 deletions(-) diff --git a/l10n/bundle.l10n.de.json b/l10n/bundle.l10n.de.json index 5b16a38b0..749424962 100644 --- a/l10n/bundle.l10n.de.json +++ b/l10n/bundle.l10n.de.json @@ -58,5 +58,6 @@ "Unused anchor \"{0}\"": "Nicht verwendeter Anker \"{0}\"", "Unresolved alias \"{0}\"": "Nicht aufgelöstes Alias \"{0}\"", "Convert string to folded block string": "Konvertieren Sie die Zeichenfolge in eine gefaltete Blockzeichenfolge", - "Convert string to literal block string": "Konvertieren Sie die Zeichenfolge in eine wörtliche Blockzeichenfolge" + "Convert string to literal block string": "Konvertieren Sie die Zeichenfolge in eine wörtliche Blockzeichenfolge", + "Invalid pattern: \"{0}\"": "Ungültiges Pattern: \"{0}\"" } diff --git a/l10n/bundle.l10n.fr.json b/l10n/bundle.l10n.fr.json index 152167853..fd3dabfba 100644 --- a/l10n/bundle.l10n.fr.json +++ b/l10n/bundle.l10n.fr.json @@ -58,5 +58,6 @@ "Unused anchor \"{0}\"": "Ancre inutilisée '{0}'", "Unresolved alias \"{0}\"": "Alias non résolu '{0}'", "Convert string to folded block string": "Convertir la chaîne en style de bloc pliée", - "Convert string to literal block string": "Convertir la chaîne en style de bloc littérale" + "Convert string to literal block string": "Convertir la chaîne en style de bloc littérale", + "Invalid pattern: \"{0}\"": "Motif non valide : \"{0}\"" } diff --git a/l10n/bundle.l10n.ja.json b/l10n/bundle.l10n.ja.json index f6d5d892c..35b4bfc44 100644 --- a/l10n/bundle.l10n.ja.json +++ b/l10n/bundle.l10n.ja.json @@ -58,5 +58,6 @@ "Unused anchor \"{0}\"": "未使用のアンカー \"{0}\"", "Unresolved alias \"{0}\"": "未解決のエイリアス \"{0}\"", "Convert string to folded block string": "文字列を折り畳みブロック文字列に変換する", - "Convert string to literal block string": "文字列をリテラルブロック文字列に変換する" + "Convert string to literal block string": "文字列をリテラルブロック文字列に変換する", + "Invalid pattern: \"{0}\"": "無効なパターンです:\"{0}\"" } diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index f179ed863..b8cdcb785 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -57,5 +57,6 @@ "Unused anchor \"{0}\"": "Unused anchor \"{0}\"", "Unresolved alias \"{0}\"": "Unresolved alias \"{0}\"", "Convert string to folded block string": "Convert string to folded block string", - "Convert string to literal block string": "Convert string to literal block string" + "Convert string to literal block string": "Convert string to literal block string", + "Invalid pattern: \"{0}\"": "Invalid pattern: \"{0}\"" } diff --git a/l10n/bundle.l10n.ko.json b/l10n/bundle.l10n.ko.json index 63452bf64..d8c4f03d0 100644 --- a/l10n/bundle.l10n.ko.json +++ b/l10n/bundle.l10n.ko.json @@ -58,5 +58,6 @@ "Unused anchor \"{0}\"": "사용되지 않은 앵커 \"{0}\"", "Unresolved alias \"{0}\"": "해결되지 않은 별칭 \"{0}\"", "Convert string to folded block string": "문자열을 접힌 블록 문자열로 변환합니다.", - "Convert string to literal block string": "문자열을 리터럴 블록 문자열로 변환합니다." + "Convert string to literal block string": "문자열을 리터럴 블록 문자열로 변환합니다.", + "Invalid pattern: \"{0}\"": "유효하지 않은 패턴입니다: \"{0}\"" } diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index f12e380c4..40bf2f3bb 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -58,5 +58,6 @@ "Unused anchor \"{0}\"": "未使用的锚点 \"{0}\"", "Unresolved alias \"{0}\"": "未解析的别名 \"{0}\"", "Convert string to folded block string": "将字符串转换为折叠块字符串", - "Convert string to literal block string": "将字符串转换为文字块字符串" + "Convert string to literal block string": "将字符串转换为文字块字符串", + "Invalid pattern: \"{0}\"": "无效的模式:\"{0}\"" } diff --git a/l10n/bundle.l10n.zh-tw.json b/l10n/bundle.l10n.zh-tw.json index cc1a981b5..36bc0a802 100644 --- a/l10n/bundle.l10n.zh-tw.json +++ b/l10n/bundle.l10n.zh-tw.json @@ -58,5 +58,6 @@ "Unused anchor \"{0}\"": "未使用的錨點 \"{0}\"", "Unresolved alias \"{0}\"": "未解析的別名 \"{0}\"", "Convert string to folded block string": "將字串轉換為折疊塊字串", - "Convert string to literal block string": "將字串轉換為文字區塊字串" + "Convert string to literal block string": "將字串轉換為文字區塊字串", + "Invalid pattern: \"{0}\"": "無效的模式:\"{0}\"" } diff --git a/src/languageservice/parser/schemaValidation/baseValidator.ts b/src/languageservice/parser/schemaValidation/baseValidator.ts index 5549a4618..0c913f631 100644 --- a/src/languageservice/parser/schemaValidation/baseValidator.ts +++ b/src/languageservice/parser/schemaValidation/baseValidator.ts @@ -824,7 +824,15 @@ export abstract class BaseValidator { if (isString(schema.pattern)) { const regex = safeCreateUnicodeRegExp(schema.pattern); - if (!regex.test(value)) { + if (!regex) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: l10n.t('Invalid pattern: "{0}"', schema.pattern), + source: this.getSchemaSource(schema, originalSchema), + schemaUri: this.getSchemaUri(schema, originalSchema), + }); + } else if (!regex.test(value)) { validationResult.problems.push({ location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, @@ -1299,6 +1307,7 @@ export abstract class BaseValidator { for (const propertyPattern of Object.keys(patternProps)) { const regex = safeCreateUnicodeRegExp(propertyPattern); + if (!regex) continue; for (const propertyName of unprocessedProperties.slice(0)) { if (!regex.test(propertyName)) continue; diff --git a/src/languageservice/utils/strings.ts b/src/languageservice/utils/strings.ts index 6171411df..609d33ba6 100644 --- a/src/languageservice/utils/strings.ts +++ b/src/languageservice/utils/strings.ts @@ -66,12 +66,26 @@ export function getIndentation(lineContent: string, position: number): number { return position; } -export function safeCreateUnicodeRegExp(pattern: string): RegExp { +export function safeCreateUnicodeRegExp(pattern: string): RegExp | undefined { + let flags = ''; + pattern = pattern.replace(/\(\?([ims]+)\)/g, (_match, modifiers: string) => { + for (const modifier of modifiers) { + if (!flags.includes(modifier)) { + flags += modifier; + } + } + return ''; + }); + // fall back to regular regexp if we cannot create Unicode one try { - return new RegExp(pattern, 'u'); + return new RegExp(pattern, flags + 'u'); } catch (ignore) { - return new RegExp(pattern); + try { + return new RegExp(pattern, flags); + } catch (e) { + return undefined; + } } } diff --git a/test/hover.test.ts b/test/hover.test.ts index bc4e013a8..79fea557f 100644 --- a/test/hover.test.ts +++ b/test/hover.test.ts @@ -515,6 +515,60 @@ users: ); }); + it('Hover handles inline pattern modifiers', async () => { + schemaProvider.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + description: { + type: 'string', + description: 'Field with dotall pattern.', + pattern: '(?s).*', + }, + multilineAndDotall: { + type: 'string', + description: 'Field with multiline and dotall pattern.', + pattern: '(?ms)^start.*end$', + }, + }, + }); + + let result = await parseSetup('description: a|n|y text'); + assert.strictEqual(MarkupContent.is(result.contents), true); + assert.strictEqual( + (result.contents as MarkupContent).value, + `Field with dotall pattern.\n\nSource: [${SCHEMA_ID}](file:///${SCHEMA_ID})` + ); + + result = await parseSetup('multilineAndDotall: start middle c|o|ntent end'); + assert.strictEqual(MarkupContent.is(result.contents), true); + assert.strictEqual( + (result.contents as MarkupContent).value, + `Field with multiline and dotall pattern.\n\nSource: [${SCHEMA_ID}](file:///${SCHEMA_ID})` + ); + expect(telemetry.messages).to.be.empty; + }); + + it('Hover handles unsupported pattern', async () => { + schemaProvider.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + unsupportedPattern: { + type: 'string', + description: 'Field with unsupported pattern.', + pattern: '(?x)text .*', + }, + }, + }); + + const result = await parseSetup('unsupportedPattern: text c|o|ntent'); + assert.strictEqual(MarkupContent.is(result.contents), true); + assert.strictEqual( + (result.contents as MarkupContent).value, + `Field with unsupported pattern.\n\nSource: [${SCHEMA_ID}](file:///${SCHEMA_ID})` + ); + expect(telemetry.messages).to.be.empty; + }); + it('hover on value and its description has multiline, indentation and special string', async () => { (() => { languageSettingsSetup = new ServiceSetup() diff --git a/test/schemaValidation.test.ts b/test/schemaValidation.test.ts index c27256c4f..a036c9aa5 100644 --- a/test/schemaValidation.test.ts +++ b/test/schemaValidation.test.ts @@ -506,6 +506,93 @@ describe('Validation Tests', () => { }) .then(done, done); }); + it('Test inline pattern modifiers', (done) => { + schemaProvider.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + multilineOnly: { + type: 'string', + pattern: '(?m)^start.*end$', + }, + dotallOnly: { + type: 'string', + pattern: '(?s)^start.*end$', + }, + multilineAndDotall: { + type: 'string', + pattern: '(?ms)^start.*end$', + }, + reversedFlags: { + type: 'string', + pattern: '(?sm)^start.*end$', + }, + allFlags: { + type: 'string', + pattern: '(?ims)^START.*END$', + }, + }, + }); + const content = [ + 'multilineOnly: |-', + ' other text', + ' start middle end', + ' more text', + 'dotallOnly: |-', + ' start', + ' middle content', + ' end', + 'multilineAndDotall: |-', + ' other text', + ' start', + ' middle content', + ' end', + ' more text', + 'reversedFlags: |-', + ' start', + ' middle content', + ' end', + 'allFlags: |-', + ' other text', + ' start', + ' middle content', + ' end', + ' more text', + ].join('\n'); + parseSetup(content) + .then(function (result) { + assert.equal(result.length, 0); + }) + .then(done, done); + }); + it('Test an unsupported pattern', (done) => { + schemaProvider.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + prop: { + type: 'string', + pattern: '(?x)text .*', + }, + }, + }); + parseSetup('prop: text content') + .then(function (result) { + assert.equal(result.length, 1); + assert.deepEqual( + result[0], + createDiagnosticWithData( + 'Invalid pattern: "(?x)text .*"', + 0, + 6, + 0, + 18, + DiagnosticSeverity.Error, + `yaml-schema: file:///${SCHEMA_ID}`, + `file:///${SCHEMA_ID}` + ) + ); + }) + .then(done, done); + }); }); describe('Number tests', () => { From 06ebc6d47749564226f664796fd4259a4d737f72 Mon Sep 17 00:00:00 2001 From: Morgan Chang Date: Fri, 1 May 2026 12:56:49 -0400 Subject: [PATCH 2/2] added more tests Signed-off-by: Morgan Chang --- test/schemaValidation.test.ts | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/test/schemaValidation.test.ts b/test/schemaValidation.test.ts index a036c9aa5..55ef0113f 100644 --- a/test/schemaValidation.test.ts +++ b/test/schemaValidation.test.ts @@ -510,6 +510,10 @@ describe('Validation Tests', () => { schemaProvider.addSchema(SCHEMA_ID, { type: 'object', properties: { + caseInsensitiveField: { + type: 'string', + pattern: '(?i)^(yes|no)$', + }, multilineOnly: { type: 'string', pattern: '(?m)^start.*end$', @@ -518,13 +522,13 @@ describe('Validation Tests', () => { type: 'string', pattern: '(?s)^start.*end$', }, - multilineAndDotall: { + multilineAndDotallCombinedFlags: { type: 'string', pattern: '(?ms)^start.*end$', }, - reversedFlags: { + dotallAndCaseInsensitiveFieldFlags: { type: 'string', - pattern: '(?sm)^start.*end$', + pattern: '(?s)(?i)test.*content', }, allFlags: { type: 'string', @@ -533,24 +537,30 @@ describe('Validation Tests', () => { }, }); const content = [ + 'caseInsensitiveField: YES', + '', 'multilineOnly: |-', ' other text', - ' start middle end', + ' start middle content end', ' more text', + '', 'dotallOnly: |-', ' start', ' middle content', ' end', - 'multilineAndDotall: |-', + '', + 'multilineAndDotallCombinedFlags: |-', ' other text', ' start', ' middle content', ' end', ' more text', - 'reversedFlags: |-', - ' start', + '', + 'dotallAndCaseInsensitiveFieldFlags: |-', + ' TEST', ' middle content', - ' end', + ' CONTENT', + '', 'allFlags: |-', ' other text', ' start', @@ -568,13 +578,13 @@ describe('Validation Tests', () => { schemaProvider.addSchema(SCHEMA_ID, { type: 'object', properties: { - prop: { + unsupportedPattern: { type: 'string', pattern: '(?x)text .*', }, }, }); - parseSetup('prop: text content') + parseSetup('unsupportedPattern: text content') .then(function (result) { assert.equal(result.length, 1); assert.deepEqual( @@ -582,9 +592,9 @@ describe('Validation Tests', () => { createDiagnosticWithData( 'Invalid pattern: "(?x)text .*"', 0, - 6, + 20, 0, - 18, + 32, DiagnosticSeverity.Error, `yaml-schema: file:///${SCHEMA_ID}`, `file:///${SCHEMA_ID}`