Skip to content

Commit

Permalink
#55 refactor for better readability
Browse files Browse the repository at this point in the history
  • Loading branch information
timostamm committed Dec 21, 2020
1 parent daa7b0f commit 259c800
Show file tree
Hide file tree
Showing 25 changed files with 1,070 additions and 851 deletions.
1 change: 1 addition & 0 deletions packages/plugin-framework/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from './typescript-method-from-text';
export * from './typescript-literal-from-value';
export * from './typescript-enum-builder';
export * from "./typescript-file";
export * from "./typescript-imports";
1 change: 1 addition & 0 deletions packages/plugin-framework/src/typescript-import-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {AnyTypeDescriptorProto} from "./descriptor-info";
import {TypescriptFile} from "./typescript-file";


/** @deprecated */
export class TypescriptImportManager {

private readonly file: GeneratedFile;
Expand Down
295 changes: 295 additions & 0 deletions packages/plugin-framework/src/typescript-imports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
import {assert} from "@protobuf-ts/runtime";
import * as ts from "typescript";
import * as path from "path";
import {SymbolTable} from "./symbol-table";
import {AnyTypeDescriptorProto} from "./descriptor-info";
import {TypescriptFile} from "./typescript-file";


export class TypeScriptImports {

private readonly symbols: SymbolTable;


constructor(symbols: SymbolTable) {
this.symbols = symbols;
}


/**
* Import {importName} from "importFrom";
*
* Automatically finds a free name if the
* `importName` would collide with another
* identifier.
*
* Returns imported name.
*/
name(source: TypescriptFile, importName: string, importFrom: string): string {
const blackListedNames = this.symbols.list(source).map(e => e.name);
return ensureNamedImportPresent(
source.getSourceFile(),
importName,
importFrom,
blackListedNames,
statementToAdd => source.addStatement(statementToAdd, true)
);
}


/**
* Import * as importAs from "importFrom";
*
* Returns name for `importAs`.
*/
namespace(source: TypescriptFile, importAs: string, importFrom: string): string {
return ensureNamespaceImportPresent(
source.getSourceFile(),
importAs,
importFrom,
statementToAdd => source.addStatement(statementToAdd, true)
);
}


/**
* Import a previously registered identifier for a message
* or other descriptor.
*
* Uses the symbol table to look for the type, adds an
* import statement if necessary and automatically finds a
* free name if the identifier would clash in this file.
*
* If you have multiple representations for a descriptor
* in your generated code, use `kind` to discriminate.
*/
type(source: TypescriptFile, descriptor: AnyTypeDescriptorProto, kind = 'default'): string {
const symbolReg = this.symbols.get(descriptor, kind);

// symbol in this file?
if (symbolReg.file === source) {
return symbolReg.name;
}

// symbol not in file
// add an import statement
const importPath = createRelativeImportPath(
source.getSourceFile().fileName,
symbolReg.file.getFilename()
);
const blackListedNames = this.symbols.list(source).map(e => e.name);
return ensureNamedImportPresent(
source.getSourceFile(),
symbolReg.name,
importPath,
blackListedNames,
statementToAdd => source.addStatement(statementToAdd, true)
);
}


}


/**
* Import * as asName from "importFrom";
*
* If the import is already present, just return the
* identifier.
*
* If the import is not present, create the import
* statement and call `addStatementFn`.
*
* Does *not* check for collisions.
*/
function ensureNamespaceImportPresent(
currentFile: ts.SourceFile,
asName: string,
importFrom: string,
addStatementFn: (statementToAdd: ts.ImportDeclaration) => void,
): string {
const
all = findNamespaceImports(currentFile),
match = all.find(ni => ni.as === asName && ni.from === importFrom);
if (match) {
return match.as;
}
const statementToAdd = createNamespaceImport(asName, importFrom);
addStatementFn(statementToAdd);
return asName;
}

/**
* import * as <asName> from "<importFrom>";
*/
function createNamespaceImport(asName: string, importFrom: string) {
return ts.createImportDeclaration(
undefined,
undefined,
ts.createImportClause(
undefined,
ts.createNamespaceImport(ts.createIdentifier(asName))
),
ts.createStringLiteral(importFrom)
);
}

/**
* import * as <as> from "<from>";
*/
function findNamespaceImports(sourceFile: ts.SourceFile): { as: string, from: string }[] {
let r: Array<{ as: string, from: string }> = [];
for (let s of sourceFile.statements) {
if (ts.isImportDeclaration(s) && s.importClause) {
let namedBindings = s.importClause.namedBindings;
if (namedBindings && ts.isNamespaceImport(namedBindings)) {
assert(ts.isStringLiteral(s.moduleSpecifier));
r.push({
as: namedBindings.name.escapedText.toString(),
from: s.moduleSpecifier.text
});
}
}
}
return r;
}

/**
* Import {importName} from "importFrom";
*
* If the import is already present, just return the
* identifier.
*
* If the import is not present, create the import
* statement and call `addStatementFn`.
*
* If the import name is taken by another named import
* or is in the list of blacklisted names, an
* alternative name is used:
*
* Import {importName as alternativeName} from "importFrom";
*
* Returns the imported name or the alternative name.
*/
function ensureNamedImportPresent(
currentFile: ts.SourceFile,
importName: string,
importFrom: string,
blacklistedNames: string[],
addStatementFn: (statementToAdd: ts.ImportDeclaration) => void,
escapeCharacter = '$'
): string {
const
all = findNamedImports(currentFile),
taken = all.map(ni => ni.as ?? ni.name).concat(blacklistedNames),
match = all.find(ni => ni.name === importName && ni.from === importFrom);
if (match) {
return match.as ?? match.name;
}
let as: string | undefined;
if (taken.includes(importName)) {
let i = 0;
as = importName;
while (taken.includes(as)) {
as = importName + escapeCharacter;
if (i++ > 0) {
as += i;
}
}
}
const statementToAdd = createNamedImport(importName, importFrom, as);
addStatementFn(statementToAdd);
return as ?? importName;
}

/**
* import {<name>} from '<from>';
* import {<name> as <as>} from '<from>';
*/
function createNamedImport(name: string, from: string, as?: string): ts.ImportDeclaration {
if (as) {
return ts.createImportDeclaration(
undefined,
undefined,
ts.createImportClause(
undefined,
ts.createNamedImports([ts.createImportSpecifier(
ts.createIdentifier(name),
ts.createIdentifier(as)
)]),
false
),
ts.createStringLiteral(from)
);
}
return ts.createImportDeclaration(
undefined,
undefined,
ts.createImportClause(
undefined,
ts.createNamedImports([
ts.createImportSpecifier(
undefined,
ts.createIdentifier(name)
)
])
),
ts.createStringLiteral(from)
);
}

/**
* import {<name>} from '<from>';
* import {<name> as <as>} from '<from>';
*/
function findNamedImports(sourceFile: ts.SourceFile): { name: string, as: string | undefined, from: string }[] {
let r: Array<{ name: string, as: string | undefined, from: string }> = [];
for (let s of sourceFile.statements) {
if (ts.isImportDeclaration(s) && s.importClause) {
let namedBindings = s.importClause.namedBindings;
if (namedBindings && ts.isNamedImports(namedBindings)) {
for (let importSpecifier of namedBindings.elements) {
assert(ts.isStringLiteral(s.moduleSpecifier));
if (importSpecifier.propertyName) {
r.push({
name: importSpecifier.propertyName.escapedText.toString(),
as: importSpecifier.name.escapedText.toString(),
from: s.moduleSpecifier.text
})
} else {
r.push({
name: importSpecifier.name.escapedText.toString(),
as: undefined,
from: s.moduleSpecifier.text
})
}
}
}
}
}
return r;
}

/**
* Create a relative path for an import statement like
* `import {Foo} from "./foo"`
*/
function createRelativeImportPath(currentPath: string, pathToImportFrom: string): string {
// create relative path to the file to import
let fromPath = path.relative(path.dirname(currentPath), pathToImportFrom);

// on windows, this may add backslash directory separators.
// we replace them with forward slash.
if (path.sep !== "/") {
fromPath = fromPath.split(path.sep).join("/");
}

// drop file extension
fromPath = fromPath.replace(/\.[a-z]+$/, '');

// make sure to start with './' to signal relative path to module resolution
if (!fromPath.startsWith('../') && !fromPath.startsWith('./')) {
fromPath = './' + fromPath;
}
return fromPath;
}
17 changes: 8 additions & 9 deletions packages/plugin/spec/protobufts-plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ describe('protobuftsPlugin', function () {
parameter: 'long_type_string',
// includeFiles: [
//
// 'google/protobuf/unittest_enormous_descriptor.proto',
// // 'google/protobuf/unittest_proto3_lite.proto',
// // 'google/protobuf/unittest_import.proto',
// // 'google/protobuf/unittest_import_public.proto',
//
// // 'google/protobuf/wrappers.proto',
//
// // 'msg-scalar.proto',
// // 'msg-proto3-optionals.proto',
// // 'google/protobuf/any.proto',
Expand All @@ -30,19 +29,19 @@ describe('protobuftsPlugin', function () {
// // 'google/protobuf/unittest_lazy_dependencies_enum.proto',
// // 'service-simple.proto',
// // 'service-example.proto',
// 'service-style-all.proto',
// // 'service-style-all.proto',
// // 'msg-proto3-optionals.proto',
// // 'google/rpc/status.proto',
// ]
});
let generatedFiles = plugin.generate(request);

// for (let f of generatedFiles) {
// console.log('-------------------------' + f.getFilename() + '-------------------------');
// console.log(f.getContent());
// console.log();
// console.log();
// }
for (let f of generatedFiles) {
// console.log('-------------------------' + f.getFilename() + '-------------------------');
// console.log(f.getContent());
// console.log();
// console.log();
}


describe('generates valid typescript for every fixture .proto', function () {
Expand Down
Loading

0 comments on commit 259c800

Please sign in to comment.