From da0b19026ca90519d7cf9ad2136e334fc37c191a Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sat, 27 Dec 2025 07:59:22 +0300 Subject: [PATCH] Improve YAML linter with indentation and block scalar checks Adds detection for inconsistent indentation and unexpected indentation at the document root. Enhances block scalar handling by tracking parent indentation and skipping content lines accordingly. Also improves error reporting for missing map keys or list items. --- www/config.html | 54 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/www/config.html b/www/config.html index 8e50f340..b4b20c04 100644 --- a/www/config.html +++ b/www/config.html @@ -456,6 +456,23 @@ const lintYamlModel = (model, schemaTools) => { const markers = []; const markedLines = new Set(); + let blockScalarParentIndent = null; + + const isBlockScalarHeader = (text) => { + const t = (text ?? '').trimStart(); + return t.startsWith('|') || t.startsWith('>'); + }; + + const checkChildIndent = (ctx, childIndent, lineNumber) => { + if (!ctx) return; + if (ctx.childIndent == null) { + ctx.childIndent = childIndent; + return; + } + if (childIndent !== ctx.childIndent) { + markLineError(lineNumber, `YAML: inconsistent indentation (expected ${ctx.childIndent} spaces)`, 1); + } + }; const markLineError = (lineNumber, message, startColumn = 1) => { if (markedLines.has(`${lineNumber}:${message}`)) return; @@ -500,6 +517,7 @@ expected: 'object', actual: null, keys: new Map(), + childIndent: null, origin: null, reportedTypeMismatch: false, }]; @@ -566,6 +584,13 @@ if (!line.trim()) continue; const indent = countIndent(line); + if (blockScalarParentIndent !== null && indent <= blockScalarParentIndent) { + blockScalarParentIndent = null; + } + if (blockScalarParentIndent !== null && indent > blockScalarParentIndent) { + continue; // treat as block scalar content + } + if (indent === 0 && (hasTopLevelKey || hasTopLevelList)) { const trimmed = line.trim(); if (trimmed !== '---' && trimmed !== '...' && !trimmed.startsWith('#')) { @@ -581,9 +606,13 @@ const listItem = parseListItem(line); if (listItem) { + if (listItem.indent > 0 && (hasTopLevelKey || hasTopLevelList) && stack.length === 1) { + markLineError(lineNumber, 'YAML: unexpected indentation at document root', 1); + } while (stack.length > 1 && listItem.indent <= stack[stack.length - 1].indent) stack.pop(); const parent = stack[stack.length - 1]; + checkChildIndent(parent, listItem.indent, lineNumber); setActualType(parent, 'array', lineNumber); const itemSchema = schemaTools ? schemaTools.getArrayItemSchema(parent.schema) : null; @@ -595,12 +624,18 @@ expected: itemExpected, actual: null, keys: new Map(), + childIndent: null, origin: {lineNumber, startColumn: listItem.dashIndex + 1, endColumn: listItem.dashIndex + 2}, reportedTypeMismatch: false, }; stack.push(itemCtx); if (!listItem.rest) continue; + if (isBlockScalarHeader(listItem.rest)) { + blockScalarParentIndent = listItem.indent; + checkValueType(itemSchema, 'string', lineNumber, listItem.afterDashIndex + 1, line.length + 1, null); + continue; + } const inline = parseKey(' '.repeat(listItem.afterDashIndex) + listItem.rest); if (inline) { @@ -608,6 +643,7 @@ const kv = inline; while (stack.length > 1 && kv.indent <= stack[stack.length - 1].indent) stack.pop(); const ctx = stack[stack.length - 1]; + checkChildIndent(ctx, kv.indent, lineNumber); setActualType(ctx, 'object', lineNumber); const prev = ctx.keys.get(kv.key); @@ -636,6 +672,9 @@ } const propSchema = schemaTools ? schemaTools.getPropertySchema(ctx.schema, kv.key) : null; + if (isBlockScalarHeader(kv.after)) { + blockScalarParentIndent = kv.indent; + } if (kv.isContainer) { stack.push({ indent: kv.indent, @@ -643,6 +682,7 @@ expected: getExpectedContainerType(propSchema), actual: null, keys: new Map(), + childIndent: null, origin: {lineNumber, startColumn: kv.keyStartIndex + 1, endColumn: kv.keyEndIndex + 1}, reportedTypeMismatch: false, }); @@ -661,10 +701,17 @@ } const kv = parseKey(line); - if (!kv) continue; + if (!kv) { + markLineError(lineNumber, 'YAML: expected a map key (key:) or list item (-)', indent + 1); + continue; + } + if (kv.indent > 0 && (hasTopLevelKey || hasTopLevelList) && stack.length === 1) { + markLineError(lineNumber, 'YAML: unexpected indentation at document root', 1); + } while (stack.length > 1 && kv.indent <= stack[stack.length - 1].indent) stack.pop(); const ctx = stack[stack.length - 1]; + checkChildIndent(ctx, kv.indent, lineNumber); setActualType(ctx, 'object', lineNumber); const prev = ctx.keys.get(kv.key); @@ -700,12 +747,17 @@ expected: getExpectedContainerType(propSchema), actual: null, keys: new Map(), + childIndent: null, origin: {lineNumber, startColumn: kv.keyStartIndex + 1, endColumn: kv.keyEndIndex + 1}, reportedTypeMismatch: false, }); continue; } + if (isBlockScalarHeader(kv.after)) { + blockScalarParentIndent = kv.indent; + } + if (!propSchema) continue; const actual = classifyYamlScalar(kv.after).type;