Skip to content

Instantly share code, notes, and snippets.

@zenparsing
Last active July 1, 2019 15:05
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zenparsing/75381b450adb6792b892eeb15822b4d4 to your computer and use it in GitHub Desktop.
Save zenparsing/75381b450adb6792b892eeb15822b4d4 to your computer and use it in GitHub Desktop.
Symbol names

Symbol Literals

The form @identifierName is a symbol literal. Within any single module or script, two identitical symbol literals refer to the same symbol. Two identical symbol literals in separate modules or scripts refer to different symbols. A symbol literal may appear as a propery name or as a primary expression.

When a symbol literal appears as a primary expression, it is shorthand for this.@identifierName.

When a symbol literal appears as a property name, the corresponding symbol is used as the property key during evaluation.

import { AST, resolveScopes } from 'esparse';
export class Path {
constructor(node, parent = null, location = null) {
@node = node;
@location = location;
@parent = parent;
@scopeInfo = parent ? parent.@scopeInfo : null;
}
get node() {
return @node;
}
get parent() {
return @parent;
}
get parentNode() {
return @parent ? @parent.@node : null;
}
forEachChild(fn) {
if (!@node) {
return;
}
let paths = [];
AST.forEachChild(@node, (child, key, index) => {
let path = new Path(child, this, { key, index });
paths.push(path);
fn(path);
});
for (let path of paths) {
path.applyChanges();
}
}
applyChanges() {
let list = @changeList;
@changeList = [];
for (let record of list) {
if (!@node) {
break;
}
record.apply();
}
}
removeNode() {
@changeList.push(new ChangeRecord(this, 'replaceNode', [null]));
}
replaceNode(newNode) {
@changeList.push(new ChangeRecord(this, 'replaceNode', [newNode]));
}
insertNodesBefore(...nodes) {
@changeList.push(new ChangeRecord(this, 'insertNodesBefore', nodes));
}
insertNodesAfter(...nodes) {
@changeList.push(new ChangeRecord(this, 'insertNodesAfter', nodes));
}
visitChildren(visitor) {
this.forEachChild(childPath => childPath.visit(visitor));
}
visit(visitor) {
// TODO: applyChanges will not be run if called from top-level. Is this a problem?
if (!@node) {
return;
}
let method = visitor[@node.type];
if (typeof method === 'function') {
method.call(visitor, this);
}
if (!@node) {
return;
}
let { after } = visitor;
if (typeof after === 'function') {
after.call(visitor, this);
}
if (!method) {
this.visitChildren(visitor);
}
}
uniqueIdentifier(baseName, options = {}) {
let scopeInfo = @scopeInfo;
let ident = null;
for (let i = 0; true; ++i) {
let value = baseName;
if (i > 0) value += '_' + i;
if (!scopeInfo.names.has(value)) {
ident = value;
break;
}
}
scopeInfo.names.add(ident);
if (options.kind) {
@changeList.push(new ChangeRecord(this, 'insertDeclaration', [ident, options]));
}
return ident;
}
static fromParseResult(result) {
let path = new Path(result.ast);
path.@scopeInfo = getScopeInfo(result);
return path;
}
@getLocation(fn) {
if (!@parent) {
throw new Error('Node does not have a parent');
}
let { key, index } = @location;
let node = @node;
let parent = @parent.@node;
let valid = typeof index === 'number' ?
parent[key][index] === node :
parent[key] === node;
if (!valid) {
AST.forEachChild(parent, (child, k, i, stop) => {
if (child === node) {
valid = true;
@location = { key: (key = k), index: (index = i) };
return stop;
}
});
}
if (!valid) {
throw new Error('Unable to determine node location');
}
fn(parent, key, index);
}
@getBlock() {
let path = this;
while (path) {
switch (path.node.type) {
case 'Script':
case 'Module':
case 'Block':
case 'FunctionBody':
return path;
}
path = path.parent;
}
return null;
}
}
class ChangeRecord {
constructor(path, name, args) {
@path = path;
@name = name;
@args = args;
}
apply() {
switch (@name) {
case 'replaceNode': return @replaceNode(@args[0]);
case 'insertNodesAfter': return @insertNodesAfter(@args);
case 'insertNodesBefore': return @insertNodesBefore(@args);
case 'insertDeclaration': return @insertDeclaration(...@args);
default: throw new Error('Invalid change record type');
}
}
@replaceNode(newNode) {
if (@path.@parent) {
@path.@getLocation((parent, key, index) => {
if (typeof index !== 'number') {
parent[key] = newNode;
} else if (newNode) {
parent[key].splice(index, 1, newNode);
} else {
parent[key].splice(index, 1);
}
});
}
@path.@node = newNode;
}
@insertNodesAfter(nodes) {
@path.@getLocation((parent, key, index) => {
if (typeof index !== 'number') {
throw new Error('Node is not contained within a node list');
}
parent[key].splice(index + 1, 0, ...nodes);
});
}
@insertNodesBefore(nodes) {
@path.@getLocation((parent, key, index) => {
if (typeof index !== 'number') {
throw new Error('Node is not contained within a node list');
}
parent[key].splice(index, 0, ...nodes);
});
}
@insertDeclaration(ident, options) {
let { statements } = @path.@getBlock().node;
let i = 0;
while (i < statements.length) {
if (statements[i].type !== 'VariableDeclaration') break;
i += 1;
}
statements.splice(i, 0, {
type: 'VariableDeclaration',
kind: options.kind,
declarations: [{
type: 'VariableDeclarator',
pattern: { type: 'Identifier', value: ident },
initializer: options.initializer || null,
}],
});
}
}
function getScopeInfo(parseResult) {
let scopeTree = resolveScopes(parseResult.ast, { lineMap: parseResult.lineMap });
let names = new Set();
function visit(scope) {
scope.names.forEach((value, key) => names.add(key));
scope.free.forEach(ident => names.add(ident.value));
scope.children.forEach(visit);
}
visit(scopeTree);
return { names };
}
@hax
Copy link

hax commented Jan 10, 2019

A small question: it seems @foo syntax contradicts @deco usage?

@mbrowne
Copy link

mbrowne commented Jul 1, 2019

@zenparsing Are you still interested in pursuing this idea? I know you're not a big fan of private class fields, but they're presumably going to stage 4, and I think this could still be a nice complement—with different syntax of course (given how much traction @ has gained for decorators). I would love to help support a shorthand symbol literals proposal, but I'm not sure there's much use unless someone on the committee is also interested.

CC @ljharb in case you or someone you know on the committee might be interested in working with the community on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment