Using the TypeScript complier API

Keywords: TypeScript Javascript JSON npm

Using the TypeScript complier API

Original Address

Disclaimer

Keep in mind that this is still an incomplete API - we set its release version to 0.5 and things will change over time.After all, there are some imperfections in the first iteration.I hope the community will give more feedback to improve the API.To allow users to convert between future versions, we will record significant API changes for each new version.

Set up

First you need to install TypeScript using npm with a version higher than 1.6.

After installation, you need to link TypeScript in the project path.Otherwise, the link will be directly global.

npm install -g typescript
npm link typescript
 Copy Code

For some examples, you also need a node definition file.Run the following command to get the definition file:

npm install @types/node
 Copy Code

Once you've done that, you can try the following examples.

The compiler API has several main components:

  • Program is a TypeScript term for your entire application
  • CompilerHost is a user system API for reading files, checking directories, and case sensitivity.
  • SourceFiles represents the source files in the application and contains both text and TypeScript AST.

A small complier

This example is a basic editor that gets a list of files for TypeScript and compiles them into the appropriate JavaScript.

We can create a Program through createProgram - this creates a default ComplierHost that uses the file system to get the files.

import * as ts from "typescript"

function complier (fileNames: string[], options: ts.CompilerOptions): void {
  let program = ts.createProgram(fileNames, options)
  let emitResult = program.emit()

  let allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics)

  allDiagnostics.forEach(diagnostics => {
    if (diagnostics.file) {
      let { line, character } = diagnostics.file.getLineAndCharacterOfPosition(diagnostics.start!)
      let message = ts.flattenDiagnosticMessageText(diagnostics.messageText, '\n');
      console.log(`${diagnostics.file.fileName} (${line + 1}, ${character + 1}): ${message}`);
    } else {
      console.log(ts.flattenDiagnosticMessageText(diagnostics.messageText, '\n'))
    }
  })

  let exitCode = emitResult.emitSkipped ? 1 : 0;
  console.log(`Process exiting with code ${exitCode}.`);
  process.exit(exitCode)
}

complier(process.argv.slice(2), {
  noEmitOnError: true,
  noImplicitAny: true,
  target: ts.ScriptTarget.ES5,
  module: ts.ModuleKind.CommonJS
})
Copy Code

A simple transform function

It doesn't take much code to create a complier, but you might just want to get the appropriate JavaScript output given the TypeScript source file.Therefore, you can use ts.transplieModule to get a string => string code conversion from two lines of code.

import * as ts from "typescript";

const source = "let x: string = 'string'";

let result = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS } })

console.log(JSON.stringify(result));
Copy Code

Getting DTS from a JavaScript file

It will only run on TypeScript versions 3.7 and above.This example illustrates how to get a list of JavaScript files and display their generated d.ts files on the terminal.

import * as ts from 'typescript'

function compile(fileNames: string[], options: ts.CompilerOptions): void {
  // Create program with emit in memory
  const createdFiles = {}
  const host = ts.createCompilerHost(options)
  host.writeFile = (fileName: string, contents: string) => createdFiles[fileName] = contents;

  // Prepare and emit the d.ts file
  const program = ts.createProgram(fileNames, options, host);
  program.emit()

  // Traverse all input files
  fileNames.forEach(file => {
    console.log('### JavaScript\n');
    console.log(host.readFile(file));

    console.log('### Type Defination\n');
    const dts = file.replace('.js', '.d.ts')
    console.log(createdFiles[dts]);
  })
}

// Run complier
compile(process.argv.slice(2), {
  allowJs: true,
  declaration: true,
  emitDeclarationOnly: true,
})
Copy Code

Reprint portions of TypeScript files

This example will undo the Typescript subpart of the JavaScript source file, which is useful when you want your application's code to be the real source.For example, the export is displayed with JSDoc comments.

import * as ts from 'typescript'

/**
 * Print out a specific node from the source file
 * 
 * @param file a path to a file
 * @param identifiers top level identifiers available
 */
