atom-nuclide
Version:
A unified developer experience for web and mobile development, built as a suite of features on top of Atom to provide hackability and the support of an active community.
742 lines (659 loc) • 26.8 kB
JavaScript
Object.defineProperty(exports, '__esModule', {
value: true
});
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
exports.parseServiceDefinition = parseServiceDefinition;
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/
var _assert2;
function _assert() {
return _assert2 = _interopRequireDefault(require('assert'));
}
var _babelParse2;
function _babelParse() {
return _babelParse2 = _interopRequireDefault(require('./babel-parse'));
}
var _builtinTypes2;
function _builtinTypes() {
return _builtinTypes2 = require('./builtin-types');
}
var _location2;
function _location() {
return _location2 = require('./location');
}
var _DefinitionValidator2;
function _DefinitionValidator() {
return _DefinitionValidator2 = require('./DefinitionValidator');
}
var _resolveFrom2;
function _resolveFrom() {
return _resolveFrom2 = _interopRequireDefault(require('resolve-from'));
}
var _commonsNodeNuclideUri2;
function _commonsNodeNuclideUri() {
return _commonsNodeNuclideUri2 = _interopRequireDefault(require('../../commons-node/nuclideUri'));
}
var _fs2;
function _fs() {
return _fs2 = _interopRequireDefault(require('fs'));
}
function isPrivateMemberName(name) {
return name.startsWith('_');
}
/**
* Parse a definition file, returning an intermediate representation that has all of the
* information required to generate the remote proxy, as well as marshal and unmarshal the
* data over a network.
* @param source - The string source of the definition file.
*/
function parseServiceDefinition(fileName, source) {
return new ServiceParser().parseService(fileName, source);
}
var ServiceParser = (function () {
function ServiceParser() {
var _this = this;
_classCallCheck(this, ServiceParser);
this._defs = new Map();
this._filesTodo = [];
this._filesSeen = new Set();
// Add all builtin types
var defineBuiltinType = function defineBuiltinType(name) {
_this._defs.set(name, {
kind: 'alias',
name: name,
location: { type: 'builtin' }
});
};
(_builtinTypes2 || _builtinTypes()).namedBuiltinTypes.forEach(defineBuiltinType);
// TODO: Find a better place for this.
defineBuiltinType('NuclideUri');
}
_createClass(ServiceParser, [{
key: 'parseService',
value: function parseService(fileName, source) {
this._filesSeen.add(fileName);
this._parseFile(fileName, 'service', source);
while (this._filesTodo.length > 0) {
var _file = this._filesTodo.pop();
var contents = (_fs2 || _fs()).default.readFileSync(_file, 'utf8');
this._parseFile(_file, 'import', contents);
}
(0, (_DefinitionValidator2 || _DefinitionValidator()).validateDefinitions)(this._defs);
return this._defs;
}
}, {
key: '_parseFile',
value: function _parseFile(fileName, fileType, source) {
var parser = new FileParser(fileName, fileType, this._defs);
var imports = parser.parse(source);
for (var imp of imports) {
var resolvedFrom = (0, (_resolveFrom2 || _resolveFrom()).default)((_commonsNodeNuclideUri2 || _commonsNodeNuclideUri()).default.dirname(fileName), imp);
if (!this._filesSeen.has(resolvedFrom)) {
this._filesSeen.add(resolvedFrom);
this._filesTodo.push(resolvedFrom);
}
}
}
}]);
return ServiceParser;
})();
var FileParser = (function () {
function FileParser(fileName, fileType, defs) {
_classCallCheck(this, FileParser);
this._fileType = fileType;
this._fileName = fileName;
this._defs = defs;
this._imports = new Map();
this._importsUsed = new Set();
}
_createClass(FileParser, [{
key: '_locationOfNode',
value: function _locationOfNode(node) {
return {
type: 'source',
fileName: this._fileName,
line: node.loc.start.line
};
}
}, {
key: '_nodeLocationString',
value: function _nodeLocationString(node) {
return this._fileName + '(' + node.loc.start.line + ')';
}
}, {
key: '_errorLocations',
value: function _errorLocations(locations, message) {
var _fullMessage;
var fullMessage = (0, (_location2 || _location()).locationToString)(locations[0]) + ':' + message;
fullMessage = (_fullMessage = fullMessage).concat.apply(_fullMessage, _toConsumableArray(locations.slice(1).map(function (location) {
return '\n' + (0, (_location2 || _location()).locationToString)(location) + ': Related location';
})));
return new Error(fullMessage);
}
}, {
key: '_error',
value: function _error(node, message) {
return new Error(this._nodeLocationString(node) + ':' + message);
}
}, {
key: '_assert',
value: function _assert(node, condition, message) {
if (!condition) {
throw this._error(node, message);
}
}
// Returns set of imported files required.
// The file names returned are relative to the file being parsed.
}, {
key: 'parse',
value: function parse(source) {
this._imports = new Map();
var program = (0, (_babelParse2 || _babelParse()).default)(source);
(0, (_assert2 || _assert()).default)(program && program.type === 'Program', 'The result of parsing is a Program node.');
// Iterate through each node in the program body.
for (var node of program.body) {
// We're specifically looking for exports.
switch (node.type) {
case 'ExportNamedDeclaration':
this._parseExport(node);
break;
case 'ImportDeclaration':
this._parseImport(node);
break;
default:
// Ignore all non-export top level program elements including:
// imports, statements, variable declarations, function declarations
break;
}
}
return this._importsUsed;
}
}, {
key: '_parseExport',
value: function _parseExport(node) {
(0, (_assert2 || _assert()).default)(node.type === 'ExportNamedDeclaration');
var declaration = node.declaration;
switch (declaration.type) {
// An exported function that can be directly called by a client.
case 'FunctionDeclaration':
if (!isPrivateMemberName(declaration.id.name)) {
this._add(this._parseFunctionDeclaration(declaration));
}
break;
// An exported type alias.
case 'TypeAlias':
if (!isPrivateMemberName(declaration.id.name)) {
this._add(this._parseTypeAlias(declaration));
}
break;
// Parse classes as remotable interfaces.
case 'ClassDeclaration':
this._add(this._parseClassDeclaration(declaration));
break;
case 'InterfaceDeclaration':
this._add(this._parseInterfaceDeclaration(declaration));
break;
case 'VariableDeclaration':
// Ignore exported variables.
break;
// Unknown export declaration.
default:
throw this._error(declaration, 'Unknown declaration type ' + declaration.type + ' in definition body.');
}
}
}, {
key: '_parseImport',
value: function _parseImport(node) {
var from = node.source.value;
(0, (_assert2 || _assert()).default)(typeof from === 'string');
for (var specifier of node.specifiers) {
if (specifier.type === 'ImportSpecifier') {
var _imported = specifier.imported.name;
var local = specifier.local.name;
this._imports.set(local, {
imported: _imported,
file: from,
added: false,
location: this._locationOfNode(specifier)
});
}
}
}
}, {
key: '_add',
value: function _add(definition) {
if (this._defs.has(definition.name)) {
var existingDef = this._defs.get(definition.name);
(0, (_assert2 || _assert()).default)(existingDef != null);
throw this._errorLocations([definition.location, existingDef.location], 'Duplicate definition for ' + definition.name);
} else {
this._defs.set(definition.name, definition);
}
}
/**
* Helper function that parses an exported function declaration, and returns the function name,
* along with a FunctionType object that encodes the argument and return types of the function.
*/
}, {
key: '_parseFunctionDeclaration',
value: function _parseFunctionDeclaration(declaration) {
var _this2 = this;
if (this._fileType === 'import') {
throw this._error(declaration, 'Exported function in imported RPC file');
}
this._assert(declaration, declaration.id && declaration.id.type === 'Identifier', 'Remote function declarations must have an identifier.');
this._assert(declaration, declaration.returnType != null && declaration.returnType.type === 'TypeAnnotation', 'Remote functions must be annotated with a return type.');
var returnType = this._parseTypeAnnotation(declaration.returnType.typeAnnotation);
return {
kind: 'function',
name: declaration.id.name,
location: this._locationOfNode(declaration),
type: {
location: this._locationOfNode(declaration),
kind: 'function',
argumentTypes: declaration.params.map(function (param) {
return _this2._parseParameter(param);
}),
returnType: returnType
}
};
}
/**
* Helper function that parses an exported type alias, and returns the name of the alias,
* along with the type that it refers to.
*/
}, {
key: '_parseTypeAlias',
value: function _parseTypeAlias(declaration) {
this._assert(declaration, declaration.type === 'TypeAlias', 'parseTypeAlias accepts a TypeAlias node.');
return {
kind: 'alias',
location: this._locationOfNode(declaration),
name: declaration.id.name,
definition: this._parseTypeAnnotation(declaration.right)
};
}
/**
* Parse a ClassDeclaration AST Node.
* @param declaration - The AST node.
*/
}, {
key: '_parseClassDeclaration',
value: function _parseClassDeclaration(declaration) {
var _this3 = this;
if (this._fileType === 'import') {
throw this._error(declaration, 'Exported class in imported RPC file');
}
var def = {
kind: 'interface',
name: declaration.id.name,
location: this._locationOfNode(declaration),
constructorArgs: [],
staticMethods: new Map(),
instanceMethods: new Map()
};
var classBody = declaration.body;
for (var method of classBody.body) {
if (method.kind === 'constructor') {
def.constructorArgs = method.value.params.map(function (param) {
return _this3._parseParameter(param);
});
if (method.value.returnType) {
throw this._error(method, 'constructors may not have return types');
}
} else {
if (!isPrivateMemberName(method.key.name)) {
var _parseMethodDefinition2 = this._parseMethodDefinition(method);
var _name = _parseMethodDefinition2.name;
var _type = _parseMethodDefinition2.type;
var isStatic = Boolean(method.static);
this._validateMethod(method, _name, _type, isStatic);
this._defineMethod(_name, _type, isStatic ? def.staticMethods : def.instanceMethods);
}
}
}
if (!def.instanceMethods.has('dispose')) {
throw this._error(declaration, 'Remotable interfaces must include a dispose method');
}
return def;
}
}, {
key: '_validateMethod',
value: function _validateMethod(node, name, type, isStatic) {
if (name === 'dispose' && !isStatic) {
// Validate dispose method has a reasonable signature
if (type.argumentTypes.length > 0) {
throw this._error(node, 'dispose method may not take arguments');
}
if (!isValidDisposeReturnType(type.returnType)) {
throw this._error(node, 'dispose method must return either void or Promise<void>');
}
}
}
/**
* Parse a InterfaceDeclaration AST Node.
* @param declaration - The AST node.
*/
}, {
key: '_parseInterfaceDeclaration',
value: function _parseInterfaceDeclaration(declaration) {
var def = {
kind: 'interface',
name: declaration.id.name,
location: this._locationOfNode(declaration),
constructorArgs: null,
staticMethods: new Map(),
instanceMethods: new Map()
};
(0, (_assert2 || _assert()).default)(declaration.body.type === 'ObjectTypeAnnotation');
var properties = declaration.body.properties;
for (var property of properties) {
(0, (_assert2 || _assert()).default)(property.type === 'ObjectTypeProperty');
if (!isPrivateMemberName(property.key.name)) {
var _parseInterfaceMethodDefinition2 = this._parseInterfaceMethodDefinition(property);
var _name2 = _parseInterfaceMethodDefinition2.name;
var _type2 = _parseInterfaceMethodDefinition2.type;
(0, (_assert2 || _assert()).default)(!property.static, 'static interface members are a parse error');
this._validateMethod(property, _name2, _type2, false);
this._defineMethod(_name2, _type2, def.instanceMethods);
}
}
if (!def.instanceMethods.has('dispose')) {
throw this._error(declaration, 'Remotable interfaces must include a dispose method');
}
return def;
}
}, {
key: '_defineMethod',
value: function _defineMethod(name, type, peers) {
if (peers.has(name)) {
// $FlowFixMe(peterhal)
var relatedLocation = peers.get(name).location;
throw this._errorLocations([type.location, relatedLocation], 'Duplicate method definition ' + name);
} else {
peers.set(name, type);
}
}
/**
* Helper function that parses an method definition in a class.
* @param defintion - The MethodDefinition AST node.
* @returns A record containing the name of the method, and a FunctionType object
* encoding the arguments and return type of the method.
*/
}, {
key: '_parseMethodDefinition',
value: function _parseMethodDefinition(definition) {
var _this4 = this;
this._assert(definition, definition.type === 'MethodDefinition', 'This is a MethodDefinition object.');
this._assert(definition, definition.key && definition.key.type === 'Identifier', 'This method defintion has an key (a name).');
this._assert(definition, definition.value.returnType && definition.value.returnType.type === 'TypeAnnotation', definition.key.name + ' missing a return type annotation.');
var returnType = this._parseTypeAnnotation(definition.value.returnType.typeAnnotation);
return {
location: this._locationOfNode(definition.key),
name: definition.key.name,
type: {
location: this._locationOfNode(definition.value),
kind: 'function',
argumentTypes: definition.value.params.map(function (param) {
return _this4._parseParameter(param);
}),
returnType: returnType
}
};
}
/**
* Parses an method definition in an interface.
* Note that interface method definitions are slightly different structure to class methods.
* @param defintion - The ObjectTypeProperty AST node.
* @returns A record containing the name of the method, and a FunctionType object
* encoding the arguments and return type of the method.
*/
}, {
key: '_parseInterfaceMethodDefinition',
value: function _parseInterfaceMethodDefinition(definition) {
var _this5 = this;
this._assert(definition, definition.type === 'ObjectTypeProperty', 'This is a ObjectTypeProperty object.');
this._assert(definition, definition.key && definition.key.type === 'Identifier', 'This method definition has an key (a name).');
this._assert(definition, definition.value.returnType != null, definition.key.name + ' missing a return type annotation.');
var returnType = this._parseTypeAnnotation(definition.value.returnType);
return {
location: this._locationOfNode(definition.key),
name: definition.key.name,
type: {
location: this._locationOfNode(definition.value),
kind: 'function',
argumentTypes: definition.value.params.map(function (param) {
return _this5._parseInterfaceParameter(param);
}),
returnType: returnType
}
};
}
}, {
key: '_parseInterfaceParameter',
value: function _parseInterfaceParameter(param) {
if (!param.typeAnnotation) {
throw this._error(param, 'Parameter ' + param.name + ' doesn\'t have type annotation.');
} else {
var _name3 = param.name;
var _type3 = this._parseTypeAnnotation(param.typeAnnotation);
if (param.optional && _type3.kind !== 'nullable') {
return {
name: _name3,
type: {
location: this._locationOfNode(param),
kind: 'nullable',
type: _type3
}
};
} else {
return {
name: _name3,
type: _type3
};
}
}
}
}, {
key: '_parseParameter',
value: function _parseParameter(param) {
// Parameter with a default type, e.g. (x: number = 1).
// Babel's transpiled implementation will take care of actually setting the default.
if (param.type === 'AssignmentPattern') {
return this._parseParameter(_extends({}, param.left, {
// Having a default value implies that it's optional.
optional: true
}));
}
if (!param.typeAnnotation) {
throw this._error(param, 'Parameter ' + param.name + ' doesn\'t have type annotation.');
} else {
var _name4 = param.name;
var _type4 = this._parseTypeAnnotation(param.typeAnnotation.typeAnnotation);
if (param.optional && _type4.kind !== 'nullable') {
return {
name: _name4,
type: {
location: this._locationOfNode(param),
kind: 'nullable',
type: _type4
}
};
} else {
return {
name: _name4,
type: _type4
};
}
}
}
/**
* Helper function that parses a Flow type annotation into our intermediate format.
* @returns {Type} A representation of the type.
*/
}, {
key: '_parseTypeAnnotation',
value: function _parseTypeAnnotation(typeAnnotation) {
var _this6 = this;
var location = this._locationOfNode(typeAnnotation);
switch (typeAnnotation.type) {
case 'AnyTypeAnnotation':
return { location: location, kind: 'any' };
case 'MixedTypeAnnotation':
return { location: location, kind: 'mixed' };
case 'StringTypeAnnotation':
return { location: location, kind: 'string' };
case 'NumberTypeAnnotation':
return { location: location, kind: 'number' };
case 'BooleanTypeAnnotation':
return { location: location, kind: 'boolean' };
case 'StringLiteralTypeAnnotation':
return { location: location, kind: 'string-literal', value: typeAnnotation.value };
case 'NumberLiteralTypeAnnotation':
return { location: location, kind: 'number-literal', value: typeAnnotation.value };
case 'BooleanLiteralTypeAnnotation':
return { location: location, kind: 'boolean-literal', value: typeAnnotation.value };
case 'NullableTypeAnnotation':
return {
location: location,
kind: 'nullable',
type: this._parseTypeAnnotation(typeAnnotation.typeAnnotation)
};
case 'ObjectTypeAnnotation':
return {
location: location,
kind: 'object',
fields: typeAnnotation.properties.map(function (prop) {
(0, (_assert2 || _assert()).default)(prop.type === 'ObjectTypeProperty');
return {
location: _this6._locationOfNode(prop),
name: prop.key.name,
type: _this6._parseTypeAnnotation(prop.value),
optional: prop.optional
};
})
};
case 'VoidTypeAnnotation':
return { location: location, kind: 'void' };
case 'TupleTypeAnnotation':
return {
location: location,
kind: 'tuple',
types: typeAnnotation.types.map(this._parseTypeAnnotation.bind(this))
};
case 'UnionTypeAnnotation':
return {
location: location,
kind: 'union',
types: typeAnnotation.types.map(this._parseTypeAnnotation.bind(this))
};
case 'IntersectionTypeAnnotation':
return {
location: location,
kind: 'intersection',
types: typeAnnotation.types.map(this._parseTypeAnnotation.bind(this))
};
case 'GenericTypeAnnotation':
return this._parseGenericTypeAnnotation(typeAnnotation);
default:
throw this._error(typeAnnotation, 'Unknown type annotation ' + typeAnnotation.type + '.');
}
}
/**
* Helper function that parses annotations of type 'GenericTypeAnnotation'. Meant to be called
* from parseTypeAnnotation.
*/
}, {
key: '_parseGenericTypeAnnotation',
value: function _parseGenericTypeAnnotation(typeAnnotation) {
(0, (_assert2 || _assert()).default)(typeAnnotation.type === 'GenericTypeAnnotation');
var id = this._parseTypeName(typeAnnotation.id);
var location = this._locationOfNode(typeAnnotation);
switch (id) {
case 'Array':
return {
location: location,
kind: 'array',
type: this._parseGenericTypeParameterOfKnownType(id, typeAnnotation)
};
case 'Set':
return {
location: location,
kind: 'set',
type: this._parseGenericTypeParameterOfKnownType(id, typeAnnotation)
};
case 'Promise':
return {
location: location,
kind: 'promise',
type: this._parseGenericTypeParameterOfKnownType(id, typeAnnotation)
};
case 'ConnectableObservable':
return {
location: location,
kind: 'observable',
type: this._parseGenericTypeParameterOfKnownType(id, typeAnnotation)
};
case 'Map':
this._assert(typeAnnotation, typeAnnotation.typeParameters != null && typeAnnotation.typeParameters.params.length === 2, id + ' takes exactly two type parameters.');
return {
location: location,
kind: 'map',
keyType: this._parseTypeAnnotation(typeAnnotation.typeParameters.params[0]),
valueType: this._parseTypeAnnotation(typeAnnotation.typeParameters.params[1])
};
default:
this._assert(typeAnnotation, id !== 'Observable', 'Use of Observable in RPC interface. Use ConnectableObservable instead.');
// Named types are represented as Generic types with no type parameters.
this._assert(typeAnnotation, typeAnnotation.typeParameters == null, 'Unknown generic type ' + id + '.');
var imp = this._imports.get(id);
if (id !== 'NuclideUri' && imp != null && !imp.added) {
imp.added = true;
this._importsUsed.add(imp.file);
if (id !== imp.imported) {
return { location: location, kind: 'named', name: imp.imported };
}
}
return { location: location, kind: 'named', name: id };
}
}
}, {
key: '_parseGenericTypeParameterOfKnownType',
value: function _parseGenericTypeParameterOfKnownType(id, typeAnnotation) {
this._assert(typeAnnotation, typeAnnotation.typeParameters != null && typeAnnotation.typeParameters.params.length === 1, id + ' has exactly one type parameter.');
return this._parseTypeAnnotation(typeAnnotation.typeParameters.params[0]);
}
/**
* Type names may either be simple Identifiers, or they may be
* qualified identifiers.
*/
}, {
key: '_parseTypeName',
value: function _parseTypeName(type) {
switch (type.type) {
case 'Identifier':
return type.name;
case 'QualifiedTypeIdentifier':
(0, (_assert2 || _assert()).default)(type.id.type === 'Identifier');
return this._parseTypeName(type.qualification) + '.' + type.id.name;
default:
throw this._error(type, 'Expected named type. Found ' + type.type);
}
}
}]);
return FileParser;
})();
function isValidDisposeReturnType(type) {
return type.kind === 'void' || type.kind === 'promise' && type.type.kind === 'void';
}
// Maps type names to the imported name and file that they are imported from.
// Set of files required by imports