UNPKG

linted-paths

Version:

Type-safe file system operations with compile-time path validation

171 lines 6.08 kB
import * as ts from 'typescript'; import path from 'path'; import fs from 'fs'; import { findProjectRoot } from './utils/project-root.js'; export class PathLinter { constructor(program, options = {}) { this.program = program; this.checker = program.getTypeChecker(); this.projectRoot = findProjectRoot(); this.options = { severity: 'error', ...options }; } /** * Runs the linter on all source files */ run() { const diagnostics = []; for (const sourceFile of this.program.getSourceFiles()) { if (!sourceFile.isDeclarationFile) { diagnostics.push(...this.checkFile(sourceFile)); } } return diagnostics; } /** * Checks a single source file for path validation issues */ checkFile(sourceFile) { const diagnostics = []; const visit = (node) => { if (ts.isVariableDeclaration(node) && node.initializer) { const diagnostic = this.checkVariableDeclaration(node); if (diagnostic) { diagnostics.push(diagnostic); } } ts.forEachChild(node, visit); }; visit(sourceFile); return diagnostics; } /** * Checks if a variable declaration has path validation issues */ checkVariableDeclaration(node) { if (!ts.isStringLiteral(node.initializer)) { return null; } // Check if the variable has a type annotation if (!node.type) { return null; } // Check if the type annotation is one of our path types if (!this.isPathType(node.type)) { return null; } const pathValue = node.initializer.text; if (!this.isValidPath(pathValue)) { return { category: this.getSeverityCategory(), code: 1001, messageText: `Invalid path: "${pathValue}" does not exist or is not accessible`, file: node.getSourceFile(), start: node.initializer.getStart(), length: node.initializer.getWidth(), }; } return null; } /** * Checks if a type is one of our path type aliases using AST */ isPathType(typeNode) { if (!ts.isTypeReferenceNode(typeNode)) { return false; } // Use AST to get the type name instead of getText() const typeName = this.getTypeNameFromAST(typeNode.typeName); return typeName === 'FilePathStr' || typeName === 'FolderPathStr' || typeName === 'AnyPathStr'; } /** * Extracts type name from AST nodes */ getTypeNameFromAST(typeName) { if (ts.isIdentifier(typeName)) { return typeName.text; } else if (ts.isQualifiedName(typeName)) { // For qualified names like 'linted-paths.FilePathStr', get the last part return this.getTypeNameFromAST(typeName.right); } return ''; } /** * Validates if a path is valid and exists using proper path resolution */ isValidPath(pathValue) { if (!pathValue || typeof pathValue !== 'string') { return false; } // Check for invalid characters using regex (this is appropriate for string validation) const invalidChars = /[<>:"|?*\x00-\x1f]/; if (invalidChars.test(pathValue)) { return false; } // Resolve the path properly using Node.js path utilities let fullPath; if (path.isAbsolute(pathValue)) { fullPath = pathValue; } else { fullPath = path.resolve(this.projectRoot, pathValue); } // Use path.relative to check if the resolved path is within project root const relativePath = path.relative(this.projectRoot, fullPath); if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { return false; } // Check if path exists using file system try { return fs.existsSync(fullPath); } catch { return false; } } /** * Gets the TypeScript diagnostic category based on severity */ getSeverityCategory() { switch (this.options.severity) { case 'warn': return ts.DiagnosticCategory.Warning; case 'off': return ts.DiagnosticCategory.Suggestion; case 'error': default: return ts.DiagnosticCategory.Error; } } } /** * Creates a TypeScript compiler plugin for path validation */ export function createPathLinterPlugin(options = {}) { return { create(info) { const linter = new PathLinter(info.languageService.getProgram(), options); // Create a proxy that extends the original language service return new Proxy(info.languageService, { get(target, prop) { if (prop === 'getSemanticDiagnostics') { return (fileName) => { // Get original diagnostics const originalDiagnostics = target.getSemanticDiagnostics(fileName); // Get our custom diagnostics const sourceFile = info.languageService.getProgram().getSourceFile(fileName); if (!sourceFile) return originalDiagnostics; const customDiagnostics = linter.checkFile(sourceFile); // Combine diagnostics return [...originalDiagnostics, ...customDiagnostics]; }; } // Return original property using proper typing return Reflect.get(target, prop); }, }); }, }; } //# sourceMappingURL=linter.js.map