function extract(file: string, identifiers: string[]): void {
  // Create a Program that represents a project
  // Then take out its source file to parse its AST
  let program = ts.createProgram([file], { allowJs: true });
  const sourceFile = program.getSourceFile(file)

  // In order to print AST, we will use the printer of TypeScript
  const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });

  // To give constructive error messages, keep track of identifiers found and not found
  const unfoundNodes = [], foundNodes = [];

  // The root node of the AST that traverses the source file, possibly with a top-level identifier
  // You can use https://ts-ast-viewer.com/to expand this list to view the AST of the file
  // Then use the same pattern below
  ts.forEachChild(sourceFile, node => {
    let name = ''

    // This is an incomplete set of AST nodes
    if (ts.isFunctionDeclaration(node)) {
      name = node.name.text;
      // Hide method body when printing
      node.body = undefined;
    } else if (ts.isVariableStatement(node)) {
      name = node.declarationList.declarations[0].name.getText(sourceFile);
    } else if (ts.isInterfaceDeclaration(node)) {
      name = node.name.text;
    }

    const container = identifiers.includes(name) ? foundNodes : unfoundNodes;
    container.push([name, node])
  });

  // Either print the node found or provide a list of identifiers found
  if (!foundNodes.length) {
    console.log(`Could not found any of ${identifiers.join(',')} in ${file}, found: ${unfoundNodes.filter(f => f[0].map(f=>f[0]).join(''))}`);
    process.exitCode = 1;
  } else {
    foundNodes.map(f => {
      const [name , node] = f;
      console.log('###' + name + '\n');
      console.log(printer.printNode(ts.EmitHint.Unspecified, node, sourceFile)) + '\n';
    });
  }
}

// Run the extract function with the parameters of the script
Copy Code

Traverse AST with a small checker

The Node interface is the root interface of TypeScript AST.Usually we use the forEachChild function to recursively traverse trees.This includes visitor patterns and often provides more flexibility.

As an example of how to traverse a file AST, consider the smallest inspector that does the following:

  • Check that all loop statements are enclosed in curly brackets.
  • Check that all if/else statements are enclosed in curly brackets.
  • Is the strong symbol (===/!==) used to replace the loose one (== /!=).
import { readFileSync } from 'fs'
import * as ts from 'typescript'

export function delint (sourceFile: ts.SourceFile) {
  delintNode(sourceFile)

  function delintNode (node: ts.Node) {
    switch (node.kind) {
      case ts.SyntaxKind.ForStatement:
      case ts.SyntaxKind.ForInStatement:
      case ts.SyntaxKind.WhileStatement:
      case ts.SyntaxKind.DoStatement:
        if ((node as ts.IterationStatement).statement.kind !== ts.SyntaxKind.Block) {
          report(
            node,
            'A looping statement\'s contents should be wrapped in a block body.'
          )
        }
        break;
      case ts.SyntaxKind.IfStatement:
        const ifStatement = node as ts.IfStatement;
        if (ifStatement.thenStatement.kind !== ts.SyntaxKind.Block) {
          report(ifStatement.thenStatement, 'An if statement\'s contents should be wrapped in a block body.');
        }
        if (
          ifStatement.elseStatement &&
          ifStatement.elseStatement.kind !== ts.SyntaxKind.Block &&
          ifStatement.elseStatement.kind !== ts.SyntaxKind.IfStatement
        ) {
          report(
            ifStatement.elseStatement,
            'An else statement\'s contents should be wrapped in a block body.'
          );
        }
        break;
      case ts.SyntaxKind.BinaryExpression:
        const op = (node as ts.BinaryExpression).operatorToken.kind;
        if (op === ts.SyntaxKind.EqualsEqualsToken || op === ts.SyntaxKind.ExclamationEqualsToken) {
          report(node, 'Use \'==\' and \'!==\'.');
        }
        break;
    }
    ts.forEachChild(node, delintNode)
  }

  function report (node: ts.Node, message: string) {
    const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
    console.log(`${sourceFile.fileName} (${line + 1}, ${character + 1}): ${message}`);
  }
}

const fileNames = process.argv.slice(2);
fileNames.forEach(fileName => {
  // Parse a file
  const sourceFile = ts.createSourceFile(
    fileName,
    readFileSync(fileName).toString(),
    ts.ScriptTarget.ES2015,
    /* setParentNodes */ true
  );

  // delint it
  delint(sourceFile)
});
Copy Code

