UNPKG

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
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); } }