|
461 | 461 | return {type: 'string'}; |
462 | 462 | }; |
463 | 463 |
|
| 464 | + const parseYamlValue = (raw) => { |
| 465 | + const trimmed = raw.trim(); |
| 466 | + if (!trimmed) return {ok: false}; |
| 467 | + if (/^\$\{[^}{]+\}$/.test(trimmed)) return {ok: false, dynamic: true}; |
| 468 | + if (trimmed.startsWith('|') || trimmed.startsWith('>')) return {ok: false, block: true}; |
| 469 | + if (window.jsyaml && window.jsyaml.load) { |
| 470 | + try { |
| 471 | + return {ok: true, value: window.jsyaml.load(trimmed)}; |
| 472 | + } catch (e) { |
| 473 | + // nothing |
| 474 | + } |
| 475 | + } |
| 476 | + if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) { |
| 477 | + const inner = trimmed.slice(1, -1); |
| 478 | + return {ok: true, value: inner.replace(/\\"/g, '"').replace(/\\\\/g, '\\')}; |
| 479 | + } |
| 480 | + if (trimmed.startsWith('\'') && trimmed.endsWith('\'') && trimmed.length >= 2) { |
| 481 | + const inner = trimmed.slice(1, -1); |
| 482 | + return {ok: true, value: inner.replace(/''/g, '\'')}; |
| 483 | + } |
| 484 | + if (trimmed === 'true' || trimmed === 'false') return {ok: true, value: trimmed === 'true'}; |
| 485 | + if (trimmed === 'null' || trimmed === '~') return {ok: true, value: null}; |
| 486 | + if (isIntLike(trimmed)) return {ok: true, value: parseInt(trimmed, 10)}; |
| 487 | + if (isNumberLike(trimmed)) return {ok: true, value: Number(trimmed)}; |
| 488 | + return {ok: true, value: trimmed}; |
| 489 | + }; |
| 490 | + |
464 | 491 | const lintYamlModel = (model, schemaTools) => { |
465 | 492 | const markers = []; |
466 | 493 | const markedLines = new Set(); |
|
587 | 614 | }); |
588 | 615 | }; |
589 | 616 |
|
| 617 | + const valueEquals = (a, b) => { |
| 618 | + if (a === b) return true; |
| 619 | + if (typeof a !== typeof b) return false; |
| 620 | + if (a && b && typeof a === 'object') { |
| 621 | + const aIsArray = Array.isArray(a); |
| 622 | + const bIsArray = Array.isArray(b); |
| 623 | + if (aIsArray !== bIsArray) return false; |
| 624 | + if (aIsArray) { |
| 625 | + if (a.length !== b.length) return false; |
| 626 | + for (let i = 0; i < a.length; i++) { |
| 627 | + if (!valueEquals(a[i], b[i])) return false; |
| 628 | + } |
| 629 | + return true; |
| 630 | + } |
| 631 | + const aKeys = Object.keys(a); |
| 632 | + const bKeys = Object.keys(b); |
| 633 | + if (aKeys.length !== bKeys.length) return false; |
| 634 | + for (const key of aKeys) { |
| 635 | + if (!Object.prototype.hasOwnProperty.call(b, key)) return false; |
| 636 | + if (!valueEquals(a[key], b[key])) return false; |
| 637 | + } |
| 638 | + return true; |
| 639 | + } |
| 640 | + return false; |
| 641 | + }; |
| 642 | + |
| 643 | + const schemaAllowsTypeLoose = (schema, actualType) => { |
| 644 | + if (!schemaTools || !schema) return true; |
| 645 | + const types = schemaTools.getSchemaTypes(schema); |
| 646 | + if (types.size === 0) return true; |
| 647 | + if (actualType === 'integer' && types.has('number')) return true; |
| 648 | + return types.has(actualType); |
| 649 | + }; |
| 650 | + |
| 651 | + const collectConstraintSchemas = (schema, actualType) => { |
| 652 | + if (!schemaTools || !schema) return []; |
| 653 | + schema = schemaTools.resolveRef(schema); |
| 654 | + if (!schema) return []; |
| 655 | + if (Array.isArray(schema.anyOf)) { |
| 656 | + const res = []; |
| 657 | + for (const alt of schema.anyOf) res.push(...collectConstraintSchemas(alt, actualType)); |
| 658 | + return res; |
| 659 | + } |
| 660 | + if (Array.isArray(schema.oneOf)) { |
| 661 | + const res = []; |
| 662 | + for (const alt of schema.oneOf) res.push(...collectConstraintSchemas(alt, actualType)); |
| 663 | + return res; |
| 664 | + } |
| 665 | + if (!schemaAllowsTypeLoose(schema, actualType)) return []; |
| 666 | + return [schema]; |
| 667 | + }; |
| 668 | + |
| 669 | + const getSchemaEnumValues = (schema) => { |
| 670 | + const values = []; |
| 671 | + if (Array.isArray(schema.enum)) values.push(...schema.enum); |
| 672 | + if (Object.prototype.hasOwnProperty.call(schema, 'const')) values.push(schema.const); |
| 673 | + return values; |
| 674 | + }; |
| 675 | + |
| 676 | + const checkValueConstraints = (schema, actualType, rawValue, lineNumber, startColumn, endColumn, keyName) => { |
| 677 | + if (!schemaTools || !schema) return; |
| 678 | + if (actualType === 'dynamic') return; |
| 679 | + const parsed = parseYamlValue(rawValue); |
| 680 | + if (!parsed.ok) return; |
| 681 | + |
| 682 | + const candidates = collectConstraintSchemas(schema, actualType); |
| 683 | + if (candidates.length === 0) return; |
| 684 | + |
| 685 | + const hasConstraints = candidates.some((s) => ( |
| 686 | + (Array.isArray(s.enum) || Object.prototype.hasOwnProperty.call(s, 'const')) || |
| 687 | + (actualType === 'string' && typeof s.pattern === 'string') |
| 688 | + )); |
| 689 | + if (!hasConstraints) return; |
| 690 | + |
| 691 | + const hasUnconstrained = candidates.some((s) => ( |
| 692 | + !(Array.isArray(s.enum) || Object.prototype.hasOwnProperty.call(s, 'const')) && |
| 693 | + !(actualType === 'string' && typeof s.pattern === 'string') |
| 694 | + )); |
| 695 | + if (hasUnconstrained) return; |
| 696 | + |
| 697 | + const value = parsed.value; |
| 698 | + const matchesAny = candidates.some((s) => { |
| 699 | + const enums = getSchemaEnumValues(s); |
| 700 | + if (enums.length > 0 && !enums.some((v) => valueEquals(v, value))) return false; |
| 701 | + if (actualType === 'string' && typeof s.pattern === 'string') { |
| 702 | + try { |
| 703 | + const re = new RegExp(s.pattern); |
| 704 | + if (!re.test(String(value))) return false; |
| 705 | + } catch (e) { |
| 706 | + return true; |
| 707 | + } |
| 708 | + } |
| 709 | + return true; |
| 710 | + }); |
| 711 | + if (matchesAny) return; |
| 712 | + |
| 713 | + const enumValues = []; |
| 714 | + const patterns = []; |
| 715 | + for (const s of candidates) { |
| 716 | + enumValues.push(...getSchemaEnumValues(s)); |
| 717 | + if (actualType === 'string' && typeof s.pattern === 'string') patterns.push(s.pattern); |
| 718 | + } |
| 719 | + const enumLabel = unique(enumValues).map((v) => toYamlScalar(v)).join(', '); |
| 720 | + const patternLabel = unique(patterns).join(' | '); |
| 721 | + |
| 722 | + let message; |
| 723 | + const label = keyName ? `"${keyName}"` : 'value'; |
| 724 | + if (enumValues.length && patterns.length) { |
| 725 | + message = `Value for ${label} must be one of: ${enumLabel}; or match pattern: ${patternLabel}`; |
| 726 | + } else if (enumValues.length) { |
| 727 | + message = `Value for ${label} must be one of: ${enumLabel}`; |
| 728 | + } else if (patterns.length) { |
| 729 | + message = `Value for ${label} must match pattern: ${patternLabel}`; |
| 730 | + } else { |
| 731 | + return; |
| 732 | + } |
| 733 | + |
| 734 | + pushMarker({ |
| 735 | + severity: monaco.MarkerSeverity.Error, |
| 736 | + message, |
| 737 | + startLineNumber: lineNumber, |
| 738 | + startColumn, |
| 739 | + endLineNumber: lineNumber, |
| 740 | + endColumn: Math.max(startColumn + 1, endColumn), |
| 741 | + }); |
| 742 | + }; |
| 743 | + |
590 | 744 | for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) { |
591 | 745 | let line = model.getLineContent(lineNumber); |
592 | 746 | if (!line.trim()) continue; |
|
702 | 856 | const actual = classifyYamlScalar(valueText).type; |
703 | 857 | const valueStartColumn = kv.valueStartIndex + 1 + (kv.after.length - kv.after.trimStart().length); |
704 | 858 | checkValueType(propSchema, actual, lineNumber, valueStartColumn, line.length + 1, kv.key); |
| 859 | + checkValueConstraints(propSchema, actual, valueText, lineNumber, valueStartColumn, line.length + 1, kv.key); |
705 | 860 | } |
706 | 861 | continue; |
707 | 862 | } |
708 | 863 |
|
709 | 864 | const scalar = classifyYamlScalar(listItem.rest).type; |
710 | 865 | checkValueType(itemSchema, scalar, lineNumber, listItem.afterDashIndex + 1, line.length + 1, null); |
| 866 | + checkValueConstraints(itemSchema, scalar, listItem.rest, lineNumber, listItem.afterDashIndex + 1, line.length + 1, null); |
711 | 867 | continue; |
712 | 868 | } |
713 | 869 |
|
|
774 | 930 | const actual = classifyYamlScalar(kv.after).type; |
775 | 931 | const valueStartColumn = kv.valueStartIndex + 1 + (kv.after.length - kv.after.trimStart().length); |
776 | 932 | checkValueType(propSchema, actual, lineNumber, valueStartColumn, line.length + 1, kv.key); |
| 933 | + checkValueConstraints(propSchema, actual, kv.after, lineNumber, valueStartColumn, line.length + 1, kv.key); |
777 | 934 | } |
778 | 935 |
|
779 | 936 | return markers; |
|
910 | 1067 | const props = getObjectProperties(contextSchema); |
911 | 1068 | const suggestions = Object.keys(props).map((key) => { |
912 | 1069 | const s = resolveRef(props[key]); |
913 | | - const wantsBlock = s && (s.type === 'object' || s.type === 'array' || s.properties); |
| 1070 | + const wantsArray = s && s.type === 'array'; |
| 1071 | + const wantsBlock = s && (s.type === 'object' || wantsArray || s.properties); |
914 | 1072 | const indent = listItem ? ' '.repeat(listItem.contentIndent) : ' '.repeat(countIndent(lineNoComment)); |
915 | 1073 | const innerIndent = indent + ' '; |
916 | 1074 |
|
917 | | - const insertText = wantsBlock ? `${key}:\n${innerIndent}` : `${key}: `; |
| 1075 | + const insertText = wantsArray ? `${key}:\n${indent}` : (wantsBlock ? `${key}:\n${innerIndent}` : `${key}: `); |
918 | 1076 | const hasValueSuggestions = !wantsBlock && getValueSuggestions(s).length > 0; |
919 | 1077 |
|
920 | 1078 | return { |
|
0 commit comments