In this example, we don't need to create a type checker because we just want to traverse each source file.

All ts.SyntaxKind can Enumeration Type Found in.

Write Incremental Program Monitor

TypeScript 2.7 introduced two new APIs: one for creating "watcher" programs and providing a set of APIs that trigger refactoring; the other "builder" API that watcher can use.BuilderPrograms is a program instance that caches errors and issues them on previously compiled modules if the module or its dependencies are not updated in a cascade manner."Watcher" programs can use generator program instances to update only the results of affected files (such as errors and issues) during compilation.This can speed up large projects with many files.

This API is used internally in compiler to implement the--watch pattern, but it can also be used by other tools, such as:

import ts = require("typescript")

const formatHost: ts.FormatDiagnosticsHost = {
  getCanonicalFileName: path => path,
  getCurrentDirectory: ts.sys.getCurrentDirectory,
  getNewLine: () => ts.sys.newLine
};

function watchMain () {
  const configPath = ts.findConfigFile(
    /*searchPath*/ "./",
    ts.sys.fileExists,
    "tsconfig.json"
  );
  if (!configPath) {
    throw new Error("Could not find a valid 'tsconfig.json'.");
  }

  // TypeScript can create "policies" using several different programs:
  // * ts.createEmitAndSemanticDiagnosticsBuilderProgram
  // * ts.createSemanticDiagnosticsBuilderProgram
  // * ts.createAbstractBuilder

  // The first two API s produce the Generator program.
  // They use an incremental policy to only recheck and issue files whose contents may have changed
  // Or its dependencies may change, which may affect previous type checks and changes that issue results
  // The last API performs a full type check after each change.
  // The only difference between `createEmitAndSemanticDiagnosticsBuilderProgram` and `createSemanticDiagnosticsBuilderProgram` is that it does not emit
  // It is preferable to use `createSemanticDiagnosticsBuilderProgram` for pure type check scenarios or when another tool/process process process process process is issued
  const createProgram = ts.createSemanticDiagnosticsBuilderProgram;

  // Note that `createWatchCompilerHost` also has an overload that requires a set of root files.
  const host = ts.createWatchCompilerHost(
    configPath,
    {},
    ts.sys,
    createProgram,
    reportDiagnostic,
    reportWatchStatusChanged
  );

  // Technically, you can override any given hook function on the host, although you may not need to.
  // Note that we assume that `origCreateProgram'and `origPostProgramCreate' do not use `this'at all.
  const origCreateProgram = host.createProgram
  host.createProgram = (rootNames: ReadonlyArray<string>, options, host, oldProgram) => {
    console.log("** We're about to create the program! **");
    return origCreateProgram(rootNames, options, host, oldProgram);
  };

  const origPostProgramCreate = host.afterProgramCreate;
  host.afterProgramCreate = program => {
    console.log("** We finished making the program! **");
    origPostProgramCreate!(program);
  };

  // `createWatchProgram` Creates an initial program, monitors the file, and updates the program over time.
  ts.createWatchProgram(host);
}

function reportDiagnostic(diagnostic: ts.Diagnostic) {
  console.error("Error", diagnostic.code, ":", ts.flattenDiagnosticMessageText(diagnostic.messageText, formatHost.getNewLine()));
}

/**
 * Diagnostic information is printed every time a status change is monitored
 * This is mainly used for messages such as Start Compiling or Finish Compiling.
 */
function reportWatchStatusChanged (diagnostic: ts.Diagnostic) {
  console.info(ts.formatDiagnostic(diagnostic, formatHost));
}

watchMain();
Copy Code

Incremental build support using language services

Can be referred to Using the Language Service API Get more details.

The service layer provides an additional layer of utilities that can help simplify some complex scenarios.In the code snippet below, we will try to build an incremental build server that monitors a set of files and only updates the output of files that have changed.We'll create an object called LanguageService to do this.Similar to the program in the previous example.We need a Language ServiceHost.LanguageServiceHost does this through version, isOpen flag, and criptSnapshot.This version allows language services to track file changes.IsOpen tells the language service to save the AST in memory when using the file.ScriptSnapshot is an abstraction of text that allows language services to query for changes.

