meld
Version:
Meld: A template language for LLM prompts
280 lines (251 loc) • 7.95 kB
text/typescript
import { DirectiveNode, ImportDirectiveData } from 'meld-spec';
import { MeldDirectiveError, DirectiveLocation } from '@core/errors/MeldDirectiveError.js';
import { DirectiveErrorCode } from '@services/pipeline/DirectiveService/errors/DirectiveError.js';
import { ErrorSeverity } from '@core/errors/MeldError.js';
// Local definition for StructuredPath interface
interface StructuredPath {
raw: string;
normalized?: string;
structured?: {
base?: string;
segments?: string[];
variables?: {
special?: string[];
[key: string]: any;
};
};
}
// Definition for Import item from meld-ast 3.4.0
interface ImportItem {
name: string;
alias?: string;
}
/**
* Convert AST SourceLocation to DirectiveLocation
*/
function convertLocation(location: any): DirectiveLocation | undefined {
if (!location) return undefined;
return {
line: location.start?.line || 0,
column: location.start?.column || 0,
filePath: location.filePath
};
}
/**
* Validates @import directives
*/
export function validateImportDirective(node: DirectiveNode): void {
const directive = node.directive as ImportDirectiveData;
// Validate that a path exists
if (!directive.path) {
throw new MeldDirectiveError(
'Import directive requires a path',
'import',
{
location: convertLocation(node.location),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
// Get path value from structured path or string
let pathValue: string;
if (typeof directive.path === 'string') {
pathValue = directive.path;
} else if (directive.path.raw) {
pathValue = directive.path.raw;
} else if (directive.path.normalized) {
pathValue = directive.path.normalized;
} else {
throw new MeldDirectiveError(
'Import directive has an invalid path format',
'import',
{
location: convertLocation(node.location),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
// Validate path is not empty
if (pathValue.trim() === '') {
throw new MeldDirectiveError(
'Import path cannot be empty',
'import',
{
location: convertLocation(node.location),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
// Allow path variables starting with $ but still validate ..
if (!pathValue.startsWith('$') && pathValue.includes('..')) {
throw new MeldDirectiveError(
'Import path cannot contain parent directory references (..) unless using a path variable',
'import',
{
location: convertLocation(node.location),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
// Validate imports - use structured imports array if available, otherwise use importList
if (directive.imports && Array.isArray(directive.imports)) {
validateStructuredImports(directive.imports, node);
} else if (directive.importList && directive.importList !== '*') {
validateImportList(directive.importList, node);
}
}
/**
* Validates structured imports array from meld-ast 3.4.0
* @private
*/
function validateStructuredImports(imports: ImportItem[], node: DirectiveNode): void {
if (imports.length === 0) {
// Empty imports array is valid - equivalent to "*"
return;
}
// Check if there's a wildcard import
if (imports.some(imp => imp.name === '*')) {
if (imports.length > 1) {
throw new MeldDirectiveError(
'Wildcard import (*) cannot be combined with other imports',
'import',
{
location: convertLocation(node.location),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
return; // Single wildcard import is valid
}
// Validate each import item
for (const item of imports) {
if (!item.name || item.name.trim() === '') {
throw new MeldDirectiveError(
'Import identifier cannot be empty',
'import',
{
location: convertLocation(node.location),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
// Only validate the alias if it's a non-undefined, non-null string
// This allows for cases where alias is undefined (no alias specified)
if (item.alias !== undefined && item.alias !== null && item.alias.trim() === '') {
throw new MeldDirectiveError(
'Import alias cannot be empty',
'import',
{
location: convertLocation(node.location),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
}
}
/**
* Validates import list
* @private
*/
function validateImportList(importList: string, node: DirectiveNode): void {
if (importList === '*') {
return; // Wildcard import is valid
}
// Remove brackets if present using direct string manipulation
let cleanList = importList;
if (cleanList.startsWith('[') && cleanList.endsWith(']')) {
cleanList = cleanList.substring(1, cleanList.length - 1);
}
// Split by commas and validate each part
const parts = cleanList.split(',');
for (const part of parts) {
const trimmedPart = part.trim();
if (trimmedPart === '') {
throw new MeldDirectiveError(
'Import list contains an empty item',
'import',
{
location: convertLocation(node.location),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
// Handle colon syntax (var:alias)
if (trimmedPart.includes(':')) {
const [name, alias] = trimmedPart.split(':').map(s => s.trim());
if (!name || name === '') {
throw new MeldDirectiveError(
'Import identifier cannot be empty',
'import',
{
location: convertLocation(node.location),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
if (!alias || alias === '') {
throw new MeldDirectiveError(
'Import alias cannot be empty',
'import',
{
location: convertLocation(node.location),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
continue;
}
// Handle 'as' syntax (var as alias) without regex
const asIndex = trimmedPart.indexOf(' as ');
if (asIndex !== -1) {
const name = trimmedPart.substring(0, asIndex).trim();
const alias = trimmedPart.substring(asIndex + 4).trim();
if (!name || name === '') {
throw new MeldDirectiveError(
'Import identifier cannot be empty',
'import',
{
location: convertLocation(node.location),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
// When using 'as' syntax, the alias must not be empty
if (!alias || alias === '') {
throw new MeldDirectiveError(
'Import alias cannot be empty',
'import',
{
location: convertLocation(node.location),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
continue;
}
// Simple import without alias
if (trimmedPart === '') {
throw new MeldDirectiveError(
'Import identifier cannot be empty',
'import',
{
location: convertLocation(node.location),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
}
}