لغة TypeScript

واجهة برمجة مترجم تايب سكريبت

42 دقيقة الدرس 25 من 40

واجهة برمجة مترجم تايب سكريبت

توفر واجهة برمجة مترجم تايب سكريبت وصولاً برمجياً إلى مترجم تايب سكريبت، مما يسمح لك بتحليل وتحويل وتوليد كود تايب سكريبت. تمكنك هذه الواجهة القوية من بناء محولات كود مخصصة، وأدوات تدقيق، ومولدات كود، وأدوات تطوير. في هذا الدرس، سنستكشف شجرة البناء المجردة (AST)، وننشئ محولات مخصصة، ونولد كوداً، ونجري فحص الأنواع برمجياً.

فهم AST في تايب سكريبت

شجرة البناء المجردة (AST) هي تمثيل شجري لكود المصدر الخاص بك. يمكن تمثيل كل جزء من كود تايب سكريبت كعقد في هذه الشجرة:

// تثبيت TypeScript كتبعية npm install typescript // basic-ast.ts import * as ts from 'typescript'; // تحليل كود المصدر إلى AST const sourceCode = \` const greeting: string = "مرحباً بالعالم!"; function add(a: number, b: number): number { return a + b; } \`; const sourceFile = ts.createSourceFile( 'example.ts', sourceCode, ts.ScriptTarget.Latest, true ); // اجتياز AST function visit(node: ts.Node, depth: number = 0): void { const indent = ' '.repeat(depth); console.log(\`${indent}${ts.SyntaxKind[node.kind]}\`); ts.forEachChild(node, (child) => visit(child, depth + 1)); } console.log('بنية AST:'); visit(sourceFile); // الإخراج: // SourceFile // VariableStatement // VariableDeclarationList // VariableDeclaration // Identifier // StringKeyword // StringLiteral // FunctionDeclaration // Identifier // Parameter // Identifier // NumberKeyword // Parameter // Identifier // NumberKeyword // NumberKeyword // Block // ReturnStatement // BinaryExpression // Identifier // PlusToken // Identifier
ملاحظة: يمثل AST كودك كشجرة هرمية من العقد. كل عقدة لديها خاصية kind تحدد نوع عنصر البناء الذي تمثله.

البحث عن العقد وتصفيتها

يمكنك البحث في AST عن أنواع محددة من العقد:

import * as ts from 'typescript'; // مساعد للعثور على العقد حسب النوع function findNodes<T extends ts.Node>( node: ts.Node, kind: ts.SyntaxKind ): T[] { const results: T[] = []; function visit(node: ts.Node): void { if (node.kind === kind) { results.push(node as T); } ts.forEachChild(node, visit); } visit(node); return results; } const sourceCode = \` const x = 10; let y = 20; var z = 30; function foo() {} const bar = () => {}; \`; const sourceFile = ts.createSourceFile( 'example.ts', sourceCode, ts.ScriptTarget.Latest, true ); // العثور على جميع إعلانات المتغيرات const variables = findNodes<ts.VariableDeclaration>( sourceFile, ts.SyntaxKind.VariableDeclaration ); console.log('أسماء المتغيرات:'); variables.forEach(v => { if (ts.isIdentifier(v.name)) { console.log(\` ${v.name.text}\`); } }); // العثور على جميع إعلانات الدوال const functions = findNodes<ts.FunctionDeclaration>( sourceFile, ts.SyntaxKind.FunctionDeclaration ); console.log('\nأسماء الدوال:'); functions.forEach(f => { if (f.name) { console.log(\` ${f.name.text}\`); } }); // التصفية المتقدمة: العثور على جميع إعلانات const function findConstDeclarations(node: ts.Node): ts.VariableDeclaration[] { const results: ts.VariableDeclaration[] = []; function visit(node: ts.Node): void { if (ts.isVariableStatement(node)) { const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0; if (isConst) { node.declarationList.declarations.forEach(decl => { results.push(decl); }); } } ts.forEachChild(node, visit); } visit(node); return results; } const constDeclarations = findConstDeclarations(sourceFile); console.log('\nإعلانات Const:'); constDeclarations.forEach(decl => { if (ts.isIdentifier(decl.name)) { console.log(\` ${decl.name.text}\`); } });