If you just want to implement the monitoring style, you can study the monitor program API above.

import * as fs from 'fs'
import * as ts from 'typescript'

function watch (rootFileNames: string[], options: ts.CompilerOptions) {
  const files: ts.MapLike<{ version: number }> = {};

  // Initialization File List
  rootFileNames.forEach(fileName => {
    files[fileName] = { version: 0 };
  });

  // Create a language service host to allow LS to communicate with the host
  const serviceHost: ts.LanguageServiceHost = {
    getScriptFileNames: () => rootFileNames,
    getScriptVersion: fileName => files[fileName] && files[fileName].version.toString(),
    getScriptSnapshot: fileName => {
      if (!fs.existsSync(fileName)) {
        return undefined
      }
      return ts.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString())
    },
    getCurrentDirectory: () => process.cwd(),
    getCompilationSettings: () => options,
    getDefaultLibFileName: options => ts.getDefaultLibFilePath(options),
    fileExists: ts.sys.fileExists,
    readFile: ts.sys.readFile,
    readDirectory: ts.sys.readDirectory
  };

  // Create Language Service File
  const services = ts.createLanguageService(serviceHost, ts.createDocumentRegistry());

  // Start listening
  rootFileNames.forEach(fileName => {
    // First, issue all files
    emitFile(fileName);

    // Add a script above to listen for the next change
    fs.watchFile(fileName, { persistent: true, interval: 250 }, (curr, prev) => {
      // Check timestamp
      if (+curr.mtime <= +prev.mtime) {
        return;
      }

      // Updating version indicates that the file has been modified
      files[fileName].version ++

      // Write changes to disk
      emitFile(fileName)
    })
  })

  function emitFile(fileName: string) {
    let output = services.getEmitOutput(fileName);

    if (!output.emitSkipped) {
      console.log(`Emitting ${fileName}`);
    } else {
      console.log(`Emitting ${fileName} failed`);
      logErrors(fileName)
    }

    output.outputFiles.forEach(o => {
      fs.writeFileSync(o.name, o.text, "utf8")
    })
  }

  function logErrors(fileName: string) {
    let allDiagnostics = services
      .getCompilerOptionsDiagnostics()
      .concat(services.getSyntacticDiagnostics(fileName))
      .concat(services.getSemanticDiagnostics(fileName));
    
    allDiagnostics.forEach(diagnostic => {
      let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
      if (diagnostic.file) {
        let { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
          diagnostic.start!
        );
        console.log(` Error ${diagnostic.file.fileName} (${line + 1}): ${message}`);
      } else {
        console.log(` Error: ${message} `);
      }
    })
  }
}

// Initialize the files that make up the program to be all.ts files in the current directory
const currentDirectoryFiles = fs.readdirSync(process.cwd()).filter(fileName => fileName.length >= 3 && fileName.substr(fileName.length - 3, 3) === '.ts');

// Start listening
watch(currentDirectoryFiles, { module: ts.ModuleKind.CommonJS })
Copy Code

Custom Module Resolution

By implementing the optional method, you can override the standard way a compiler parses a module: CompilerHost.resolvedModuleNames:

CompilerHost.resolveModuleNames(moduleNames: string[], containingFile: string): string[]

This method gives a list of module names in a file and expects to return an array of moduleNames.length in size, where each element stores either of the following:

  • Resolution of the corresponding name in the module Names array of the RelveModule instance with the non-empty property resolveFileName
  • Return undefined if module name cannot be resolved

You can call the standard module resolution process by calling resolveModuleName:

resolveModuleName(moduleName: string, containingFile: string, options: CompilerOptions, moduleResolutionHost: ModuleResolutionHost): ResolvedModuleNameWithFallbackLocations.

This function returns an object that stores the result of module resolution (the value of the resolvedModule property) and a list of file names considered candidates before making the current decision.

import * as ts from "typescript";
import * as path from "path";

