"use strict"; const COMPONENT_DECLARATION = /\b(component|layout)\s+([A-Za-z_$][\w$]*)\s*\{/; function findMatchingBrace(source, openingBrace) { let depth = 0; let quote = null; let escaped = false; for (let index = openingBrace; index < source.length; index += 1) { const character = source[index]; if (quote) { if (escaped) escaped = false; else if (character === "\\") escaped = true; else if (character === quote) quote = null; continue; } if (character === '"' || character === "'" || character === "`") quote = character; else if (character === "{") depth += 1; else if (character === "}" && --depth === 0) return index; } return -1; } function inferType(defaultValue) { const value = defaultValue.trim(); if (value === "undefined") return "unknown"; if (/^(?:true|false)$/.test(value)) return "boolean"; if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(value)) return "number"; if (/^["'`]/.test(value)) return "string"; if (value.startsWith("[")) return "array"; if (value.startsWith("{")) return "object"; if (value === "null") return "null"; return "unknown"; } function runtimeType(type) { const value = String(type || "") .trim() .replace(/^readonly\s+/, ""); if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "string"; if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "number"; if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "boolean"; if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "bigint"; if (/^(?:Array\s*<|ReadonlyArray\s*<|.+\[\])/.test(value) || /^\[/.test(value)) return "array"; if (/^(?:Record\s*<|object\b|\{)/.test(value)) return "object"; if (/=>|^Function$/.test(value)) return "function"; return value || "unknown"; } function stringLiteral(value) { const match = /^(?:"([\s\S]*)"|'([\s\S]*)'|`([\s\S]*)`)$/.exec(value.trim()); return match ? (match[1] ?? match[2] ?? match[3]) : null; } function inferOptions(source, propName) { const escaped = propName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const options = new Set(); const comparisons = new RegExp(`\\b${escaped}\\s*(?:===|!==|==|!=)\\s*(["'])(.*?)\\1`, "g"); let match; while ((match = comparisons.exec(source)) !== null) options.add(match[2]); return [...options].sort(); } function parseComponentMetadata(source, uri = null) { const declaration = COMPONENT_DECLARATION.exec(source); if (!declaration) return null; const propsKeyword = /\bprops\s*\{/.exec(source.slice(declaration.index)); const props = []; const events = []; if (propsKeyword) { const start = declaration.index + propsKeyword.index; const openingBrace = source.indexOf("{", start); const closingBrace = findMatchingBrace(source, openingBrace); const bodyEnd = closingBrace === -1 ? source.length : closingBrace; const body = source.slice(openingBrace + 1, bodyEnd); const linePattern = /^(?:\s*\/\/\s*@required\s*\r?\n)?\s*([A-Za-z_$][\w$]*)(?:\s*:\s*([^=\r\n]+?))?(?:\s*=\s*(.*?))?\s*$/gm; let propMatch; while ((propMatch = linePattern.exec(body)) !== null) { const annotation = propMatch[2] && propMatch[2].trim(); const hasDefault = propMatch[3] !== undefined; const defaultValue = hasDefault ? propMatch[3] : "undefined"; const name = propMatch[1]; props.push({ name, defaultValue, required: !hasDefault || defaultValue.trim() === "undefined" || /^\s*\/\/\s*@required/m.test(propMatch[0]), type: annotation || inferType(defaultValue), options: inferOptions(source, name), }); } const eventPattern = /^\s*@event\s+([A-Za-z_$][\w$]*)\s*=\s*function\s*$/gm; let eventMatch; while ((eventMatch = eventPattern.exec(body)) !== null) events.push(eventMatch[1]); } return { kind: declaration[1], name: declaration[2], props, events, uri }; } function unwrapAttributeValue(value) { const trimmed = value.trim(); if (trimmed.startsWith("{") && trimmed.endsWith("}")) return trimmed.slice(1, -1).trim(); return trimmed; } function attributeValueType(value, symbols = new Map()) { const unwrapped = unwrapAttributeValue(value); if (/^[A-Za-z_$][\w$]*$/.test(unwrapped) && symbols.has(unwrapped)) { return symbols.get(unwrapped); } if (/^(?:true|false)$/.test(unwrapped)) return "boolean"; if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(unwrapped)) return "number"; if (unwrapped.startsWith("[")) return "array"; if (unwrapped.startsWith("{")) return "object"; return "string"; } function isTypeCompatible(prop, value, symbols = new Map()) { const expected = runtimeType(prop.type); if (expected === "unknown" || expected === "null") return true; const actual = runtimeType(attributeValueType(value, symbols)); if (expected === actual) return true; if (expected === "number" && actual === "string") return Number.isFinite(Number(value)); if (expected === "boolean" && actual === "string") return /^(?:true|false|1|0|yes|no|on|off)?$/i.test(value); return false; } function parseComponentTags(source) { const tags = []; const pattern = /<([A-Z][A-Za-z0-9_$]*)(\s[\s\S]*?)?\s*\/?>/g; let match; while ((match = pattern.exec(source)) !== null) { const attributes = []; const attributeSource = match[2] || ""; const attributeOffset = match.index + match[0].indexOf(attributeSource); const attributePattern = /([^\s=/>]+)\s*=\s*(["'])([\s\S]*?)\2/g; let attributeMatch; while ((attributeMatch = attributePattern.exec(attributeSource)) !== null) { const nameStart = attributeOffset + attributeMatch.index; attributes.push({ name: attributeMatch[1], value: attributeMatch[3], nameStart, nameEnd: nameStart + attributeMatch[1].length, }); } tags.push({ name: match[1], start: match.index, end: match.index + match[0].length, nameStart: match.index + 1, nameEnd: match.index + 1 + match[1].length, attributes, }); } return tags; } function validateComponentTags(source, components) { const diagnostics = []; const own = parseComponentMetadata(source); const symbols = new Map((own?.props || []).map((prop) => [prop.name, prop.type])); const statePattern = /^\s*state\s+([A-Za-z_$][\w$]*)(?:\s*:\s*([^=\r\n]+?))?\s*=\s*(.*?)\s*$/gm; let stateMatch; while ((stateMatch = statePattern.exec(source)) !== null) { symbols.set(stateMatch[1], stateMatch[2]?.trim() || inferType(stateMatch[3])); } for (const tag of parseComponentTags(source)) { const component = components.get(tag.name); if (!component) continue; const provided = new Map(tag.attributes.map((attribute) => [attribute.name, attribute])); const declared = new Map(component.props.map((prop) => [prop.name, prop])); const declaredEvents = new Set(component.events || []); for (const prop of component.props) { if (prop.required && !provided.has(prop.name)) { diagnostics.push({ severity: "error", code: "wrn-missing-component-prop", message: `<${tag.name}> requires prop \`${prop.name}\` (${prop.type}).`, start: tag.nameStart, end: tag.nameEnd, }); } } for (const attribute of tag.attributes) { if (attribute.name.startsWith("@")) { const eventName = attribute.name.slice(1); if (!declaredEvents.has(eventName)) { diagnostics.push({ severity: "warning", code: "wrn-unknown-component-event", message: `Unknown event \`${eventName}\` on <${tag.name}>.`, start: attribute.nameStart, end: attribute.nameEnd, }); } continue; } const prop = declared.get(attribute.name); if (!prop) { diagnostics.push({ severity: "warning", code: "wrn-unknown-component-prop", message: `Unknown prop \`${attribute.name}\` on <${tag.name}>.`, start: attribute.nameStart, end: attribute.nameEnd, }); continue; } const actualType = attributeValueType(attribute.value, symbols); if (!isTypeCompatible(prop, attribute.value, symbols)) { diagnostics.push({ severity: "error", code: "wrn-component-prop-type", message: `Prop \`${attribute.name}\` on <${tag.name}> expects ${prop.type}, but received ${runtimeType(actualType)}.`, start: attribute.nameStart, end: attribute.nameEnd, }); } const literal = stringLiteral(attribute.value) ?? attribute.value; if (prop.options.length > 0 && !prop.options.includes(literal)) { diagnostics.push({ severity: "warning", code: "wrn-component-prop-option", message: `Prop \`${attribute.name}\` should be one of: ${prop.options.join(", ")}.`, start: attribute.nameStart, end: attribute.nameEnd, }); } } } return diagnostics; } module.exports = { attributeValueType, inferType, runtimeType, isTypeCompatible, parseComponentMetadata, parseComponentTags, validateComponentTags, };