Skip to content

Transformer Class #866

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ docs/
node_modules/
out/
raw/
.history
.history
lib/visitor/dist/lib/

lib/visitor/dist/src/
Empty file modified bin/asinit
100644 → 100755
Empty file.
52 changes: 41 additions & 11 deletions cli/asc.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,19 +212,31 @@ exports.main = function main(argv, options, callback) {
// Set up transforms
const transforms = [];
if (args.transform) {
args.transform.forEach(transform =>
transforms.push(
require(
path.isAbsolute(transform = transform.trim())
? transform
: path.join(process.cwd(), transform)
)
)
);
args.transform.forEach(transform => {
transforms.push(require(path.isAbsolute(transform = transform.trim()) ?
transform : path.join(process.cwd(), transform)));
});
}
function applyTransform(name, ...args) {
transforms.forEach(transform => {
if (typeof transform[name] === "function") transform[name](...args);
try {
// Export transform function function
if (transform[name] && typeof transform[name] === "function") {
transform[name](...args);
} else if (transform.default) {
// Export default class which needs to be constructor
if (!isConstructor(transform.default)){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's works only for default exports which could cause to problems

throw new Error("default exported transformer must be a constructor");
}
const transformer = new transform.default(...args);
if (typeof transformer[name] !== "function") {
throw new Error("Transformer missing " + name + " method.");
}
transformer[name]();
}
} catch (e) {
callback(e);
}
});
}

Expand Down Expand Up @@ -426,7 +438,7 @@ exports.main = function main(argv, options, callback) {
}

// Call afterParse transform hook
applyTransform("afterParse", parser);
applyTransform("afterParse", parser, writeFile, readFile, baseDir, writeStdout, writeStderr);

// Parse additional files, if any
{
Expand Down Expand Up @@ -796,6 +808,20 @@ exports.main = function main(argv, options, callback) {
}
});
}

function writeStderr(contents) {
if (!writeStderr.used) {
stats.writeCount++;
writeStderr.used = true;
}
stats.writeTime += measure(() => {
if (typeof contents === "string") {
stderr.write(contents, { encoding: "utf8" });
} else {
stderr.write(contents);
}
});
}
}

/** Checks diagnostics emitted so far for errors. */
Expand Down Expand Up @@ -939,3 +965,7 @@ exports.tscOptions = {
types: [],
allowJs: false
};

function isConstructor(func) {
return (func && typeof func === "function" && func.prototype && func.prototype.constructor) === func;
Copy link
Member

@MaxGraey MaxGraey Oct 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be

function isConstructor(fn) {
  return typeof fn === "function" && !!fn.prototype && fn.prototype.constructor === fn;
}

Just check:

assert.equal(isConstructor(undefined), false);
assert.equal(isConstructor(null), false);

Currently all this true

}
3 changes: 2 additions & 1 deletion cli/transform.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

import { Parser } from "../src/parser";

declare function writeFile(name: string, contents: string | Uint8Array, baseDir: string): void;
export interface Transform {
/** Called when parsing is complete, before a program is instantiated from the AST. */
afterParse(parser: Parser): void;
afterParse(parser: Parser, writer?: typeof writeFile, baseDir?: string): void;
}
94 changes: 94 additions & 0 deletions lib/transformer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# AST Transformer

After parsing the files imported by the entry files, the Abstract Syntax Tree (AST) is made. With the `--transform` argument, the compiler will load any js files provided and if the exports include a Transformer class.

## Transformer Class

By extending the Transformer class your subclass gains access to the `Parser`, `writeFile` function, `baseDir` is where the project's root is located, and `stdout` and `stderr`

```ts
export abstract class Transformer {
constructor(
protected parser: Parser,
protected writeFile: FileWriter,
protected baseDir: string,
protected stdout: (data: string) => void,
protected stderr: (data: string) => void
) {}

get program(): Program {
return this.parser.program;
}

abstract afterParse(): void;
}
```

### Example ASTPrinter

Below is an example of visiting each source that is a user entry file and creating a string from visiting the AST.

```ts
/**
* Example of using a transformer to print the AST for each entry file
*/
export default class Printer extends Transformer {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to latest best of practice for JS/TS we should avoid default exports

afterParse(): void {
const files = this.parser.program.sources.filter(
_source => _source.sourceKind == SourceKind.USER_ENTRY
);
files.forEach(source => {
// Create a string of source rebuilt from the AST node `source`
const sourceText: string = ASTBuilder.build(source);
this.stdout(sourceText);
});
}
}
```

To try it out first you might need to build the transformer files:

```bash
npm run build
```

Then in the test directory build the sample assemblyscript file:

```bash
cd tests && npm run build
```

## Visitors

There is a base AST visitor class in `src/base.ts` which visits each node in the AST. If extended, overwritten methods visit methods will be called when traversing the tree.

For example, `src/examples/functions.ts` collects the names of function and method declarations.

```ts
class FunctionVisitor extends BaseVisitor {
funcsFound: string[] = [];
currentClass: string;

visitClassDeclaration(node: ClassDeclaration): void {
// Remember current class
this.currentClass = node.name.text;
// Important to pass call parent visitor if you want to visit child nodes
super.visitClassDeclaration(node);
// Note can't call `super.visit(node) because it will call this function again.
}

visitFunctionDeclaration(node: FunctionDeclaration): void {
this.funcsFound.push("Function: " + node.name.text);
}

visitMethodDeclaration(node: MethodDeclaration): void {
this.funcsFound.push("Method: " + this.currentClass + "." + node.name.text);
}

static visit(node: Node): string {
const visitor = new FunctionVisitor();
visitor.visit(node);
return visitor.funcsFound.join("\n");
}
}
```
1 change: 1 addition & 0 deletions lib/transformer/dist/ASTPrinter.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions lib/transformer/dist/FuncVisitor.js

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions lib/transformer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "as-transformer",
"description": "Tools for transforming the Abstract Syntax Tree (AST)",
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"build": "npm run webpack",
"webpack": "../../node_modules/.bin/webpack --config=./webpack.config.js",
"build:dev": "npm run webpack -- --mode=development",
"test": "cd tests && npm run test"
},
"author": "W",
"license": "MIT"
}
Loading