function createCompilerHost(options: ts.CompilerOptions, moduleSearchLocations: string[]): ts.CompilerHost {
  return {
    getSourceFile,
    getDefaultLibFileName: () => "lib.d.ts",
    writeFile: (fileName, content) => ts.sys.writeFile(fileName, content),
    getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
    getDirectories: path => ts.sys.getDirectories(path),
    getCanonicalFileName: fileName =>
      ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
    getNewLine: () => ts.sys.newLine,
    useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
    fileExists,
    readFile,
    resolveModuleNames
  };

  function fileExists(fileName: string): boolean {
    return ts.sys.fileExists(fileName);
  }

  function readFile(fileName: string): string | undefined {
    return ts.sys.readFile(fileName);
  }

  function getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void) {
    const sourceText = ts.sys.readFile(fileName);
    return sourceText !== undefined
      ? ts.createSourceFile(fileName, sourceText, languageVersion)
      : undefined;
  }

  function resolveModuleNames(
    moduleNames: string[],
    containingFile: string
  ): ts.ResolvedModule[] {
    const resolvedModules: ts.ResolvedModule[] = [];
    for (const moduleName of moduleNames) {
      // try to use standard resolution
      let result = ts.resolveModuleName(moduleName, containingFile, options, {
        fileExists,
        readFile
      });
      if (result.resolvedModule) {
        resolvedModules.push(result.resolvedModule);
      } else {
        // check fallback locations, for simplicity assume that module at location
        // should be represented by '.d.ts' file
        for (const location of moduleSearchLocations) {
          const modulePath = path.join(location, moduleName + ".d.ts");
          if (fileExists(modulePath)) {
            resolvedModules.push({ resolvedFileName: modulePath });
          }
        }
      }
    }
    return resolvedModules;
  }
}

function compile(sourceFiles: string[], moduleSearchLocations: string[]): void {
  const options: ts.CompilerOptions = {
    module: ts.ModuleKind.AMD,
    target: ts.ScriptTarget.ES5
  };
  const host = createCompilerHost(options, moduleSearchLocations);
  const program = ts.createProgram(sourceFiles, options, host);

  /// do something with program...
}
Copy Code

Create and print TypeScript AST

TypeScript has factory functions and a print API that you can use in conjunction with.

  • Factory functions allow you to generate new tree nodes in the AST format of TypeScript.
  • The print API takes an existing tree structure (generated by a createSourceFile or factory function) and generates an output string.

Here is an example of using these two methods to generate a factorial function:

import ts = require("typescript");

function makeFactorialFunction() {
  const functionName = ts.createIdentifier("factorial");
  const paramName = ts.createIdentifier("n");
  const parameter = ts.createParameter(
    /*decorators*/ undefined,
    /*modifiers*/ undefined,
    /*dotDotDotToken*/ undefined,
    paramName
  );

  const condition = ts.createBinary(paramName, ts.SyntaxKind.LessThanEqualsToken, ts.createLiteral(1));
  const ifBody = ts.createBlock([ts.createReturn(ts.createLiteral(1))], /*multiline*/ true);

  const decrementedArg = ts.createBinary(paramName, ts.SyntaxKind.MinusToken, ts.createLiteral(1));
  const recurse = ts.createBinary(paramName, ts.SyntaxKind.AsteriskToken, ts.createCall(functionName, /*typeArgs*/ undefined, [decrementedArg]));
  const statements = [ts.createIf(condition, ifBody), ts.createReturn(recurse)];

  return ts.createFunctionDeclaration(
    /*decorators*/ undefined,
    /*modifiers*/ [ts.createToken(ts.SyntaxKind.ExportKeyword)],
    /*asteriskToken*/ undefined,
    functionName,
    /*typeParameters*/ undefined,
    [parameter],
    /*returnType*/ ts.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
    ts.createBlock(statements, /*multiline*/ true)
  );
}

const resultFile = ts.createSourceFile("someFileName.ts", "", ts.ScriptTarget.Latest, /*setParentNodes*/ false, ts.ScriptKind.TS);
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });

const result = printer.printNode(ts.EmitHint.Unspecified, makeFactorialFunction(), resultFile);
console.log(result);
Copy Code

