mancha
Version:
Javscript HTML rendering engine
680 lines • 30.4 kB
JavaScript
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { JSDOM } from "jsdom";
import * as ts from "typescript";
import { getAttributeOrDataset } from "./dome.js";
import * as expressions from "./expressions/index.js";
import { TRUSTED_DATA_ATTRIBS } from "./trusted_attributes.js";
async function getDiagnostics(content, options) {
// Create temp file in the same directory as the HTML file (or cwd if no filePath)
// This allows TypeScript's module resolution to find node_modules properly
const baseDir = options.filePath ? path.dirname(path.resolve(options.filePath)) : process.cwd();
const tempFilePath = path.join(baseDir, `temp_type_check_${Math.random().toString(36).substring(2, 15)}.ts`);
await fs.writeFile(tempFilePath, content);
try {
const compilerOptions = {
noEmit: true,
strict: options.strict,
strictNullChecks: options.strict,
target: ts.ScriptTarget.ES2022,
module: ts.ModuleKind.ESNext,
moduleResolution: ts.ModuleResolutionKind.Bundler,
baseUrl: baseDir,
lib: ["lib.es2022.full.d.ts", "lib.dom.d.ts"],
skipLibCheck: true,
skipDefaultLibCheck: false,
allowImportingTsExtensions: true,
resolveJsonModule: true,
allowSyntheticDefaultImports: true,
};
const host = ts.createCompilerHost(compilerOptions);
const program = ts.createProgram([tempFilePath], compilerOptions, host);
const allDiagnostics = ts.getPreEmitDiagnostics(program);
// Filter diagnostics:
// 1. Only keep semantic errors (2000-2999) and strict mode errors (18000-18999)
// 2. Only keep diagnostics from our temp file (ignore errors from imported type definitions)
const diagnostics = allDiagnostics.filter((d) => ((d.code >= 2000 && d.code < 3000) || (d.code >= 18000 && d.code < 19000)) &&
d.file?.fileName === tempFilePath);
return { diagnostics, tempFilePath };
}
finally {
await fs.unlink(tempFilePath);
}
}
const AST_FACTORY = new expressions.EvalAstFactory();
function parseExpression(expression) {
const ast = expressions.parse(expression, AST_FACTORY);
if (!ast) {
throw new Error(`Failed to parse expression "${expression}"`);
}
return ast;
}
// Replace @import:MODULE_PATH:TYPE_NAME with import("MODULE_PATH").TYPE_NAME
// and resolve relative paths to absolute paths
function replaceImportSyntax(typeString, baseDir) {
return typeString.replace(/@import:([^:]+):([A-Za-z_][A-Za-z0-9_]*)/g, (_match, modulePath, typeName) => {
// If baseDir is provided and the path is relative, resolve it to an absolute path
if (baseDir && (modulePath.startsWith("./") || modulePath.startsWith("../"))) {
const absolutePath = path.resolve(baseDir, modulePath);
return `import("${absolutePath}").${typeName}`;
}
return `import("${modulePath}").${typeName}`;
});
}
function isPlainObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function ensurePlainObject(value, context) {
if (!isPlainObject(value)) {
throw new Error(`${context} must evaluate to an object`);
}
return value;
}
function descriptorToTypeScript(descriptor, baseDir, key) {
if (typeof descriptor !== "string") {
const description = typeof descriptor === "object" && descriptor !== null
? JSON.stringify(descriptor)
: String(descriptor);
throw new Error(`Type for "${key}" must be a string, received ${description}`);
}
return replaceImportSyntax(descriptor, baseDir);
}
function parseTypesAttribute(raw, baseDir) {
const ast = parseExpression(raw);
const value = ast.evaluate({});
const typeObject = ensurePlainObject(value, ":types expression");
const result = new Map();
for (const [name, descriptor] of Object.entries(typeObject)) {
result.set(name, descriptorToTypeScript(descriptor, baseDir, name));
}
return result;
}
// Extract $$-prefixed identifiers from expressions (query parameters).
function extractQueryParamIdentifiers(scope) {
const identifiers = new Set();
const expressions = collectExpressions(scope);
// Match $$identifier pattern (valid JS identifier after $$).
const pattern = /\$\$([a-zA-Z_][a-zA-Z0-9_]*)/g;
for (const entry of expressions) {
for (const match of entry.expression.matchAll(pattern)) {
identifiers.add(`$$${match[1]}`);
}
}
return identifiers;
}
function buildTypeScriptSource(types, scope, baseDir) {
const namespace = "M"; // Use a constant namespace
// Add reference directive for DOM lib
const libDirectives = '/// <reference lib="dom" />\n/// <reference lib="es2021" />';
// Default mancha-specific globals available in all templates
const defaultGlobals = ["declare const $elem: Element;", "declare const $event: Event;"].join("\n");
// Apply import syntax transformation to all type values
const declarations = Array.from(types.entries())
.map(([key, value]) => {
const resolvedType = replaceImportSyntax(value, baseDir);
return `declare let ${key}: ${resolvedType};`;
})
.join("\n");
// Global augmentations
const globalAugmentations = [
"interface TemplateElement extends Element {",
" close(): void;",
" showModal(): void;",
"}",
"",
"interface Element {",
" querySelector<K extends keyof HTMLElementTagNameMap>(selector: K): HTMLElementTagNameMap[K];",
" querySelector(selector: string): TemplateElement;",
"}",
"",
"interface Document {",
" querySelector<K extends keyof HTMLElementTagNameMap>(selector: K): HTMLElementTagNameMap[K];",
" querySelector(selector: string): TemplateElement;",
"}",
].join("\n");
const prefix = `${libDirectives}\n${globalAugmentations}\nnamespace ${namespace} {\n${defaultGlobals}\n${declarations}\n`;
const suffix = "\n}";
const expressionMap = new Map();
let currentOffset = prefix.length;
// Recursively build checks from scope
const buildChecks = (currentScope, indent = "") => {
let result = "";
// Add expressions in current scope
for (const entry of currentScope.expressions) {
const expressionPrefix = `${indent}(`;
const code = `${expressionPrefix}${entry.expression});\n`;
expressionMap.set(currentOffset + expressionPrefix.length, entry);
result += code;
currentOffset += code.length;
}
// Add for loops with their nested scopes
for (const forLoop of currentScope.forLoops) {
const expressionPrefix = `${indent}for (const ${forLoop.itemName} of (`;
const forLoopHeader = `${expressionPrefix}${forLoop.itemsExpression.expression})) {\n`;
expressionMap.set(currentOffset + expressionPrefix.length, forLoop.itemsExpression);
result += forLoopHeader;
currentOffset += forLoopHeader.length;
result += buildChecks(forLoop.scope, `${indent} `);
const closingBrace = `${indent}}\n`;
result += closingBrace;
currentOffset += closingBrace.length;
}
return result;
};
const checks = buildChecks(scope);
const source = `${prefix}${checks}${suffix}`;
return { source, expressionMap };
}
function getAttributeValueRange(element, attributeName, attrValue, valueSubstring, dom, html, valueOffsetInAttrValue) {
const elementLocation = dom.nodeLocation(element);
if (!elementLocation)
return undefined;
// biome-ignore lint/suspicious/noExplicitAny: location structure is complex
const attrLocation = elementLocation.attrs?.[attributeName];
if (!attrLocation)
return undefined;
const attrText = html.slice(attrLocation.startOffset, attrLocation.endOffset);
const equalsIndex = attrText.indexOf("=");
if (equalsIndex === -1) {
return {
start: attrLocation.startOffset,
length: attrLocation.endOffset - attrLocation.startOffset,
};
}
let cursor = equalsIndex + 1;
while (cursor < attrText.length && /\s/.test(attrText[cursor] ?? "")) {
cursor++;
}
let baseOffset = cursor;
if (cursor < attrText.length && (attrText[cursor] === '"' || attrText[cursor] === "'")) {
baseOffset = cursor + 1;
}
let offsetWithinValue = valueOffsetInAttrValue;
if (offsetWithinValue == null || offsetWithinValue < 0) {
offsetWithinValue = attrValue.indexOf(valueSubstring);
if (offsetWithinValue < 0) {
offsetWithinValue = attrValue.trimStart().indexOf(valueSubstring);
if (offsetWithinValue < 0) {
offsetWithinValue = 0;
}
else {
offsetWithinValue += attrValue.length - attrValue.trimStart().length;
}
}
}
const start = attrLocation.startOffset + baseOffset + offsetWithinValue;
return { start, length: valueSubstring.length };
}
function getTextExpressionRange(textNode, match, trimmedExpression, dom) {
const location = dom.nodeLocation(textNode);
if (!location)
return undefined;
const rawExpression = match[1] ?? "";
// biome-ignore lint/suspicious/noExplicitAny: location structure is complex
const startOffset = location.startOffset;
const leadingWhitespace = rawExpression.length - rawExpression.trimStart().length;
const start = startOffset + (match.index ?? 0) + 2 + leadingWhitespace;
return { start, length: trimmedExpression.length };
}
// Helper to check if an element is nested within another :types element
function hasTypesAncestor(element) {
let parent = element.parentElement;
while (parent) {
if (getAttributeOrDataset(parent, "types", ":")) {
return true;
}
parent = parent.parentElement;
}
return false;
}
// Compute type of for-loop variable based on the items expression
function computeForLoopVariableType(itemsType) {
// Handle array types: string[] -> string, Array<T> -> T
if (itemsType.endsWith("[]")) {
return itemsType.slice(0, -2);
}
const match = itemsType.match(/^Array<(.+)>$/);
if (match) {
return match[1];
}
// For complex types, try to infer element type
// This is a simple heuristic - for more complex cases, TypeScript would need to resolve the type
return "any";
}
// Find for-loop ancestors of an element and compute their variable types
function getForLoopContext(element, typesMap) {
const forLoopTypes = new Map();
let current = element.parentElement;
while (current) {
const forAttr = getAttributeOrDataset(current, "for", ":");
if (forAttr) {
const parts = forAttr.split(" in ");
const itemName = parts[0].trim();
const itemsExpression = parts[1].trim();
// Try to determine the type of the items expression
// First check if it's a simple variable name in our types map
const itemsType = typesMap.get(itemsExpression);
if (itemsType) {
const itemType = computeForLoopVariableType(itemsType);
forLoopTypes.set(itemName, itemType);
}
else if (forLoopTypes.has(itemsExpression)) {
// Check if it's a for-loop variable from an outer loop (for shadowing)
const itemsType = forLoopTypes.get(itemsExpression);
if (itemsType) {
const itemType = computeForLoopVariableType(itemsType);
forLoopTypes.set(itemName, itemType);
}
}
else {
// Try to infer from more complex expressions like user.scores
// For now, use a simple heuristic
const match = itemsExpression.match(/^(\w+)\.(\w+)$/);
if (match) {
const parentType = forLoopTypes.get(match[1]);
if (parentType) {
// e.g., user.scores where user is already in forLoopTypes
const propertyName = match[2];
// Extract property type from parent type (simple heuristic)
const propMatch = parentType.match(new RegExp(`${propertyName}:\\s*([^,}]+)`));
if (propMatch) {
const propType = propMatch[1].trim();
const itemType = computeForLoopVariableType(propType);
forLoopTypes.set(itemName, itemType);
}
}
}
}
}
// Stop if we hit a :types boundary (don't cross into parent :types scopes)
if (current !== element && getAttributeOrDataset(current, "types", ":")) {
break;
}
current = current.parentElement;
}
return forLoopTypes;
}
// Recursively process a :types element and its nested :types descendants
async function processTypesElement(element, parentTypes, options, processedElements, dom, html, htmlSourceFile) {
// Skip if already processed
if (processedElements.has(element)) {
return [];
}
processedElements.add(element);
const allDiagnostics = [];
const typesAttr = getAttributeOrDataset(element, "types", ":");
if (!typesAttr)
return allDiagnostics;
const baseDir = options.filePath ? path.dirname(path.resolve(options.filePath)) : undefined;
const typesAttrName = element.hasAttribute(":types") ? ":types" : "data-types";
try {
// Parse types for this element
const elementTypes = parseTypesAttribute(typesAttr, baseDir);
// Merge with parent types (element types override parent types)
const mergedTypes = new Map([...parentTypes, ...elementTypes]);
// Get expressions for this element (excluding nested :types descendants)
const scope = getExpressionsExcludingNestedTypes(element, dom, html);
// Extract query params and add to merged types if not present
const queryParams = extractQueryParamIdentifiers(scope);
for (const param of queryParams) {
if (!mergedTypes.has(param)) {
mergedTypes.set(param, "string | null");
}
}
const expressionDiagnostics = validateExpressions(scope, htmlSourceFile, options);
allDiagnostics.push(...expressionDiagnostics);
// Type check expressions in this scope
const { source, expressionMap } = buildTypeScriptSource(mergedTypes, scope, baseDir);
if (options.debug) {
console.log("\n=== Generated TypeScript Source ===");
console.log(source);
console.log("\n=== Expression Map (TS offset -> HTML range) ===");
for (const [offset, entry] of expressionMap.entries()) {
console.log(` TS offset ${offset}: "${entry.expression}" -> HTML range ${JSON.stringify(entry.range)}`);
}
console.log(`\n=== HTML Length: ${html.length} ===\n`);
}
const { diagnostics: rawDiagnostics, tempFilePath } = await getDiagnostics(source, options);
if (options.debug && rawDiagnostics.length > 0) {
console.log("=== Raw TypeScript Diagnostics ===");
console.log(` Generated source length: ${source.length}`);
console.log(` Temp file: ${tempFilePath}`);
for (const diag of rawDiagnostics) {
const msg = ts.flattenDiagnosticMessageText(diag.messageText, "\n");
console.log(` start=${diag.start}, length=${diag.length}, code=${diag.code}`);
console.log(` message: ${msg}`);
}
}
// Remap diagnostics to original source locations
for (const diag of rawDiagnostics) {
if (diag.start === undefined) {
allDiagnostics.push(diag);
continue;
}
let bestMatch;
for (const [offset, entry] of expressionMap.entries()) {
if (offset <= diag.start) {
if (!bestMatch || offset > bestMatch.offset) {
bestMatch = { offset, entry };
}
}
}
if (bestMatch?.entry.range) {
const { range } = bestMatch.entry;
const tsOffsetInGeneratedCode = diag.start - bestMatch.offset;
const newStart = range.start + tsOffsetInGeneratedCode;
const newLength = diag.length;
if (options.debug) {
const msg = ts.flattenDiagnosticMessageText(diag.messageText, "\n");
console.log(`=== Remapping diagnostic ===`);
console.log(` TS start=${diag.start}, matched offset=${bestMatch.offset}`);
console.log(` Expression: "${bestMatch.entry.expression}"`);
console.log(` HTML range: ${JSON.stringify(range)}`);
console.log(` newStart=${newStart}, newLength=${newLength}, in bounds: ${newStart < html.length}`);
console.log(` Message: ${msg}`);
}
allDiagnostics.push({
...diag,
file: htmlSourceFile,
start: newStart,
length: newLength,
});
}
else {
if (options.debug) {
const msg = ts.flattenDiagnosticMessageText(diag.messageText, "\n");
console.log(`=== Unmapped diagnostic ===`);
console.log(` TS start=${diag.start}, length=${diag.length}`);
console.log(` Message: ${msg}`);
}
allDiagnostics.push(diag);
}
}
// Find and process nested :types elements
const nestedTypesElements = findDirectNestedTypesElements(element);
for (const nestedElement of nestedTypesElements) {
// Compute for-loop context for the nested element
const forLoopContext = getForLoopContext(nestedElement, mergedTypes);
// Merge for-loop variables with parent types (for-loop variables take precedence)
const nestedParentTypes = new Map([...mergedTypes, ...forLoopContext]);
const nestedDiagnostics = await processTypesElement(nestedElement, nestedParentTypes, options, processedElements, dom, html, htmlSourceFile);
allDiagnostics.push(...nestedDiagnostics);
}
}
catch (error) {
const tagName = element.tagName?.toLowerCase() ?? "element";
const message = error instanceof Error ? error.message : typeof error === "string" ? error : String(error);
const attrRange = getAttributeValueRange(element, typesAttrName, typesAttr, typesAttr, dom, html, 0);
allDiagnostics.push({
file: attrRange ? htmlSourceFile : undefined,
start: attrRange?.start,
length: attrRange?.length,
category: ts.DiagnosticCategory.Error,
code: 91001,
source: "mancha-type-checker",
messageText: `Failed to evaluate :types on <${tagName}>: ${message}`,
});
}
return allDiagnostics;
}
// Find nested :types elements that are direct descendants (not grandchildren through other :types)
const TRUSTED_DATA_ATTRIBUTE_SET = new Set(TRUSTED_DATA_ATTRIBS.map((attr) => attr.toLowerCase()));
function findDirectNestedTypesElements(element) {
const result = [];
const walker = element.ownerDocument.createTreeWalker(element, 1); // 1 = SHOW_ELEMENT
while (walker.nextNode()) {
const node = walker.currentNode;
if (node === element)
continue;
const typesAttr = getAttributeOrDataset(node, "types", ":");
if (typesAttr) {
// Check if this is a direct nested :types (not nested within another :types first)
let parent = node.parentElement;
let foundIntermediateTypes = false;
while (parent && parent !== element) {
if (getAttributeOrDataset(parent, "types", ":")) {
foundIntermediateTypes = true;
break;
}
parent = parent.parentElement;
}
if (!foundIntermediateTypes) {
result.push(node);
}
}
}
return result;
}
function isExpressionAttribute(attrName) {
const normalized = attrName.toLowerCase();
// Skip path-based attributes since they contain file paths, not expressions.
if (PATH_ATTRIBUTES.includes(normalized)) {
return false;
}
if (normalized === ":types" || normalized === "data-types") {
return false;
}
if (normalized.startsWith(":")) {
return true;
}
if (!normalized.startsWith("data-")) {
return false;
}
return TRUSTED_DATA_ATTRIBUTE_SET.has(normalized);
}
// Get expressions excluding those in nested :types elements
function getExpressionsExcludingNestedTypes(root, dom, html) {
const scope = { expressions: [], forLoops: [] };
const processedForElements = new Set();
const processElement = (element, currentScope) => {
// Stop if we encounter a nested :types element
if (element !== root && getAttributeOrDataset(element, "types", ":")) {
return;
}
// Check if this element has a :for attribute
const forAttr = getAttributeOrDataset(element, "for", ":");
if (forAttr) {
if (processedForElements.has(element))
return;
processedForElements.add(element);
const parts = forAttr.split(" in ");
const itemName = parts[0].trim();
const itemsExpression = parts[1].trim();
const attributeName = element.hasAttribute(":for")
? ":for"
: element.hasAttribute("data-for")
? "data-for"
: ":for";
const attr = element.getAttributeNode(attributeName);
const attrValue = attr?.value ?? "";
const valueIndex = attrValue.lastIndexOf(itemsExpression);
const itemsExpressionEntry = {
expression: itemsExpression,
source: {
kind: "attribute",
element,
attributeName,
attributeKind: "for-items",
},
range: getAttributeValueRange(element, attributeName, attrValue, itemsExpression, dom, html, valueIndex),
};
currentScope.expressions.push(itemsExpressionEntry);
const forScope = { expressions: [], forLoops: [] };
processDescendants(element, forScope);
currentScope.forLoops.push({
itemName,
itemsExpression: itemsExpressionEntry,
scope: forScope,
});
}
else {
// Process attributes
for (const attr of Array.from(element.attributes)) {
if (isExpressionAttribute(attr.name)) {
currentScope.expressions.push({
expression: attr.value,
source: {
kind: "attribute",
element,
attributeName: attr.name,
attributeKind: "default",
},
range: getAttributeValueRange(element, attr.name, attr.value, attr.value, dom, html, 0),
});
}
}
// Process children
for (const child of Array.from(element.childNodes)) {
if (child.nodeType === 1) {
processElement(child, currentScope);
}
else if (child.nodeType === 3) {
processTextNode(child, currentScope);
}
}
}
};
const processDescendants = (element, currentScope) => {
for (const child of Array.from(element.childNodes)) {
if (child.nodeType === 1) {
processElement(child, currentScope);
}
else if (child.nodeType === 3) {
processTextNode(child, currentScope);
}
}
};
const processTextNode = (textNode, currentScope) => {
const text = textNode.nodeValue;
if (text) {
const matches = text.matchAll(/{{(.*?)}}/g);
for (const match of matches) {
const trimmedExpression = match[1]?.trim() ?? "";
if (!trimmedExpression)
continue;
currentScope.expressions.push({
expression: trimmedExpression,
source: { kind: "text", node: textNode },
range: getTextExpressionRange(textNode, match, trimmedExpression, dom),
});
}
}
};
// Start processing from root
processElement(root, scope);
return scope;
}
function collectExpressions(scope, result = []) {
for (const entry of scope.expressions) {
result.push(entry);
}
for (const loop of scope.forLoops) {
result.push(loop.itemsExpression);
collectExpressions(loop.scope, result);
}
return result;
}
function describeExpressionSource(source) {
if (source.kind === "attribute") {
const tagName = source.element.tagName?.toLowerCase() ?? "element";
if (source.attributeKind === "for-items") {
return `:for items expression on <${tagName}>`;
}
return `attribute "${source.attributeName}" on <${tagName}>`;
}
const parentTag = source.node.parentElement?.tagName?.toLowerCase();
if (parentTag) {
return `text interpolation inside <${parentTag}>`;
}
return "text interpolation";
}
function createExpressionDiagnostic(entry, error, htmlSourceFile) {
const expressionPreview = entry.expression.trim() || entry.expression;
const errorMessage = error instanceof Error ? error.message : typeof error === "string" ? error : String(error);
const message = `Unsupported expression (${expressionPreview}) in ${describeExpressionSource(entry.source)}: ${errorMessage}`;
return {
file: entry.range ? htmlSourceFile : undefined,
start: entry.range?.start,
length: entry.range?.length,
category: ts.DiagnosticCategory.Error,
code: 91002,
source: "mancha-type-checker",
messageText: message,
};
}
// Attributes whose values are file paths, not expressions.
const PATH_ATTRIBUTES = [":render", "data-render"];
const RESTRICTED_PROPERTY_NAMES = new Set([
"src",
"href",
"value",
"checked",
"selected",
"disabled",
"readonly",
"open",
"multiple",
"required",
]);
function validateExpressions(scope, htmlSourceFile, options) {
const diagnostics = [];
const expressions = collectExpressions(scope);
const syntaxLevel = options.propertySyntaxLevel ?? "error";
for (const entry of expressions) {
const candidate = entry.expression.trim();
if (!candidate)
continue;
// Skip :render attributes since they contain file paths, not expressions.
if (entry.source.kind === "attribute" && PATH_ATTRIBUTES.includes(entry.source.attributeName)) {
continue;
}
if (entry.source.kind === "attribute" &&
syntaxLevel !== "ignore" &&
entry.source.attributeName.startsWith(":")) {
const propName = entry.source.attributeName.slice(1);
if (RESTRICTED_PROPERTY_NAMES.has(propName)) {
const category = syntaxLevel === "warning" ? ts.DiagnosticCategory.Warning : ts.DiagnosticCategory.Error;
diagnostics.push({
file: entry.range ? htmlSourceFile : undefined,
start: entry.range?.start,
length: entry.range?.length,
category,
code: 91003, // New code
source: "mancha-type-checker",
messageText: `Property '${propName}' must be bound using ':prop:${propName}' instead of ':${propName}'`,
});
}
}
try {
parseExpression(candidate);
}
catch (error) {
diagnostics.push(createExpressionDiagnostic(entry, error, htmlSourceFile));
}
}
return diagnostics;
}
export async function typeCheck(html, options) {
const dom = new JSDOM(html, { includeNodeLocations: true });
const htmlSourceFile = ts.createSourceFile(options.filePath ?? "template.html", html, ts.ScriptTarget.Latest, false, ts.ScriptKind.Unknown);
const allTypeNodes = dom.window.document.querySelectorAll("[\\:types], [data-types]");
const allDiagnostics = [];
const processedElements = new Set();
// Find top-level :types elements (not nested within other :types)
const topLevelTypeNodes = Array.from(allTypeNodes).filter((node) => !hasTypesAncestor(node));
// Process each top-level :types element and its nested descendants
for (const node of topLevelTypeNodes) {
const diagnostics = await processTypesElement(node, new Map(), options, processedElements, dom, html, htmlSourceFile);
allDiagnostics.push(...diagnostics);
}
const documentRoot = dom.window.document.documentElement ??
dom.window.document.body ??
dom.window.document.firstElementChild;
if (documentRoot) {
const globalScope = getExpressionsExcludingNestedTypes(documentRoot, dom, html);
const globalExpressionDiagnostics = validateExpressions(globalScope, htmlSourceFile, options);
allDiagnostics.push(...globalExpressionDiagnostics);
}
return allDiagnostics;
}
//# sourceMappingURL=type_checker.js.map