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.
This commit is contained in:
Sergey Krashevich
2025-12-27 07:59:22 +03:00
parent 247d4063ed
commit da0b19026c
+53 -1
View File
@@ -456,6 +456,23 @@
const lintYamlModel = (model, schemaTools) => { const lintYamlModel = (model, schemaTools) => {
const markers = []; const markers = [];
const markedLines = new Set(); 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) => { const markLineError = (lineNumber, message, startColumn = 1) => {
if (markedLines.has(`${lineNumber}:${message}`)) return; if (markedLines.has(`${lineNumber}:${message}`)) return;
@@ -500,6 +517,7 @@
expected: 'object', expected: 'object',
actual: null, actual: null,
keys: new Map(), keys: new Map(),
childIndent: null,
origin: null, origin: null,
reportedTypeMismatch: false, reportedTypeMismatch: false,
}]; }];
@@ -566,6 +584,13 @@
if (!line.trim()) continue; if (!line.trim()) continue;
const indent = countIndent(line); 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)) { if (indent === 0 && (hasTopLevelKey || hasTopLevelList)) {
const trimmed = line.trim(); const trimmed = line.trim();
if (trimmed !== '---' && trimmed !== '...' && !trimmed.startsWith('#')) { if (trimmed !== '---' && trimmed !== '...' && !trimmed.startsWith('#')) {
@@ -581,9 +606,13 @@
const listItem = parseListItem(line); const listItem = parseListItem(line);
if (listItem) { 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(); while (stack.length > 1 && listItem.indent <= stack[stack.length - 1].indent) stack.pop();
const parent = stack[stack.length - 1]; const parent = stack[stack.length - 1];
checkChildIndent(parent, listItem.indent, lineNumber);
setActualType(parent, 'array', lineNumber); setActualType(parent, 'array', lineNumber);
const itemSchema = schemaTools ? schemaTools.getArrayItemSchema(parent.schema) : null; const itemSchema = schemaTools ? schemaTools.getArrayItemSchema(parent.schema) : null;
@@ -595,12 +624,18 @@
expected: itemExpected, expected: itemExpected,
actual: null, actual: null,
keys: new Map(), keys: new Map(),
childIndent: null,
origin: {lineNumber, startColumn: listItem.dashIndex + 1, endColumn: listItem.dashIndex + 2}, origin: {lineNumber, startColumn: listItem.dashIndex + 1, endColumn: listItem.dashIndex + 2},
reportedTypeMismatch: false, reportedTypeMismatch: false,
}; };
stack.push(itemCtx); stack.push(itemCtx);
if (!listItem.rest) continue; 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); const inline = parseKey(' '.repeat(listItem.afterDashIndex) + listItem.rest);
if (inline) { if (inline) {
@@ -608,6 +643,7 @@
const kv = inline; const kv = inline;
while (stack.length > 1 && kv.indent <= stack[stack.length - 1].indent) stack.pop(); while (stack.length > 1 && kv.indent <= stack[stack.length - 1].indent) stack.pop();
const ctx = stack[stack.length - 1]; const ctx = stack[stack.length - 1];
checkChildIndent(ctx, kv.indent, lineNumber);
setActualType(ctx, 'object', lineNumber); setActualType(ctx, 'object', lineNumber);
const prev = ctx.keys.get(kv.key); const prev = ctx.keys.get(kv.key);
@@ -636,6 +672,9 @@
} }
const propSchema = schemaTools ? schemaTools.getPropertySchema(ctx.schema, kv.key) : null; const propSchema = schemaTools ? schemaTools.getPropertySchema(ctx.schema, kv.key) : null;
if (isBlockScalarHeader(kv.after)) {
blockScalarParentIndent = kv.indent;
}
if (kv.isContainer) { if (kv.isContainer) {
stack.push({ stack.push({
indent: kv.indent, indent: kv.indent,
@@ -643,6 +682,7 @@
expected: getExpectedContainerType(propSchema), expected: getExpectedContainerType(propSchema),
actual: null, actual: null,
keys: new Map(), keys: new Map(),
childIndent: null,
origin: {lineNumber, startColumn: kv.keyStartIndex + 1, endColumn: kv.keyEndIndex + 1}, origin: {lineNumber, startColumn: kv.keyStartIndex + 1, endColumn: kv.keyEndIndex + 1},
reportedTypeMismatch: false, reportedTypeMismatch: false,
}); });
@@ -661,10 +701,17 @@
} }
const kv = parseKey(line); 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(); while (stack.length > 1 && kv.indent <= stack[stack.length - 1].indent) stack.pop();
const ctx = stack[stack.length - 1]; const ctx = stack[stack.length - 1];
checkChildIndent(ctx, kv.indent, lineNumber);
setActualType(ctx, 'object', lineNumber); setActualType(ctx, 'object', lineNumber);
const prev = ctx.keys.get(kv.key); const prev = ctx.keys.get(kv.key);
@@ -700,12 +747,17 @@
expected: getExpectedContainerType(propSchema), expected: getExpectedContainerType(propSchema),
actual: null, actual: null,
keys: new Map(), keys: new Map(),
childIndent: null,
origin: {lineNumber, startColumn: kv.keyStartIndex + 1, endColumn: kv.keyEndIndex + 1}, origin: {lineNumber, startColumn: kv.keyStartIndex + 1, endColumn: kv.keyEndIndex + 1},
reportedTypeMismatch: false, reportedTypeMismatch: false,
}); });
continue; continue;
} }
if (isBlockScalarHeader(kv.after)) {
blockScalarParentIndent = kv.indent;
}
if (!propSchema) continue; if (!propSchema) continue;
const actual = classifyYamlScalar(kv.after).type; const actual = classifyYamlScalar(kv.after).type;