Use Type Checker

In this example, we will iterate through AST and use a checker to serialize class information.We'll use a type checker to get symbols and type information, as well as JSDoc comments for the exported classes, their constructors, and their respective constructor parameters.

import * as ts from "typescript";
import * as fs from "fs";

interface DocEntry {
  name?: string;
  fileName?: string;
  documentation?: string;
  type?: string;
  constructors?: DocEntry[];
  parameters?: DocEntry[];
  returnType?: string;
}

/** Generate documentation for all classes in a set of .ts files */
function generateDocumentation(
  fileNames: string[],
  options: ts.CompilerOptions
): void {
  // Build a program using the set of root file names in fileNames
  let program = ts.createProgram(fileNames, options);

  // Get the checker, we will use it to find more about classes
  let checker = program.getTypeChecker();
  let output: DocEntry[] = [];

  // Visit every sourceFile in the program
  for (const sourceFile of program.getSourceFiles()) {
    if (!sourceFile.isDeclarationFile) {
      // Walk the tree to search for classes
      ts.forEachChild(sourceFile, visit);
    }
  }

  // print out the doc
  fs.writeFileSync("classes.json", JSON.stringify(output, undefined, 4));

  return;

  /** visit nodes finding exported classes */
  function visit(node: ts.Node) {
    // Only consider exported nodes
    if (!isNodeExported(node)) {
      return;
    }

    if (ts.isClassDeclaration(node) && node.name) {
      // This is a top level class, get its symbol
      let symbol = checker.getSymbolAtLocation(node.name);
      if (symbol) {
        output.push(serializeClass(symbol));
      }
      // No need to walk any further, class expressions/inner declarations
      // cannot be exported
    } else if (ts.isModuleDeclaration(node)) {
      // This is a namespace, visit its children
      ts.forEachChild(node, visit);
    }
  }

  /** Serialize a symbol into a json object */
  function serializeSymbol(symbol: ts.Symbol): DocEntry {
    return {
      name: symbol.getName(),
      documentation: ts.displayPartsToString(symbol.getDocumentationComment(checker)),
      type: checker.typeToString(
        checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration!)
      )
    };
  }

  /** Serialize a class symbol information */
  function serializeClass(symbol: ts.Symbol) {
    let details = serializeSymbol(symbol);

    // Get the construct signatures
    let constructorType = checker.getTypeOfSymbolAtLocation(
      symbol,
      symbol.valueDeclaration!
    );
    details.constructors = constructorType
      .getConstructSignatures()
      .map(serializeSignature);
    return details;
  }

  /** Serialize a signature (call or construct) */
  function serializeSignature(signature: ts.Signature) {
    return {
      parameters: signature.parameters.map(serializeSymbol),
      returnType: checker.typeToString(signature.getReturnType()),
      documentation: ts.displayPartsToString(signature.getDocumentationComment(checker))
    };
  }

  /** True if this is visible outside this file, false otherwise */
  function isNodeExported(node: ts.Node): boolean {
    return (
      (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0 ||
      (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
    );
  }
}

generateDocumentation(process.argv.slice(2), {
  target: ts.ScriptTarget.ES5,
  module: ts.ModuleKind.CommonJS
});
Copy Code

Use this script:

tsc docGenerator.ts --m commonjs
node docGenerator.js test.ts
Copy Code

Enter the following example:

/**
 * Documentation for C
 */
class C {
    /**
     * constructor documentation
     * @param a my parameter documentation
     * @param b another parameter documentation
     */
    constructor(a: string, b: C) { }
}
Copy Code

We can get this output:

[
    {
        "name": "C",
        "documentation": "Documentation for C ",
        "type": "typeof C",
        "constructors": [
            {
                "parameters": [
                    {
                        "name": "a",
                        "documentation": "my parameter documentation",
                        "type": "string"
                    },
                    {
                        "name": "b",
                        "documentation": "another parameter documentation",
                        "type": "C"
                    }
                ],
                "returnType": "C",
                "documentation": "constructor documentation"
            }
        ]
    }
]
Copy Code

Posted by tzicha on Sun, 17 May 2020 17:47:43 -0700