UNPKG

type-compiler

Version:

A TypeScript compiler plugin for enhanced runtime type checking and analysis with Zod validation

492 lines (491 loc) 23.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); const ts = __importStar(require("typescript/lib/tsserverlibrary")); /** * Check if a field name matches any pattern in the specialFieldValidators */ function getMatchingValidator(fieldName, specialFieldValidators, parentTypeName, contextualValidators) { // First check contextual validators if parent type name is provided if (parentTypeName && contextualValidators) { // Check for exact parent type match if (parentTypeName in contextualValidators) { const contextValidator = contextualValidators[parentTypeName]; // Handle different validator formats if (typeof contextValidator === 'object' && !('pattern' in contextValidator)) { // It's a direct field mapping const fieldsMap = contextValidator; if (fieldName in fieldsMap) { const validator = fieldsMap[fieldName]; if (typeof validator === 'string') { return { validator, source: `contextual (${parentTypeName})` }; } else { return { validator: validator.validator, source: `contextual (${parentTypeName})`, errorMessage: validator.errorMessage }; } } } } // Check for pattern-based parent type match for (const pattern in contextualValidators) { const contextValidator = contextualValidators[pattern]; // Skip non-pattern validators if (typeof contextValidator !== 'object' || !('pattern' in contextValidator) || !contextValidator.pattern) { continue; } // Safely assert the type for the pattern-based validator const patternValidator = contextValidator; try { const regex = new RegExp(pattern); if (regex.test(parentTypeName) && patternValidator.fields) { // Check if the field exists in this pattern-based validator if (fieldName in patternValidator.fields) { const validator = patternValidator.fields[fieldName]; if (typeof validator === 'string') { return { validator, source: `contextual pattern (${pattern})`, pattern }; } else { return { validator: validator.validator, source: `contextual pattern (${pattern})`, pattern, errorMessage: validator.errorMessage }; } } } } catch (e) { // Invalid regex pattern console.warn(`Invalid regex pattern in contextualValidators: ${pattern}`); } } } // Then check special field validators (direct match) if (fieldName in specialFieldValidators) { const validator = specialFieldValidators[fieldName]; if (typeof validator === 'string') { return { validator, source: 'field name' }; } else if (validator && typeof validator === 'object' && 'validator' in validator) { return { validator: validator.validator, source: 'field name', errorMessage: validator.errorMessage }; } } // Finally check special field validators with pattern matching for (const pattern in specialFieldValidators) { const validatorConfig = specialFieldValidators[pattern]; if (validatorConfig && typeof validatorConfig === 'object' && 'pattern' in validatorConfig && validatorConfig.pattern) { try { const regex = new RegExp(pattern); if (regex.test(fieldName)) { return { validator: validatorConfig.validator, source: `pattern (${pattern})`, pattern, errorMessage: validatorConfig.errorMessage }; } } catch (e) { // Invalid regex pattern console.warn(`Invalid regex pattern in specialFieldValidators: ${pattern}`); } } } // No match found return { validator: null, source: 'none' }; } /** * Get a human-readable description of a validator */ function getValidatorDescription(validator) { if (validator.includes('.email()')) return 'email address'; if (validator.includes('.url()')) return 'URL'; if (validator.includes('.uuid()')) return 'UUID'; if (validator.includes('.date()')) return 'date'; if (validator.includes('.min(') && validator.includes('.max(')) return 'number within range'; if (validator.includes('.regex(')) return 'matches regex pattern'; if (validator.includes('.min(')) return 'minimum value/length'; if (validator.includes('.max(')) return 'maximum value/length'; if (validator.includes('.ip()')) return 'IP address'; if (validator.includes('.enum(')) return 'one of a set of values'; return 'custom validation'; } /** * Check if a node is a property declaration in an interface or type */ function isPropertyDeclaration(node) { return node.kind === ts.SyntaxKind.PropertySignature; } /** * Create the TypeScript Language Service plugin */ function init(modules) { const typescript = modules.typescript; function create(info) { // Get plugin configuration const config = info.config; const specialFieldValidators = config.specialFieldValidators || {}; // Create a proxy for the language service const proxy = Object.create(null); const ls = info.languageService; /** * Add validator information to hover tooltips */ proxy.getQuickInfoAtPosition = (fileName, position) => { const original = ls.getQuickInfoAtPosition(fileName, position); if (!original) return original; // Get source file and node at position const program = ls.getProgram(); if (!program) return original; const sourceFile = program.getSourceFile(fileName); if (!sourceFile) return original; // Find the node at the current position const node = findNodeAtPosition(sourceFile, position); if (!node) return original; // Check if this is a property declaration in an interface or type if (isPropertyDeclaration(node) && node.name) { const fieldName = node.name.getText(); // Find parent type name let parentTypeName; let parent = node.parent; while (parent) { if (ts.isInterfaceDeclaration(parent) && parent.name) { parentTypeName = parent.name.getText(); break; } else if (ts.isTypeAliasDeclaration(parent) && parent.name) { parentTypeName = parent.name.getText(); break; } else if (ts.isTypeLiteralNode(parent)) { parentTypeName = 'AnonymousType'; break; } parent = parent.parent; } // Get validator information const validatorInfo = getMatchingValidator(fieldName, specialFieldValidators, parentTypeName, config.contextualValidators); if (validatorInfo.validator) { const description = getValidatorDescription(validatorInfo.validator); let validationText = `Will be validated as ${description}`; // Add context info if (validatorInfo.source.startsWith('contextual')) { validationText = `Matches ${validatorInfo.source} - ${validationText}`; } else if (validatorInfo.source.startsWith('field pattern')) { validationText = `Matches ${validatorInfo.source} - ${validationText}`; } // Add validation info to the hover text const newDisplayParts = [...(original.displayParts || [])]; newDisplayParts.push({ text: '\n\n', kind: 'lineBreak' }, { text: '(type-compiler)', kind: 'label' }, { text: ' ', kind: 'space' }, { text: validationText, kind: 'text' }, { text: '\n', kind: 'lineBreak' }, { text: validatorInfo.validator, kind: 'text' }); return { ...original, displayParts: newDisplayParts, documentation: original.documentation || [] }; } } return original; }; /** * Helper function to find the node at a specific position */ function findNodeAtPosition(sourceFile, position) { function find(node) { if (position >= node.getStart() && position < node.getEnd()) { return typescript.forEachChild(node, find) || node; } return undefined; } return find(sourceFile); } /** * Add completion entries for field names that would trigger validation */ proxy.getCompletionsAtPosition = (fileName, position, options) => { const original = ls.getCompletionsAtPosition(fileName, position, options); if (!original) return original; // Get source file const program = ls.getProgram(); if (!program) return original; const sourceFile = program.getSourceFile(fileName); if (!sourceFile) return original; // Check if we're in an interface or type context const node = findNodeAtPosition(sourceFile, position); if (!node) return original; const parent = node.parent; const isInInterfaceOrType = parent && (parent.kind === ts.SyntaxKind.InterfaceDeclaration || parent.kind === ts.SyntaxKind.TypeLiteral); if (isInInterfaceOrType) { // Add suggestions for common field names with special validators const completions = [...original.entries]; // Get the current field name prefix being typed const currentPrefix = node.getText() || ''; // Extract validation patterns (exact matches and patterns) Object.entries(specialFieldValidators).forEach(([pattern, validatorConfig]) => { // Handle exact matches if (typeof validatorConfig === 'string' && !pattern.startsWith('^') && !pattern.includes('(')) { const description = getValidatorDescription(validatorConfig); // Only add completion if it matches the current prefix (if any) if (!currentPrefix || pattern.startsWith(currentPrefix)) { completions.push({ name: pattern, kind: typescript.ScriptElementKind.memberVariableElement, kindModifiers: typescript.ScriptElementKindModifier.none, sortText: '0-' + pattern, // Sort at the top insertText: pattern, isSnippet: true, labelDetails: { description: `Field with ${description} validation` } }); } } // Handle pattern-based validators - properly type check else if (typeof validatorConfig === 'object' && 'pattern' in validatorConfig && validatorConfig.pattern) { try { // Generate example field names based on patterns const patternSuggestions = generateFieldSuggestionsFromPattern(pattern, validatorConfig.validator, currentPrefix); // Add each generated suggestion to completions patternSuggestions.forEach(suggestion => { const description = getValidatorDescription(validatorConfig.validator); completions.push({ name: suggestion.name, kind: typescript.ScriptElementKind.memberVariableElement, kindModifiers: typescript.ScriptElementKindModifier.none, sortText: '1-' + suggestion.name, // Sort after exact matches insertText: suggestion.name, isSnippet: true, labelDetails: { description: `Matches pattern ${pattern} - ${description}` } }); }); } catch (error) { // Invalid regex pattern, skip console.warn(`Invalid regex pattern in specialFieldValidators: ${pattern}`); } } }); return { ...original, entries: completions }; } return original; }; /** * Generate field name suggestions from a regex pattern */ function generateFieldSuggestionsFromPattern(pattern, validator, currentPrefix = '') { const suggestions = []; // Common field name templates based on validation types const validatorType = getValidatorDescription(validator); // Create example field names based on the pattern and validator type if (pattern.startsWith('^') && pattern.endsWith('$')) { // Exact pattern (^something$) const exactName = pattern.slice(1, -1); if (!currentPrefix || exactName.startsWith(currentPrefix)) { suggestions.push({ name: exactName, pattern }); } } else if (pattern.startsWith('^')) { // Starts with pattern (^prefix) const prefix = pattern.slice(1); // Generate common field names based on validator type const examples = generateExamplesForValidatorType(prefix, validatorType); examples.forEach(example => { if (!currentPrefix || example.startsWith(currentPrefix)) { suggestions.push({ name: example, pattern }); } }); } else if (pattern.endsWith('$')) { // Ends with pattern (suffix$) const suffix = pattern.slice(0, -1); // Generate common field names based on validator type const examples = generateExamplesForValidatorType('', validatorType, suffix); examples.forEach(example => { if (!currentPrefix || example.startsWith(currentPrefix)) { suggestions.push({ name: example, pattern }); } }); } else if (pattern.includes('.*')) { // Contains wildcard pattern (pre.*post) const [prefix, suffix] = pattern.split('.*'); // Generate common field names based on validator type const examples = generateExamplesForValidatorType(prefix, validatorType, suffix); examples.forEach(example => { if (!currentPrefix || example.startsWith(currentPrefix)) { suggestions.push({ name: example, pattern }); } }); } return suggestions; } /** * Generate example field names based on validator type */ function generateExamplesForValidatorType(prefix = '', validatorType, suffix = '') { const examples = []; switch (validatorType) { case 'email address': examples.push(`${prefix}email${suffix}`, `${prefix}userEmail${suffix}`, `${prefix}contactEmail${suffix}`, `${prefix}primaryEmail${suffix}`); break; case 'URL': examples.push(`${prefix}url${suffix}`, `${prefix}website${suffix}`, `${prefix}profileUrl${suffix}`, `${prefix}homepageUrl${suffix}`); break; case 'UUID': examples.push(`${prefix}id${suffix}`, `${prefix}uuid${suffix}`, `${prefix}userId${suffix}`, `${prefix}recordId${suffix}`); break; case 'date': examples.push(`${prefix}date${suffix}`, `${prefix}birthDate${suffix}`, `${prefix}createdAt${suffix}`, `${prefix}lastModified${suffix}`); break; case 'number within range': examples.push(`${prefix}age${suffix}`, `${prefix}score${suffix}`, `${prefix}rating${suffix}`, `${prefix}percentage${suffix}`); break; case 'matches regex pattern': examples.push(`${prefix}phoneNumber${suffix}`, `${prefix}postalCode${suffix}`, `${prefix}customFormat${suffix}`); break; case 'IP address': examples.push(`${prefix}ip${suffix}`, `${prefix}ipAddress${suffix}`, `${prefix}serverIp${suffix}`); break; default: examples.push(`${prefix}value${suffix}`, `${prefix}customField${suffix}`); } return examples; } /** * Add diagnostics for fields that will have special validation */ proxy.getSemanticDiagnostics = (fileName) => { const original = ls.getSemanticDiagnostics(fileName); const diagnostics = [...original]; const program = ls.getProgram(); if (!program) return original; const sourceFile = program.getSourceFile(fileName); if (!sourceFile) return original; // Find all interface and type declarations typescript.forEachChild(sourceFile, node => { let parentTypeName; if (ts.isInterfaceDeclaration(node) && node.name) { parentTypeName = node.name.getText(); } else if (ts.isTypeAliasDeclaration(node) && node.name) { parentTypeName = node.name.getText(); } else if (ts.isTypeLiteralNode(node)) { parentTypeName = 'AnonymousType'; } if (parentTypeName) { // For each property in the interface/type typescript.forEachChild(node, property => { if (isPropertyDeclaration(property) && property.name) { const fieldName = property.name.getText(); const validatorInfo = getMatchingValidator(fieldName, specialFieldValidators, parentTypeName, config.contextualValidators); if (validatorInfo.validator) { const description = getValidatorDescription(validatorInfo.validator); let messageText = `Field "${fieldName}" will be validated as ${description}`; // Add context info if (validatorInfo.source.startsWith('contextual')) { messageText = `${messageText} (from ${validatorInfo.source})`; } else if (validatorInfo.source.startsWith('field pattern')) { messageText = `${messageText} (matches ${validatorInfo.pattern})`; } // Add an informational diagnostic diagnostics.push({ category: typescript.DiagnosticCategory.Message, code: 9000, // Custom code for our plugin source: 'type-compiler', messageText, file: sourceFile, start: property.name.getStart(), length: property.name.getWidth() }); } } }); } }); return diagnostics; }; // Proxy all other methods for (const k of Object.keys(ls)) { if (!(k in proxy)) { proxy[k] = function () { return ls[k].apply(ls, arguments); }; } } return proxy; } return { create }; } module.exports = init;