إنشاء محولات مخصصة

تسمح لك المحولات بتعديل AST قبل توليد الكود. هذا مفيد لتحسين الكود أو إضافة ميزات أو فرض أنماط:

import * as ts from 'typescript'; // تحويل جميع استدعاءات console.log إلى console.debug function createConsoleTransformer(): ts.TransformerFactory<ts.SourceFile> { return (context) => { return (sourceFile) => { const visitor = (node: ts.Node): ts.Node => { // التحقق من أن هذا console.log if ( ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === 'console' && node.expression.name.text === 'log' ) { // استبدال 'log' بـ 'debug' return ts.factory.updateCallExpression( node, ts.factory.createPropertyAccessExpression( node.expression.expression, 'debug' ), node.typeArguments, node.arguments ); } return ts.visitEachChild(node, visitor, context); }; return ts.visitNode(sourceFile, visitor); }; }; } // تطبيق المحول const sourceCode = \` console.log("مرحباً"); console.log("بالعالم"); console.error("خطأ"); \`; const sourceFile = ts.createSourceFile( 'example.ts', sourceCode, ts.ScriptTarget.Latest, true ); const result = ts.transform(sourceFile, [createConsoleTransformer()]); const printer = ts.createPrinter(); const transformedCode = printer.printFile(result.transformed[0]); console.log('الكود المحول:'); console.log(transformedCode); // الإخراج: // console.debug("مرحباً"); // console.debug("بالعالم"); // console.error("خطأ"); result.dispose(); // محول أكثر تقدماً: إضافة تسجيل لجميع الدوال function createFunctionLoggerTransformer(): ts.TransformerFactory<ts.SourceFile> { return (context) => { return (sourceFile) => { const visitor = (node: ts.Node): ts.Node => { if (ts.isFunctionDeclaration(node) && node.body) { const functionName = node.name?.text || 'مجهولة'; // إنشاء بيان console.log const logStatement = ts.factory.createExpressionStatement( ts.factory.createCallExpression( ts.factory.createPropertyAccessExpression( ts.factory.createIdentifier('console'), 'log' ), undefined, [ts.factory.createStringLiteral(\`دخول الدالة: ${functionName}\`)] ) ); // إضافة بيان السجل في بداية الدالة const newBody = ts.factory.updateBlock(node.body, [ logStatement, ...node.body.statements ]); return ts.factory.updateFunctionDeclaration( node, node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, newBody ); } return ts.visitEachChild(node, visitor, context); }; return ts.visitNode(sourceFile, visitor); }; }; }
نصيحة: استخدم واجهة برمجة ts.factory لإنشاء عقد AST جديدة. هذا يضمن أن العقد منظمة بشكل صحيح ومتوافقة مع مترجم تايب سكريبت.

توليد الكود

يمكنك توليد كود تايب سكريبت برمجياً باستخدام واجهة برمجة المصنع:

import * as ts from 'typescript'; // توليد فئة بسيطة function generateUserClass(): ts.ClassDeclaration { // إنشاء الخصائص const idProperty = ts.factory.createPropertyDeclaration( [ts.factory.createModifier(ts.SyntaxKind.PublicKeyword)], 'id', undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), undefined ); const nameProperty = ts.factory.createPropertyDeclaration( [ts.factory.createModifier(ts.SyntaxKind.PublicKeyword)], 'name', undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), undefined ); // إنشاء البناء const constructor = ts.factory.createConstructorDeclaration( undefined, [ ts.factory.createParameterDeclaration( undefined, undefined, 'id', undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword) ), ts.factory.createParameterDeclaration( undefined, undefined, 'name', undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword) ) ], ts.factory.createBlock([ ts.factory.createExpressionStatement( ts.factory.createBinaryExpression( ts.factory.createPropertyAccessExpression( ts.factory.createThis(), 'id' ), ts.SyntaxKind.EqualsToken, ts.factory.createIdentifier('id') ) ), ts.factory.createExpressionStatement( ts.factory.createBinaryExpression( ts.factory.createPropertyAccessExpression( ts.factory.createThis(), 'name' ), ts.SyntaxKind.EqualsToken, ts.factory.createIdentifier('name') ) ) ]) ); // إنشاء الطريقة const greetMethod = ts.factory.createMethodDeclaration( [ts.factory.createModifier(ts.SyntaxKind.PublicKeyword)], undefined, 'greet', undefined, undefined, [], ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), ts.factory.createBlock([ ts.factory.createReturnStatement( ts.factory.createTemplateExpression( ts.factory.createTemplateHead('مرحباً، اسمي '), [ ts.factory.createTemplateSpan( ts.factory.createPropertyAccessExpression( ts.factory.createThis(), 'name' ), ts.factory.createTemplateTail('') ) ] ) ) ]) ); // إنشاء الفئة return ts.factory.createClassDeclaration( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'User', undefined, undefined, [idProperty, nameProperty, constructor, greetMethod] ); } // طباعة الكود المولد const userClass = generateUserClass(); const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); const sourceFile = ts.createSourceFile( 'generated.ts', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS ); const result = printer.printNode( ts.EmitHint.Unspecified, userClass, sourceFile ); console.log('الفئة المولدة:'); console.log(result); // الإخراج: // export class User { // public id: number; // public name: string; // constructor(id: number, name: string) { // this.id = id; // this.name = name; // } // public greet(): string { // return \`مرحباً، اسمي ${this.name}\`; // } // }

فحص الأنواع برمجياً

تسمح لك واجهة برمجة المترجم بإجراء فحص الأنواع على الكود دون إصدار ملفات:

import * as ts from 'typescript'; import * as path from 'path'; // إنشاء برنامج لفحص الأنواع function createProgram(fileNames: string[], options: ts.CompilerOptions): ts.Program { const host = ts.createCompilerHost(options); return ts.createProgram(fileNames, options, host); } // فحص نوع ملف function typeCheck(fileName: string): void { const options: ts.CompilerOptions = { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS, strict: true, noEmit: true }; const program = createProgram([fileName], options); const diagnostics = ts.getPreEmitDiagnostics(program); if (diagnostics.length === 0) { console.log('✓ لم يتم العثور على أخطاء في الأنواع'); return; } console.log(\`✗ تم العثور على ${diagnostics.length} أخطاء في الأنواع:\`); diagnostics.forEach(diagnostic => { if (diagnostic.file) { const { line, character } = ts.getLineAndCharacterOfPosition( diagnostic.file, diagnostic.start! ); const message = ts.flattenDiagnosticMessageText( diagnostic.messageText, '\n' ); console.log( \` ${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}\` ); } else { console.log( \` ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}\` ); } }); } // فحص النوع مع معلومات النوع المخصصة function analyzeTypes(sourceCode: string): void { const sourceFile = ts.createSourceFile( 'temp.ts', sourceCode, ts.ScriptTarget.Latest, true ); const options: ts.CompilerOptions = { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS }; const host = ts.createCompilerHost(options); const originalGetSourceFile = host.getSourceFile; host.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => { if (fileName === 'temp.ts') { return sourceFile; } return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile); }; const program = ts.createProgram(['temp.ts'], options, host); const checker = program.getTypeChecker(); function visit(node: ts.Node): void { if (ts.isVariableDeclaration(node) && node.initializer) { const type = checker.getTypeAtLocation(node.initializer); const typeName = checker.typeToString(type); if (ts.isIdentifier(node.name)) { console.log(\`المتغير '${node.name.text}' له النوع: ${typeName}\`); } } ts.forEachChild(node, visit); } visit(sourceFile); } // مثال على الاستخدام const code = \` const name = "أحمد"; const age = 30; const isActive = true; const items = [1, 2, 3]; const user = { name: "فاطمة", age: 25 }; \`; console.log('تحليل الأنواع:'); analyzeTypes(code); // الإخراج: // المتغير 'name' له النوع: "أحمد" // المتغير 'age' له النوع: 30 // المتغير 'isActive' له النوع: true // المتغير 'items' له النوع: number[] // المتغير 'user' له النوع: { name: string; age: number; }
ملاحظة: TypeChecker هو الجزء الأكثر قوة من واجهة برمجة المترجم. يوفر الوصول إلى جميع معلومات الأنواع ويمكنه الإجابة على أسئلة حول الأنواع والرموز وعلاقاتها.

