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.
685 lines (626 loc) • 21.8 kB
JavaScript
Object.defineProperty(exports, '__esModule', {
value: true
});
exports.validateDefinitions = validateDefinitions;
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); } }
/**
* Throws if a named type referenced in an RPC interface is not defined.
* The error message thrown is suitable for display to a human.
*
* NOTE: Will also mutate the incoming definitions in place to make them easier to marshal.
*/
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 _location2;
function _location() {
return _location2 = require('./location');
}
var _assert2;
function _assert() {
return _assert2 = _interopRequireDefault(require('assert'));
}
var _commonsNodeCollection2;
function _commonsNodeCollection() {
return _commonsNodeCollection2 = require('../../commons-node/collection');
}
function validateDefinitions(definitions) {
var namedTypes = new Map();
gatherKnownTypes();
validate();
function validate() {
findMissingTypeNames();
findRecursiveAliases();
validateReturnTypes();
cannonicalize();
}
function findMissingTypeNames() {
visitAllTypes(checkTypeForMissingNames);
}
function gatherKnownTypes() {
for (var definition of definitions.values()) {
switch (definition.kind) {
case 'alias':
case 'interface':
namedTypes.set(definition.name, definition);
break;
}
}
}
function checkTypeForMissingNames(type) {
switch (type.kind) {
case 'any':
case 'mixed':
case 'string':
case 'boolean':
case 'number':
case 'string-literal':
case 'boolean-literal':
case 'number-literal':
case 'void':
break;
case 'promise':
checkTypeForMissingNames(type.type);
break;
case 'observable':
checkTypeForMissingNames(type.type);
break;
case 'array':
checkTypeForMissingNames(type.type);
break;
case 'set':
checkTypeForMissingNames(type.type);
break;
case 'nullable':
checkTypeForMissingNames(type.type);
break;
case 'map':
checkTypeForMissingNames(type.keyType);
checkTypeForMissingNames(type.valueType);
break;
case 'object':
type.fields.map(function (field) {
return field.type;
}).forEach(checkTypeForMissingNames);
break;
case 'tuple':
type.types.forEach(checkTypeForMissingNames);
break;
case 'union':
type.types.forEach(checkTypeForMissingNames);
break;
case 'intersection':
type.types.forEach(checkTypeForMissingNames);
break;
case 'function':
type.argumentTypes.forEach(function (parameter) {
return checkTypeForMissingNames(parameter.type);
});
checkTypeForMissingNames(type.returnType);
break;
case 'named':
var name = type.name;
if (!namedTypes.has(name)) {
throw error(type, 'No definition for type ' + name + '.');
}
break;
default:
throw new Error(JSON.stringify(type));
}
}
function findRecursiveAliases() {
for (var definition of definitions.values()) {
switch (definition.kind) {
case 'alias':
checkAliasLayout(definition);
break;
}
}
}
function checkAliasLayout(alias) {
if (alias.definition) {
validateLayoutRec([alias], alias.definition);
}
}
/**
* Validates that a type does not directly contain any types which are known to
* directly or indirectly contain it.
*
* If recursion is found the chain of types which recursively contain each other is reported.
*/
function validateLayoutRec(containingDefinitions, type) {
function validateTypeRec(typeRec) {
validateLayoutRec(containingDefinitions, typeRec);
}
switch (type.kind) {
case 'any':
case 'mixed':
case 'string':
case 'boolean':
case 'number':
case 'string-literal':
case 'boolean-literal':
case 'number-literal':
case 'void':
break;
case 'promise':
case 'observable':
validateTypeRec(type.type);
break;
case 'nullable':
// Nullable breaks the layout chain
break;
case 'map':
case 'array':
case 'set':
// Containers break the layout chain as they may be empty.
break;
case 'object':
type.fields.filter(function (field) {
return !field.optional;
}).map(function (field) {
return field.type;
}).forEach(validateTypeRec);
break;
case 'tuple':
type.types.forEach(validateTypeRec);
break;
case 'union':
// Union types break the layout chain.
// TODO: Strictly we should detect alternates which directly contain their parent union,
// or if all alternates indirectly contain the parent union.
break;
case 'intersection':
type.types.forEach(validateTypeRec);
break;
case 'function':
break;
case 'named':
var name = type.name;
// $FlowFixMe(peterhal)
var definition = namedTypes.get(name);
if (containingDefinitions.indexOf(definition) !== -1) {
throw errorDefinitions(containingDefinitions.slice(containingDefinitions.indexOf(definition)), 'Type ' + name + ' contains itself.');
} else if (definition.kind === 'alias' && definition.definition != null) {
containingDefinitions.push(definition);
(0, (_assert2 || _assert()).default)(definition.definition);
validateLayoutRec(containingDefinitions, definition.definition);
containingDefinitions.pop();
}
break;
default:
throw new Error(JSON.stringify(type));
}
}
function validateReturnTypes() {
for (var definition of definitions.values()) {
switch (definition.kind) {
case 'function':
validateType(definition.type);
break;
case 'alias':
if (definition.definition != null) {
validateAliasType(definition.definition);
}
break;
case 'interface':
if (definition.constructorArgs != null) {
definition.constructorArgs.forEach(function (parameter) {
return validateType(parameter.type);
});
}
definition.instanceMethods.forEach(validateType);
definition.staticMethods.forEach(validateType);
break;
}
}
}
// Validates a type which must be a return type.
// Caller must resolve named types.
function validateReturnType(funcType, type) {
function invalidReturnTypeError() {
return error(funcType, 'The return type of a remote function must be of type Void, Promise, or Observable');
}
switch (type.kind) {
case 'void':
break;
case 'promise':
case 'observable':
if (type.type.kind !== 'void') {
validateType(type.type);
}
break;
default:
throw invalidReturnTypeError();
}
}
// Aliases may be return types, or non-return types.
function validateAliasType(type) {
switch (type.kind) {
case 'void':
break;
case 'promise':
case 'observable':
if (type.type.kind !== 'void') {
validateType(type.type);
}
break;
case 'named':
// No need to recurse, as the target alias definition will get validated seperately.
break;
default:
validateType(type);
break;
}
}
function isLiteralType(type) {
switch (type.kind) {
case 'string-literal':
case 'number-literal':
case 'boolean-literal':
return true;
default:
return false;
}
}
function validateIntersectionType(intersectionType) {
var fields = flattenIntersection(intersectionType);
var fieldNameToLocation = new Map();
for (var field of fields) {
if (fieldNameToLocation.has(field.name)) {
// TODO allow duplicate field names if they have the same type.
var otherLocation = fieldNameToLocation.get(field.name);
(0, (_assert2 || _assert()).default)(otherLocation != null);
throw errorLocations([intersectionType.location, field.location, otherLocation], 'Duplicate field name \'' + field.name + '\' in intersection types are not supported.');
}
fieldNameToLocation.set(field.name, field.location);
}
}
function validateUnionType(type) {
var alternates = flattenUnionAlternates(type.types);
if (isLiteralType(alternates[0])) {
validateLiteralUnionType(type, alternates);
} else if (alternates[0].kind === 'object') {
validateObjectUnionType(type, alternates);
} else {
throw errorLocations([type.location, alternates[0].location], 'Union alternates must be either be typed object or literal types.');
}
}
function validateLiteralUnionType(type, alternates) {
alternates.reduce(function (previousAlternates, alternate) {
validateType(alternate);
// Ensure a valid alternate
if (!isLiteralType(alternate)) {
throw errorLocations([type.location, alternate.location], 'Union alternates may only be literal types.');
}
// Ensure no duplicates
previousAlternates.forEach(function (previous) {
(0, (_assert2 || _assert()).default)(previous.kind === 'string-literal' || previous.kind === 'number-literal' || previous.kind === 'boolean-literal');
(0, (_assert2 || _assert()).default)(alternate.kind === 'string-literal' || alternate.kind === 'number-literal' || alternate.kind === 'boolean-literal');
if (previous.value === alternate.value) {
throw errorLocations([type.location, previous.location, alternate.location], 'Union alternates may not have the same value.');
}
});
previousAlternates.push(alternate);
return previousAlternates;
}, []);
}
function validateObjectUnionType(type, alternates) {
alternates.forEach(function (alternate) {
validateType(alternate);
// Ensure alternates match
if (alternate.kind !== 'object') {
throw errorLocations([type.location, alternates[0].location, alternate.location], 'Union alternates must be of the same type.');
}
});
type.discriminantField = findObjectUnionDiscriminant(type, alternates);
}
function findObjectUnionDiscriminant(type, alternates) {
// Get set of fields which are literal types in al alternates.
(0, (_assert2 || _assert()).default)(alternates.length > 0);
// $FlowFixMe
var possibleFields = alternates.reduce(function (possibilities, alternate) {
var alternatePossibilities = possibleDiscriminantFieldsOfUnionAlternate(alternate);
if (alternatePossibilities.size === 0) {
throw errorLocations([type.location, alternate.location], 'Object union alternative has no possible discriminant fields.');
}
// Use null to represent the set containing everything.
if (possibilities == null) {
return alternatePossibilities;
} else {
return (0, (_commonsNodeCollection2 || _commonsNodeCollection()).setIntersect)(alternatePossibilities, possibilities);
}
}, null);
var validFields = Array.from(possibleFields).filter(function (fieldName) {
return isValidDiscriminantField(alternates, fieldName);
});
if (validFields.length > 0) {
// If there are multiple valid discriminant fields, we just pick the first.
return validFields[0];
} else {
// TODO: Better error message why each possibleFields is invalid.
throw error(type, 'No valid discriminant field for union type.');
}
}
function isValidDiscriminantField(alternates, candidateField) {
// $FlowFixMe
var fieldTypes = alternates.map(function (alternate) {
return resolvePossiblyNamedType(getObjectFieldByName(alternate, candidateField).type);
});
// Fields in all alternates must have same type.
if (!fieldTypes.every(function (fieldType) {
return fieldType.kind === fieldTypes[0].kind;
})) {
return false;
}
// Must not have duplicate values in any alternate.
// All alternates must be unique.
return new Set(fieldTypes.map(function (fieldType) {
return fieldType.value;
})).size === alternates.length;
}
function getObjectFieldByName(type, fieldName) {
var result = type.fields.find(function (field) {
return field.name === fieldName;
});
(0, (_assert2 || _assert()).default)(result != null);
return result;
}
function possibleDiscriminantFieldsOfUnionAlternate(alternate) {
return new Set(alternate.fields.filter(function (field) {
return isLiteralType(resolvePossiblyNamedType(field.type));
}).map(function (field) {
return field.name;
}));
}
// Validates a type which is not directly a return type.
function validateType(type) {
switch (type.kind) {
case 'any':
case 'mixed':
case 'string':
case 'boolean':
case 'number':
case 'string-literal':
case 'boolean-literal':
case 'number-literal':
break;
case 'void':
case 'promise':
case 'observable':
throw error(type, 'Promise, void and Observable types may only be used as return types');
case 'array':
validateType(type.type);
break;
case 'set':
validateType(type.type);
break;
case 'nullable':
validateType(type.type);
break;
case 'map':
validateType(type.keyType);
validateType(type.valueType);
break;
case 'object':
type.fields.map(function (field) {
return field.type;
}).forEach(validateType);
break;
case 'tuple':
type.types.forEach(validateType);
break;
case 'union':
validateUnionType(type);
break;
case 'intersection':
validateIntersectionType(type);
break;
case 'function':
type.argumentTypes.forEach(function (parameter) {
return validateType(parameter.type);
});
validateReturnType(type, resolvePossiblyNamedType(type.returnType));
break;
case 'named':
var resolvedType = resolveNamedType(type);
// Note: We do not recurse here as types may be self-recursive (through nullable for
// example).
// The resolvedType will already have been checked to be a valid alias type.
// so we only need to check the difference between alias types and non-return types.
switch (resolvedType.kind) {
case 'void':
case 'promise':
case 'observable':
throw error(type, 'Promise, void and Observable types may only be used as return types');
}
break;
default:
throw new Error(JSON.stringify(type));
}
}
// Replaces all uses of type aliases in return types with their definition
// so that clients need not be aware of aliases.
// TODO: Should replace all aliases, however that will require rewriting marsalling.
function cannonicalize() {
visitAllTypes(cannonicalizeType);
}
function cannonicalizeTypeArray(types) {
types.forEach(cannonicalizeType);
}
function cannonicalizeType(type) {
switch (type.kind) {
case 'any':
case 'mixed':
case 'string':
case 'boolean':
case 'number':
case 'string-literal':
case 'boolean-literal':
case 'number-literal':
case 'void':
break;
case 'promise':
cannonicalizeType(type.type);
break;
case 'observable':
cannonicalizeType(type.type);
break;
case 'array':
cannonicalizeType(type.type);
break;
case 'set':
cannonicalizeType(type.type);
break;
case 'nullable':
cannonicalizeType(type.type);
break;
case 'map':
cannonicalizeType(type.keyType);
cannonicalizeType(type.valueType);
break;
case 'object':
type.fields.forEach(function (field) {
cannonicalizeType(field.type);
});
break;
case 'tuple':
cannonicalizeTypeArray(type.types);
break;
case 'union':
cannonicalizeTypeArray(type.types);
type.types = flattenUnionAlternates(type.types);
break;
case 'intersection':
cannonicalizeTypeArray(type.types);
canonicalizeIntersection(type);
break;
case 'function':
type.argumentTypes.forEach(function (parameter) {
cannonicalizeType(parameter.type);
});
type.returnType = resolvePossiblyNamedType(type.returnType);
cannonicalizeType(type.returnType);
break;
case 'named':
// Note that this does not recurse, so the algorithm will always terminate.
break;
default:
throw new Error(JSON.stringify(type));
}
}
function canonicalizeIntersection(intersectionType) {
var fields = flattenIntersection(intersectionType);
intersectionType.flattened = {
kind: 'object',
location: intersectionType.location,
fields: fields
};
}
function flattenIntersection(intersectionType) {
var fields = [];
for (var _type of intersectionType.types) {
var resolvedType = resolvePossiblyNamedType(_type);
if (resolvedType.kind === 'object') {
fields.push.apply(fields, _toConsumableArray(resolvedType.fields));
} else if (resolvedType.kind === 'intersection') {
fields.push.apply(fields, _toConsumableArray(flattenIntersection(resolvedType)));
} else {
throw errorLocations([intersectionType.location, _type.location], 'Types in intersections must be object or intersection types');
}
}
return fields;
}
// Will return a named type if and only if the alias resolves to a builtin type, or an interface.
function resolvePossiblyNamedType(type) {
if (type.kind === 'named') {
return resolveNamedType(type);
} else {
return type;
}
}
function flattenUnionAlternates(types) {
var _ref;
return (_ref = []).concat.apply(_ref, _toConsumableArray(types.map(function (alternate) {
var resolvedAlternate = resolvePossiblyNamedType(alternate);
return resolvedAlternate.kind === 'union' ? flattenUnionAlternates(resolvedAlternate.types) : resolvedAlternate;
})));
}
// Returns the definition of a named type. If the type resolves to an alias it returns the
// alias's definition.
// Will return a named type if and only if the alias resolves to a builtin type, or an interface.
function resolveNamedType(_x) {
var _again = true;
_function: while (_again) {
var namedType = _x;
_again = false;
var def = namedTypes.get(namedType.name);
(0, (_assert2 || _assert()).default)(def != null);
switch (def.kind) {
case 'alias':
var type = def.definition;
if (type) {
if (type.kind === 'named') {
_x = type;
_again = true;
def = type = undefined;
continue _function;
}
return type;
}
return namedType;
case 'interface':
return namedType;
default:
throw new Error('Unexpected definition kind');
}
}
}
function visitAllTypes(operation) {
for (var definition of definitions.values()) {
switch (definition.kind) {
case 'function':
operation(definition.type);
break;
case 'alias':
if (definition.definition != null) {
operation(definition.definition);
}
break;
case 'interface':
if (definition.constructorArgs != null) {
definition.constructorArgs.forEach(function (parameter) {
return operation(parameter.type);
});
}
definition.instanceMethods.forEach(operation);
definition.staticMethods.forEach(operation);
break;
}
}
}
function error(type, message) {
return errorLocations([type.location], message);
}
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);
}
function errorDefinitions(defs, message) {
var _fullMessage2;
var fullMessage = (0, (_location2 || _location()).locationToString)(defs[0].location) + ':' + message;
fullMessage = (_fullMessage2 = fullMessage).concat.apply(_fullMessage2, _toConsumableArray(defs.slice(1).map(function (definition) {
return '\n' + (0, (_location2 || _location()).locationToString)(definition.location) + ': Related definition ' + definition.name;
})));
return new Error(fullMessage);
}
}