بناء أداة تدقيق بسيطة

دمج اجتياز AST وفحص الأنواع لبناء قواعد تدقيق مخصصة:

import * as ts from 'typescript'; interface LintError { line: number; column: number; message: string; } // قاعدة: عدم السماح بنوع 'any' function checkForAnyType(sourceFile: ts.SourceFile): LintError[] { const errors: LintError[] = []; function visit(node: ts.Node): void { // التحقق من تعليق نوع 'any' الصريح if (node.kind === ts.SyntaxKind.AnyKeyword) { const { line, character } = sourceFile.getLineAndCharacterOfPosition( node.getStart() ); errors.push({ line: line + 1, column: character + 1, message: 'استخدام نوع \'any\' غير مسموح به' }); } ts.forEachChild(node, visit); } visit(sourceFile); return errors; } // قاعدة: طلب أنواع إرجاع صريحة للدوال function checkFunctionReturnTypes(sourceFile: ts.SourceFile): LintError[] { const errors: LintError[] = []; function visit(node: ts.Node): void { if ( (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) && !node.type ) { const { line, character } = sourceFile.getLineAndCharacterOfPosition( node.getStart() ); const name = node.name ? node.name.getText() : 'مجهولة'; errors.push({ line: line + 1, column: character + 1, message: \`الدالة '${name}' تفتقد تعليق نوع الإرجاع' }); } ts.forEachChild(node, visit); } visit(sourceFile); return errors; } // تشغيل جميع قواعد التدقيق function lint(sourceCode: string): void { const sourceFile = ts.createSourceFile( 'test.ts', sourceCode, ts.ScriptTarget.Latest, true ); const anyTypeErrors = checkForAnyType(sourceFile); const returnTypeErrors = checkFunctionReturnTypes(sourceFile); const allErrors = [...anyTypeErrors, ...returnTypeErrors]; if (allErrors.length === 0) { console.log('✓ لم يتم العثور على أخطاء تدقيق'); return; } console.log(\`✗ تم العثور على ${allErrors.length} أخطاء تدقيق:\`); allErrors.forEach(error => { console.log(\` السطر ${error.line}:${error.column} - ${error.message}\`); }); } // اختبار أداة التدقيق const testCode = \` function greet(name: any) { return "مرحباً " + name; } function calculate(a: number, b: number) { return a + b; } \`; lint(testCode);
تحذير: العمل مع واجهة برمجة المترجم يمكن أن يكون معقداً. راجع دائماً وثائق تايب سكريبت الرسمية وادرس الأدوات الموجودة (مثل ts-morph) التي تغلف واجهة برمجة المترجم بواجهات أسهل في الاستخدام.
تمرين:
  1. ابنِ محولاً يحول جميع دوال الأسهم إلى إعلانات دوال عادية.
  2. أنشئ مولد كود يأخذ مخطط JSON ويولد واجهات تايب سكريبت.
  3. نفذ قاعدة تدقيق مخصصة تفرض اصطلاحات التسمية (مثل يجب أن تبدأ الواجهات بـ 'I').
  4. ابنِ أداة تحلل قاعدة كود تايب سكريبت وتولد رسماً بيانياً للتبعيات يوضح أي الملفات تستورد أيها.
  5. أنشئ محولاً يضيف تلقائياً تعليقات JSDoc إلى جميع الدوال المصدرة بناءً على توقيعات الأنواع الخاصة بها.

الخلاصة

واجهة برمجة مترجم تايب سكريبت هي أداة قوية لبناء أدوات تطوير مخصصة. من خلال فهم AST، يمكنك اجتياز وتحليل بنية الكود. تسمح لك المحولات المخصصة بتعديل الكود برمجياً. تمكن واجهة برمجة المصنع من توليد الكود، ويوفر فاحص الأنواع رؤى عميقة في معلومات الأنواع. مع هذه القدرات، يمكنك بناء أدوات تدقيق ومولدات كود وأدوات إعادة هيكلة والمزيد، مما يوسع تايب سكريبت لتناسب احتياجاتك المحددة.