<commit_before>var Errors = require('./errors');

var fs   = require('fs');
var q    = require('q');
var tls  = require('tls');
var sysu = require('util');
var util = require('./util');
var events = require('events');
var debug = function() {};
if(process.env.DEBUG) {
	try {
		debug = require('debug')('apn');
	}
	catch (e) {
		console.log("Notice: 'debug' module is not available. This should be installed with `npm install debug` to enable debug messages", e);
		debug = function() {};
	}
}

/**
 * Create a new connection to the APN service.
 * @constructor
 * @param {Object} [options]
 * @config {String} [cert="cert.pem"] The filename of the connection certificate to load from disk
 * @config {Buffer|String} [certData] The certificate data. If supplied, will be used instead of loading from disk.
 * @config {String} [key="key.pem"] The filename of the connection key to load from disk
 * @config {Buffer|String} [keyData] The key data. If supplied will be used instead of loading from disk.
 * @config {Buffer[]|String[]} [ca] An array of strings or Buffers of trusted certificates. If this is omitted several well known "root" CAs will be used, like VeriSign. - You may need to use this as some environments don't include the CA used by Apple.
 * @config {String} [pfx] File path for private key, certificate and CA certs in PFX or PKCS12 format. If supplied will be used instead of certificate and key above
 * @config {Buffer|String} [pfxData] PFX or PKCS12 format data containing the private key, certificate and CA certs. If supplied will be used instead of loading from disk.
 * @config {String} [passphrase] The passphrase for the connection key, if required
 * @config {String} [gateway="gateway.push.apple.com"] The gateway server to connect to.
 * @config {Number} [port=2195] Gateway port
 * @config {Boolean} [rejectUnauthorized=true] Reject Unauthorized property to be passed through to tls.connect()
 * @config {Boolean} [enhanced=true] Whether to use the enhanced notification format (recommended)
 * @config {Function} [errorCallback] A callback which accepts 2 parameters (err, notification). Recommended when using enhanced format.
 * @config {Number} [cacheLength=100] Number of notifications to cache for error purposes (See Readme)
 * @config {Boolean} [autoAdjustCache=false] Whether the cache should grow in response to messages being lost after errors. (Will still emit a 'cacheTooSmall' event)
 * @config {Number} [connectionTimeout=0] The duration the socket should stay alive with no activity in milliseconds. 0 = Disabled.
 */
function Connection (options) {
	if(false === (this instanceof Connection)) {
        return new Connection(options);
    }
	this.options = {
		cert: 'cert.pem',
		certData: null,
		key: 'key.pem',
		keyData: null,
		ca: null,
		pfx: null,
		pfxData: null,
		passphrase: null,
		gateway: 'gateway.push.apple.com',
		port: 2195,
		rejectUnauthorized: true,
		enhanced: true,
		errorCallback: undefined,
		cacheLength: 100,
		autoAdjustCache: true,
		connectionTimeout: 0
	};

	util.extend(this.options, options);

	this.certData = null;
	this.keyData  = null;
	this.pfxData = null;

	this.deferredInitialize = null;
	this.deferredConnection = null;

	this.currentId = 0;
	this.cachedNotifications = [];
	this.notificationBuffer  = [];

	events.EventEmitter.call(this);
};

sysu.inherits(Connection, events.EventEmitter);

/**
 * @private
 */
Connection.prototype.checkInitialized = function () {
	if ((this.keyData && this.certData) || this.pfxData) {
		this.deferredInitialize.resolve();
	}
};

/**
 * You should never need to call this method, initialisation and connection is handled by {@link Connection#sendNotification}
 * @private
 */
Connection.prototype.initialize = function () {
	if (this.deferredInitialize) {
		return this.deferredInitialize.promise;
	}

	debug("Initialising module");
	this.deferredInitialize = q.defer();

	if(this.options.pfx != null || this.options.pfxData != null) {
		if(this.options.pxfData) {
			this.pfxData = this.options.pfxData;
		}
		else {
			fs.readFile(this.options.pfx, function (err, data) {
				if (err) {
					this.deferredInitialize.reject(err);
					return;
				}
				this.pfxData = data;
				this.checkInitialized();
			}.bind(this));
		}
	}
	else {
		if (this.options.certData) {
			this.certData = this.options.certData;
		}
		else {
			fs.readFile(this.options.cert, function (err, data) {
				if (err) {
					this.deferredInitialize.reject(err);
					return;
				}
				this.certData = data.toString();
				this.checkInitialized();
			}.bind(this));
		}

		if (this.options.keyData) {
			this.keyData = this.options.keyData;
		}
		else {
			fs.readFile(this.options.key, function (err, data) {
				if (err) {
					this.deferredInitialize.reject(err);
					return;
				}
				this.keyData = data.toString();
				this.checkInitialized();
			}.bind(this));
		}
	}

	this.checkInitialized();
	return this.deferredInitialize.promise;
};

/**
 * You should never need to call this method, initialisation and connection is handled by {@link Connection#sendNotification}
 * @private
 */
Connection.prototype.connect = function () {
	if (this.deferredConnection) {
		return this.deferredConnection.promise;
	}

	debug("Initialising connection");
	this.deferredConnection = q.defer();
	this.initialize().then(function () {
		var socketOptions = {};

		if(this.pfxData) {
			socketOptions.pfx = this.pfxData;
		}
		else {
			socketOptions.key = this.keyData;
			socketOptions.cert = this.certData;
			socketOptions.ca = this.options.ca;
		}
		socketOptions.passphrase = this.options.passphrase;
		socketOptions.rejectUnauthorized = this.options.rejectUnauthorized;

		this.socket = tls.connect(
			this.options['port'],
			this.options['gateway'],
			socketOptions,
			function () {
				debug("Connection established");
				this.emit('connected');
				this.deferredConnection.resolve();
			}.bind(this));

		this.socket.setNoDelay(false);
		this.socket.setTimeout(this.options.connectionTimeout);

		this.socket.on("error", this.errorOccurred.bind(this));
		this.socket.on("timeout", this.socketTimeout.bind(this));
		this.socket.on("data", this.handleTransmissionError.bind(this));
		this.socket.on("drain", this.socketDrained.bind(this));
		this.socket.on("clientError", this.errorOccurred.bind(this));
		this.socket.on("end", this.destroyConnection.bind(this));
		this.socket.once("close", this.restartConnection.bind(this));
	}.bind(this)).fail(function (error) {
		debug("Module initialisation error:", error);
		this.emit('error', error);
		this.deferredConnection.reject(error);
		this.deferredConnection = null;
	}.bind(this));

	return this.deferredConnection.promise;
};

/**
 * @private
 */
Connection.prototype.errorOccurred = function(err) {
	debug("Socket error occurred", err);
	this.emit('socketError', err);
	if(!this.deferredConnection.promise.isResolved()) {
		this.deferredConnection.reject(err);
	}
	else {
		this.raiseError(err, null);
	}
	this.destroyConnection();
};

/**
 * @private
 */
Connection.prototype.socketDrained = function() {
	debug("Socket drained");
	if(this.options.enhanced) {
		var notification = this.cachedNotifications[this.cachedNotifications.length - 1];
		this.emit('transmitted', notification);
	}
	if (this.socket && (this.socket.socket.bufferSize != 0 || !this.socket.writable)) {
		return;
	}
	debug("Socket writeable");
	if (this.notificationBuffer.length > 0) {
		debug("Sending notification from buffer");
		this.sendNotification(this.notificationBuffer.shift());
	}
};

/**
 * @private
 */
 Connection.prototype.socketTimeout = function() {
 	debug("Socket timeout");
 	this.emit('timeout');
 	this.socket.end();
 };

/**
 * @private
 */
Connection.prototype.destroyConnection = function() {
	debug("Destroying connection");
	if (this.socket) {
		this.socket.destroy();
	}
};

/**
 * @private
 */
Connection.prototype.restartConnection = function() {
	debug("Restarting connection");
	if (this.socket) {
		this.socket.removeAllListeners();
	}

	if(!this.deferredConnection.promise.isResolved()) {
		debug("Connection error occurred before TLS Handshake");
		this.deferredConnection.reject(new Error("Unable to connect"));
	}

	this.emit('disconnected');

	this.socket = undefined;
	this.deferredConnection = undefined;

	if (this.notificationBuffer.length) {
		debug("Notification queue has %d items, resending the first", this.notificationBuffer.length);
		this.sendNotification(this.notificationBuffer.shift());
	}
};

/**
 * @private
 */
Connection.prototype.bufferNotification = function (notification) {
	this.notificationBuffer.push(notification);
};

/**
 * @private
 */
Connection.prototype.cacheNotification = function (notification) {
	this.cachedNotifications.push(notification);
	if (this.cachedNotifications.length > this.options.cacheLength) {
		debug("Clearing notification %d from the cache", this.cachedNotifications[0]['_uid']);
		this.cachedNotifications.shift();
	}
};

/**
 * @private
 */
Connection.prototype.handleTransmissionError = function (data) {
	if (data[0] == 8) {
		if (!this.options.enhanced) {
			return;
		}

		var errorCode = data[1];
		var identifier = data.readUInt32BE(2);
		var notification = undefined;
		var foundNotification = false;
		var temporaryCache = [];

		debug("Notification %d caused an error: %d", identifier, errorCode);

		while (this.cachedNotifications.length) {
			notification = this.cachedNotifications.shift();
			if (notification['_uid'] == identifier) {
				foundNotification = true;
				break;
			}
			temporaryCache.push(notification);
		}

		if (foundNotification) {
			while (temporaryCache.length) {
				temporaryCache.shift();
			}
			this.emit('transmissionError', errorCode, notification);
			this.raiseError(errorCode, notification);
		}
		else {
			this.cachedNotifications = temporaryCache;

			var differentialSize = this.cachedNotifications[0]['_uid'] - notification['_uid']
			this.emit('cacheTooSmall', differentialSize);
			if(this.options.autoAdjustCache) {
				this.options.cacheLength += differentialSize * 2;
			}

			this.emit('transmissionError', Errors["none"], null);
			this.raiseError(errorCode, null);
		}

		var count = this.cachedNotifications.length;
		debug("Buffering %d notifications", count);
		for (var i = 0; i < count; ++i) {
			notification = this.cachedNotifications.shift();
			this.bufferNotification(notification);
		}

		this.destroyConnection();
	}
};

/**
 * @private
 */
Connection.prototype.raiseError = function(errorCode, notification) {
	debug("Raising error:", errorCode, notification);
	if(errorCode instanceof Error) {
		debug("Error occurred with trace:", errorCode.stack);
	}
	if (typeof this.options.errorCallback == 'function') {
		this.options.errorCallback(errorCode, notification);
	}
};

/**
 * Send a notification to the APN service
 * @param {Notification} notification The notification object to be sent
 */
Connection.prototype.sendNotification = function (notification) {
	var token = notification.device.token;

	var encoding = notification.encoding || 'utf8';
	var message = JSON.stringify(notification);
	var messageLength = Buffer.byteLength(message, encoding);
	var position = 0;
	var data;

	if (token === undefined) {
		process.nextTick(function () {
			this.raiseError(Errors['missingDeviceToken'], notification);
		}.bind(this));
		return Errors['missingDeviceToken'];
	}
	if (messageLength > 255) {
		process.nextTick(function () {
			this.raiseError(Errors['invalidPayloadSize'], notification);
		}.bind(this));
		return Errors['invalidPayloadSize'];
	}

	this.connect().then(function() {
		debug("Sending notification");
		if (!this.socket || this.socket.socket.bufferSize !== 0 || !this.socket.writable) {
			this.bufferNotification(notification);
			return;
		}

		notification._uid = this.currentId++;
		if (this.currentId > 0xffffffff) {
			this.currentId = 0;
		}

		if (this.options.enhanced) {
			data = new Buffer(1 + 4 + 4 + 2 + token.length + 2 + messageLength);
			// Command
			data[position] = 1;
			position++;

			// Identifier
			data.writeUInt32BE(notification._uid, position);
			position += 4;

			// Expiry
			data.writeUInt32BE(notification.expiry, position);
			position += 4;

			this.cacheNotification(notification);
		}
		else {
			data = new Buffer(1 + 2 + token.length + 2 + messageLength);
			//Command
			data[position] = 0;
			position++;
		}
		// Token Length
		data.writeUInt16BE(token.length, position);
		position += 2;
		// Device Token
		position += token.copy(data, position, 0);
		// Payload Length
		data.writeUInt16BE(messageLength, position);
		position += 2;
		//Payload
		position += data.write(message, position, encoding);

		if(this.socket.write(data)) {
			this.socketDrained();
		}
	}.bind(this)).fail(function (error) {
		this.bufferNotification(notification);
		this.raiseError(error, notification);
	}.bind(this));

	return 0;
};

module.exports = Connection;
<commit_msg>Added a check to ensure the error handling doesn't fail when there are no cached notifications.<commit_after>var Errors = require('./errors');

var fs   = require('fs');
var q    = require('q');
var tls  = require('tls');
var sysu = require('util');
var util = require('./util');
var events = require('events');
var debug = function() {};
if(process.env.DEBUG) {
	try {
		debug = require('debug')('apn');
	}
	catch (e) {
		console.log("Notice: 'debug' module is not available. This should be installed with `npm install debug` to enable debug messages", e);
		debug = function() {};
	}
}

/**
 * Create a new connection to the APN service.
 * @constructor
 * @param {Object} [options]
 * @config {String} [cert="cert.pem"] The filename of the connection certificate to load from disk
 * @config {Buffer|String} [certData] The certificate data. If supplied, will be used instead of loading from disk.
 * @config {String} [key="key.pem"] The filename of the connection key to load from disk
 * @config {Buffer|String} [keyData] The key data. If supplied will be used instead of loading from disk.
 * @config {Buffer[]|String[]} [ca] An array of strings or Buffers of trusted certificates. If this is omitted several well known "root" CAs will be used, like VeriSign. - You may need to use this as some environments don't include the CA used by Apple.
 * @config {String} [pfx] File path for private key, certificate and CA certs in PFX or PKCS12 format. If supplied will be used instead of certificate and key above
 * @config {Buffer|String} [pfxData] PFX or PKCS12 format data containing the private key, certificate and CA certs. If supplied will be used instead of loading from disk.
 * @config {String} [passphrase] The passphrase for the connection key, if required
 * @config {String} [gateway="gateway.push.apple.com"] The gateway server to connect to.
 * @config {Number} [port=2195] Gateway port
 * @config {Boolean} [rejectUnauthorized=true] Reject Unauthorized property to be passed through to tls.connect()
 * @config {Boolean} [enhanced=true] Whether to use the enhanced notification format (recommended)
 * @config {Function} [errorCallback] A callback which accepts 2 parameters (err, notification). Recommended when using enhanced format.
 * @config {Number} [cacheLength=100] Number of notifications to cache for error purposes (See Readme)
 * @config {Boolean} [autoAdjustCache=false] Whether the cache should grow in response to messages being lost after errors. (Will still emit a 'cacheTooSmall' event)
 * @config {Number} [connectionTimeout=0] The duration the socket should stay alive with no activity in milliseconds. 0 = Disabled.
 */
function Connection (options) {
	if(false === (this instanceof Connection)) {
        return new Connection(options);
    }
	this.options = {
		cert: 'cert.pem',
		certData: null,
		key: 'key.pem',
		keyData: null,
		ca: null,
		pfx: null,
		pfxData: null,
		passphrase: null,
		gateway: 'gateway.push.apple.com',
		port: 2195,
		rejectUnauthorized: true,
		enhanced: true,
		errorCallback: undefined,
		cacheLength: 100,
		autoAdjustCache: true,
		connectionTimeout: 0
	};

	util.extend(this.options, options);

	this.certData = null;
	this.keyData  = null;
	this.pfxData = null;

	this.deferredInitialize = null;
	this.deferredConnection = null;

	this.currentId = 0;
	this.cachedNotifications = [];
	this.notificationBuffer  = [];

	events.EventEmitter.call(this);
};

sysu.inherits(Connection, events.EventEmitter);

/**
 * @private
 */
Connection.prototype.checkInitialized = function () {
	if ((this.keyData && this.certData) || this.pfxData) {
		this.deferredInitialize.resolve();
	}
};

/**
 * You should never need to call this method, initialisation and connection is handled by {@link Connection#sendNotification}
 * @private
 */
Connection.prototype.initialize = function () {
	if (this.deferredInitialize) {
		return this.deferredInitialize.promise;
	}

	debug("Initialising module");
	this.deferredInitialize = q.defer();

	if(this.options.pfx != null || this.options.pfxData != null) {
		if(this.options.pxfData) {
			this.pfxData = this.options.pfxData;
		}
		else {
			fs.readFile(this.options.pfx, function (err, data) {
				if (err) {
					this.deferredInitialize.reject(err);
					return;
				}
				this.pfxData = data;
				this.checkInitialized();
			}.bind(this));
		}
	}
	else {
		if (this.options.certData) {
			this.certData = this.options.certData;
		}
		else {
			fs.readFile(this.options.cert, function (err, data) {
				if (err) {
					this.deferredInitialize.reject(err);
					return;
				}
				this.certData = data.toString();
				this.checkInitialized();
			}.bind(this));
		}

		if (this.options.keyData) {
			this.keyData = this.options.keyData;
		}
		else {
			fs.readFile(this.options.key, function (err, data) {
				if (err) {
					this.deferredInitialize.reject(err);
					return;
				}
				this.keyData = data.toString();
				this.checkInitialized();
			}.bind(this));
		}
	}

	this.checkInitialized();
	return this.deferredInitialize.promise;
};

/**
 * You should never need to call this method, initialisation and connection is handled by {@link Connection#sendNotification}
 * @private
 */
Connection.prototype.connect = function () {
	if (this.deferredConnection) {
		return this.deferredConnection.promise;
	}

	debug("Initialising connection");
	this.deferredConnection = q.defer();
	this.initialize().then(function () {
		var socketOptions = {};

		if(this.pfxData) {
			socketOptions.pfx = this.pfxData;
		}
		else {
			socketOptions.key = this.keyData;
			socketOptions.cert = this.certData;
			socketOptions.ca = this.options.ca;
		}
		socketOptions.passphrase = this.options.passphrase;
		socketOptions.rejectUnauthorized = this.options.rejectUnauthorized;

		this.socket = tls.connect(
			this.options['port'],
			this.options['gateway'],
			socketOptions,
			function () {
				debug("Connection established");
				this.emit('connected');
				this.deferredConnection.resolve();
			}.bind(this));

		this.socket.setNoDelay(false);
		this.socket.setTimeout(this.options.connectionTimeout);

		this.socket.on("error", this.errorOccurred.bind(this));
		this.socket.on("timeout", this.socketTimeout.bind(this));
		this.socket.on("data", this.handleTransmissionError.bind(this));
		this.socket.on("drain", this.socketDrained.bind(this));
		this.socket.on("clientError", this.errorOccurred.bind(this));
		this.socket.on("end", this.destroyConnection.bind(this));
		this.socket.once("close", this.restartConnection.bind(this));
	}.bind(this)).fail(function (error) {
		debug("Module initialisation error:", error);
		this.emit('error', error);
		this.deferredConnection.reject(error);
		this.deferredConnection = null;
	}.bind(this));

	return this.deferredConnection.promise;
};

/**
 * @private
 */
Connection.prototype.errorOccurred = function(err) {
	debug("Socket error occurred", err);
	this.emit('socketError', err);
	if(!this.deferredConnection.promise.isResolved()) {
		this.deferredConnection.reject(err);
	}
	else {
		this.raiseError(err, null);
	}
	this.destroyConnection();
};

/**
 * @private
 */
Connection.prototype.socketDrained = function() {
	debug("Socket drained");
	if(this.options.enhanced) {
		var notification = this.cachedNotifications[this.cachedNotifications.length - 1];
		this.emit('transmitted', notification);
	}
	if (this.socket && (this.socket.socket.bufferSize != 0 || !this.socket.writable)) {
		return;
	}
	debug("Socket writeable");
	if (this.notificationBuffer.length > 0) {
		debug("Sending notification from buffer");
		this.sendNotification(this.notificationBuffer.shift());
	}
};

/**
 * @private
 */
 Connection.prototype.socketTimeout = function() {
 	debug("Socket timeout");
 	this.emit('timeout');
 	this.socket.end();
 };

/**
 * @private
 */
Connection.prototype.destroyConnection = function() {
	debug("Destroying connection");
	if (this.socket) {
		this.socket.destroy();
	}
};

/**
 * @private
 */
Connection.prototype.restartConnection = function() {
	debug("Restarting connection");
	if (this.socket) {
		this.socket.removeAllListeners();
	}

	if(!this.deferredConnection.promise.isResolved()) {
		debug("Connection error occurred before TLS Handshake");
		this.deferredConnection.reject(new Error("Unable to connect"));
	}

	this.emit('disconnected');

	this.socket = undefined;
	this.deferredConnection = undefined;

	if (this.notificationBuffer.length) {
		debug("Notification queue has %d items, resending the first", this.notificationBuffer.length);
		this.sendNotification(this.notificationBuffer.shift());
	}
};

/**
 * @private
 */
Connection.prototype.bufferNotification = function (notification) {
	this.notificationBuffer.push(notification);
};

/**
 * @private
 */
Connection.prototype.cacheNotification = function (notification) {
	this.cachedNotifications.push(notification);
	if (this.cachedNotifications.length > this.options.cacheLength) {
		debug("Clearing notification %d from the cache", this.cachedNotifications[0]['_uid']);
		this.cachedNotifications.shift();
	}
};

/**
 * @private
 */
Connection.prototype.handleTransmissionError = function (data) {
	if (data[0] == 8) {
		if (!this.options.enhanced) {
			return;
		}

		var errorCode = data[1];
		var identifier = data.readUInt32BE(2);
		var notification = undefined;
		var foundNotification = false;
		var temporaryCache = [];

		debug("Notification %d caused an error: %d", identifier, errorCode);

		while (this.cachedNotifications.length) {
			notification = this.cachedNotifications.shift();
			if (notification['_uid'] == identifier) {
				foundNotification = true;
				break;
			}
			temporaryCache.push(notification);
		}

		if (foundNotification) {
			while (temporaryCache.length) {
				temporaryCache.shift();
			}
			this.emit('transmissionError', errorCode, notification);
			this.raiseError(errorCode, notification);
		}
		else {
			this.cachedNotifications = temporaryCache;

			if(this.cachedNotifications.length > 0) {
				var differentialSize = this.cachedNotifications[0]['_uid'] - notification['_uid']
				this.emit('cacheTooSmall', differentialSize);
				if(this.options.autoAdjustCache) {
					this.options.cacheLength += differentialSize * 2;
				}
			}

			this.emit('transmissionError', Errors["none"], null);
			this.raiseError(errorCode, null);
		}

		var count = this.cachedNotifications.length;
		debug("Buffering %d notifications", count);
		for (var i = 0; i < count; ++i) {
			notification = this.cachedNotifications.shift();
			this.bufferNotification(notification);
		}

		this.destroyConnection();
	}
};

/**
 * @private
 */
Connection.prototype.raiseError = function(errorCode, notification) {
	debug("Raising error:", errorCode, notification);
	if(errorCode instanceof Error) {
		debug("Error occurred with trace:", errorCode.stack);
	}
	if (typeof this.options.errorCallback == 'function') {
		this.options.errorCallback(errorCode, notification);
	}
};

/**
 * Send a notification to the APN service
 * @param {Notification} notification The notification object to be sent
 */
Connection.prototype.sendNotification = function (notification) {
	var token = notification.device.token;

	var encoding = notification.encoding || 'utf8';
	var message = JSON.stringify(notification);
	var messageLength = Buffer.byteLength(message, encoding);
	var position = 0;
	var data;

	if (token === undefined) {
		process.nextTick(function () {
			this.raiseError(Errors['missingDeviceToken'], notification);
		}.bind(this));
		return Errors['missingDeviceToken'];
	}
	if (messageLength > 255) {
		process.nextTick(function () {
			this.raiseError(Errors['invalidPayloadSize'], notification);
		}.bind(this));
		return Errors['invalidPayloadSize'];
	}

	this.connect().then(function() {
		debug("Sending notification");
		if (!this.socket || this.socket.socket.bufferSize !== 0 || !this.socket.writable) {
			this.bufferNotification(notification);
			return;
		}

		notification._uid = this.currentId++;
		if (this.currentId > 0xffffffff) {
			this.currentId = 0;
		}

		if (this.options.enhanced) {
			data = new Buffer(1 + 4 + 4 + 2 + token.length + 2 + messageLength);
			// Command
			data[position] = 1;
			position++;

			// Identifier
			data.writeUInt32BE(notification._uid, position);
			position += 4;

			// Expiry
			data.writeUInt32BE(notification.expiry, position);
			position += 4;

			this.cacheNotification(notification);
		}
		else {
			data = new Buffer(1 + 2 + token.length + 2 + messageLength);
			//Command
			data[position] = 0;
			position++;
		}
		// Token Length
		data.writeUInt16BE(token.length, position);
		position += 2;
		// Device Token
		position += token.copy(data, position, 0);
		// Payload Length
		data.writeUInt16BE(messageLength, position);
		position += 2;
		//Payload
		position += data.write(message, position, encoding);

		if(this.socket.write(data)) {
			this.socketDrained();
		}
	}.bind(this)).fail(function (error) {
		this.bufferNotification(notification);
		this.raiseError(error, notification);
	}.bind(this));

	return 0;
};

module.exports = Connection;
<|endoftext|>


<commit_before>
var
  // #57 - see why ipad needs to double click
  isiDevice = navigator.userAgent.match(/ip(hone|od|ad)/i),
  html5_audio,
  isMobile = true,
  listenEvent = isiDevice ? 'touchend' : 'click',
  ev = EvDa({start: '', name: '', station: '', ampm: '', day: []}),
  fullName = {
    sun: 'Sundays', mon: 'Mondays', tue: 'Tuesdays', wed: 'Wednesdays',
    thu: 'Thursdays', fri: 'Fridays', sat: 'Saturdays'
  },
  tpl = {},
  time_re = /^\s*(1[0-2]|[1-9])(:[0-5][0-9])?\s*([ap]m)?\s*$/i;

// local storage
function ls(key, value) {
  if (arguments.length == 1) {
    return localStorage[key] || false;
  } else {
    localStorage[key] = value;
  }
  return value;
}

function easy_bind(list) {
  _.each(list, function(what) {

    var node = document.querySelector('#' + what);

    if(!node) {
      node = document.querySelector('input[name="' + what + '"]');
      if(!node) {
        throw new Error("Can't find anything matching " + what);
      }
    }

    if(node.nodeName == 'INPUT') {

      $(node).on('blur focus change keyup', function() {
        ev(what, this.value, {node: this});
      });

      ev(what, function(val){
        node.value = val;
      });

    } else {
      $("a,button", node).on(listenEvent, function(){
        // This tricks stupid iDevices into not fucking around and screwing with the user.
        // (Requiring a user to tap twice to select anything.  WTF apple...)
        var mthis = this;

        setTimeout(function(){
          ev(what, mthis.getAttribute('data') || mthis.innerHTML);
        }, 0);
      });

      ev(what, function(val) {
        $("a", node).removeClass('selected');
        if(val) {
          $("a", node).filter(function(){return this.innerHTML == val}).addClass("selected");
          $("a[data='" + val + "']", node).addClass("selected");
        }
      });
    }
    ev.fire(what);
  });
}

function time_markers() {
  var right_now = new Date();

  return {
    current_hour: {
      human_time: 'the current hour',
      start_time: +date_diff(right_now, {minutes: 0}) / 1000,
      end_time: +date_diff(right_now, {minutes: 0, hours: "+1"}) / 1000
    },

    last_half_hour: {
      human_time: 'the previous half hour',
      start_time: +date_diff(right_now, {minutes: "- 30 - ts.getMinutes() % 30"}) / 1000,
      end_time: +date_diff(right_now, {minutes: "- ts.getMinutes() % 30"}) / 1000
    },

    current_half_hour: {
      human_time: 'the current half hour',
      start_time: +date_diff(right_now, {minutes: " - (ts.getMinutes() % 30)"}) / 1000,
      end_time: +date_diff(right_now, {minutes: " + 30 - (ts.getMinutes() % 30)"}) / 1000
    }
  };
}

function easy_sync(list) {
  _.each(list, function(what) {
    if(ls(what)) {
      ev(what, ls(what));
    }
    ev.after(what, function(value) {
      ls(what, value);
    });
  });
  return ev('');
}

function station_select() {
  $("#station-preselect").slideUp();
  $("#station").slideDown();
}

function htmldo(what) {
  // hyperlinking
  what = what.replace(/[a-z]+:\/\/[^\s^<]+/g, '$&');

  // paragrapherizing
  what = what.replace(/\n/g, '

');

  // finalerizing
  return '
' + what + '
';
}

//
// This is a python inspired way of doing things.
// change_map has datetime.timedelta syntax and operates
//
//  as an override if it's an integer
//  as an eval if it's a string (such as +1 or -1)
//
// Currently all we care about are
// hours and minutes.
//
// seconds and milliseconds are zeroed for us.
//
// It can be empty of course.
//
function date_diff(ts, change_map) {

  change_map = change_map || {};

  if( !('hours' in change_map) ) {
    change_map['hours'] = ts.getHours();
  } else if (change_map.hours.length) {
    // oh noes! The spirit of Douglas Crockford has now cursed my family!
    eval("change_map['hours'] = ts.getHours() " + change_map['hours']);
  }

  if( !('minutes' in change_map) ) {
    change_map['minutes'] = ts.getMinutes();
  } else if (change_map.minutes.length) {
    eval("change_map['minutes'] = ts.getMinutes() " + change_map['minutes']);
  }
  console.log(change_map);

  return new Date(
    ts.getFullYear(),
    ts.getMonth(),
    ts.getDate(),
    change_map.hours,
    change_map.minutes,
    0,
    0
  );
}

function do_random() {
  set_player(random_url());
}

function set_fallback(url, count) {
  $("#flash-widget").show();

  $f("flash-widget", "http://releases.flowplayer.org/swf/flowplayer-3.2.18.swf", {
    onError: function() {
      // this means that both the html5 and flash player failed ... so we
      // just move on to a new track.
      set_player(random_url());
    },
    clip: {
      url: url,
      provider: 'audio',
      live: true,
      autoPlay: count
    },
    plugins: {
      controls: {
        height: 30,
        fullscreen: false,
        autoHide: false
      },
      audio: {
        url: "flowplayer.audio-3.2.11.swf",
      }
    }
  });
}

function set_player(url) {
  if(!html5_audio) {
    html5_audio = document.getElementById('radio-control');
  }

  var local = self.audio_count ? self.audio_count : 0;

  $("#url").html(url);

  $("#flash-widget").hide();
  html5_audio.addEventListener('error', function() {
    $("#html5-widget").hide();
    set_fallback(url, local);
  });

  html5_audio.addEventListener('loadstart', function(){
    $("#html5-widget").show();
  });

  html5_audio.src = url;

  if(self.hasOwnProperty('audio_count')) {
    // Don't auto-play if it's the first
    if (audio_count > 0) {
      html5_audio.play();
    }

    audio_count ++;
  }

  return url;
}

function random_url(){
  var
    station = random.station(),
    day = random.day(),
    what_time = random.time(),
    duration = random.duration();

  //if(random.num() == 0) {
    return 'http://indycast.net/' + station + '/live/' + what_time;
  //} else {
  //  return 'http://indycast.net/' + [station, day, random.time(), duration].join('/');
  //}
}

$(function() {
  $("#template-list > script").each(function(){
    var name = this.id.split('-').pop();
    tpl[name] = _.template(this.innerHTML);
  });
});

// #30: Da Goog!
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');

ga('create', 'UA-28399789-2', 'auto');
ga('send', 'pageview');

// mixpanel
(function(e,b){if(!b.__SV){var a,f,i,g;window.mixpanel=b;b._i=[];b.init=function(a,e,d){function f(b,h){var a=h.split(".");2==a.length&&(b=b[a[0]],h=a[1]);b[h]=function(){b.push([h].concat(Array.prototype.slice.call(arguments,0)))}}var c=b;"undefined"!==typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user".split(" ");
for(g=0;g<i.length;g++)f(c,i[g]);b._i.push([a,e,d])};b.__SV=1.2;a=e.createElement("script");a.type="text/javascript";a.async=!0;a.src="undefined"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:"file:"===e.location.protocol&&"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)?"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js":"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";f=e.getElementsByTagName("script")[0];f.parentNode.insertBefore(a,f)}})(document,window.mixpanel||[]);
mixpanel.init("4a4a5eed185198ed77fb5ca1edf90a27");
<commit_msg>api access<commit_after>
var
  // #57 - see why ipad needs to double click
  isiDevice = navigator.userAgent.match(/ip(hone|od|ad)/i),
  html5_audio,
  isMobile = true,
  listenEvent = isiDevice ? 'touchend' : 'click',
  ev = EvDa({start: '', name: '', station: '', ampm: '', day: []}),
  fullName = {
    sun: 'Sundays', mon: 'Mondays', tue: 'Tuesdays', wed: 'Wednesdays',
    thu: 'Thursdays', fri: 'Fridays', sat: 'Saturdays'
  },
  tpl = {},
  time_re = /^\s*(1[0-2]|[1-9])(:[0-5][0-9])?\s*([ap]m)?\s*$/i;

// remote API
// first we have a generic promise harness
function stub() {
  var res = {};

  // Take the things to be stubbed out
  _.each(_.toArray(arguments), function(what) {

    // Assign each one to the map as a function
    res[what] = function(cb) {

      // If the arguments passed in is another function
      // then we put this in our callback list.
      if(_.isFunction(cb)) {
        res[what].cb.push(cb);

        // and return our primary object
        // for chaining
        return res;

      } else {
        // If the arguments passed in is not a function,
        // then this is data.  We go through the list
        // that we've constructed (if any) and call each
        // callback with the argument passed in - returning
        // the results as a list, if needed.
        return _.map(res[what].cb, function(cb_ix) {
          cb_ix(cb);
        });
      }
    }

    res[what].cb = [];

  });

  return res;
}

// Then we have a queue-able api call system
function do_remote(ep, what, verb, amd) {
  remote.lock = true;
  if(_.isArray(ep)) {
    ep = ep.join('/');
  }

  $[verb || 'post']('/' + ep, what, function(res) {
    if(res.result) {
      amd.then(res.data);
    } else {
      amd.fail(res);
    }
  }, 'json')
    .fail(amd.fail)
    .always(function(){
      remote.ix++;
      console.log('r:' + ep + (what === undefined ? "" : (":" + JSON.stringify(what))));
      remote.lock = false;
      amd.always();

      if(remote.queue.length) {
        var opts = remote.queue.shift();
        do_remote.apply(this, opts);
      }
    })
}

function remote_queue(ep, what, verb) {
  var amd = stub('then', 'fail', 'always');

  if(remote.lock) {
    remote.queue.push([ep, what, verb, amd]);
  } else {
    do_remote(ep, what, verb, amd);
  }

  return amd;
}

function remote(ep, what, verb) {
  var amd = stub('then', 'fail', 'always');

  do_remote(ep, what, verb, amd);

  return amd;
}

remote.queue = [];
remote.lock = false;
remote.ix = 0;

// local storage
function ls(key, value) {
  if (arguments.length == 1) {
    return localStorage[key] || false;
  } else {
    localStorage[key] = value;
  }
  return value;
}

function easy_bind(list, instance) {
  if(!instance) {
    instance = ev;
  }

  _.each(list, function(what) {

    var node = document.querySelector('#' + what);

    if(!node) {
      node = document.querySelector('input[name="' + what + '"]');
      if(!node) {
        throw new Error("Can't find anything matching " + what);
      }
    }

    if(node.nodeName == 'INPUT') {

      $(node).on('blur focus change keyup', function() {
        instance(what, this.value, {node: this});
      });

      instance(what, function(val){
        node.value = val;
      });

    } else {
      $("a,button", node).on(listenEvent, function(){
        // This tricks stupid iDevices into not fucking around and screwing with the user.
        // (Requiring a user to tap twice to select anything.  WTF apple...)
        var mthis = this;

        setTimeout(function(){
          instance(what, mthis.getAttribute('data') || mthis.innerHTML);
        }, 0);
      });

      instance(what, function(val) {
        $("a", node).removeClass('selected');
        if(val) {
          $("a", node).filter(function(){return this.innerHTML == val}).addClass("selected");
          $("a[data='" + val + "']", node).addClass("selected");
        }
      });
    }

    instance.fire(what);
  });
}

function time_markers() {
  var right_now = new Date();

  return {
    current_hour: {
      human_time: 'the current hour',
      start_time: +date_diff(right_now, {minutes: 0}) / 1000,
      end_time: +date_diff(right_now, {minutes: 0, hours: "+1"}) / 1000
    },

    last_half_hour: {
      human_time: 'the previous half hour',
      start_time: +date_diff(right_now, {minutes: "- 30 - ts.getMinutes() % 30"}) / 1000,
      end_time: +date_diff(right_now, {minutes: "- ts.getMinutes() % 30"}) / 1000
    },

    current_half_hour: {
      human_time: 'the current half hour',
      start_time: +date_diff(right_now, {minutes: " - (ts.getMinutes() % 30)"}) / 1000,
      end_time: +date_diff(right_now, {minutes: " + 30 - (ts.getMinutes() % 30)"}) / 1000
    }
  };
}

function easy_sync(list) {
  _.each(list, function(what) {
    if(ls(what)) {
      ev(what, ls(what));
    }
    ev.after(what, function(value) {
      ls(what, value);
    });
  });
  return ev('');
}

function station_select() {
  $("#station-preselect").slideUp();
  $("#station").slideDown();
}

function htmldo(what) {
  // hyperlinking
  what = what.replace(/[a-z]+:\/\/[^\s^<]+/g, '$&');

  // paragrapherizing
  what = what.replace(/\n/g, '

');

  // finalerizing
  return '
' + what + '
';
}

//
// This is a python inspired way of doing things.
// change_map has datetime.timedelta syntax and operates
//
//  as an override if it's an integer
//  as an eval if it's a string (such as +1 or -1)
//
// Currently all we care about are
// hours and minutes.
//
// seconds and milliseconds are zeroed for us.
//
// It can be empty of course.
//
function date_diff(ts, change_map) {

  change_map = change_map || {};

  if( !('hours' in change_map) ) {
    change_map['hours'] = ts.getHours();
  } else if (change_map.hours.length) {
    // oh noes! The spirit of Douglas Crockford has now cursed my family!
    eval("change_map['hours'] = ts.getHours() " + change_map['hours']);
  }

  if( !('minutes' in change_map) ) {
    change_map['minutes'] = ts.getMinutes();
  } else if (change_map.minutes.length) {
    eval("change_map['minutes'] = ts.getMinutes() " + change_map['minutes']);
  }
  console.log(change_map);

  return new Date(
    ts.getFullYear(),
    ts.getMonth(),
    ts.getDate(),
    change_map.hours,
    change_map.minutes,
    0,
    0
  );
}

function do_random() {
  set_player(random_url());
}

function set_fallback(url, count) {
  $("#flash-widget").show();

  $f("flash-widget", "http://releases.flowplayer.org/swf/flowplayer-3.2.18.swf", {
    onError: function() {
      // this means that both the html5 and flash player failed ... so we
      // just move on to a new track.
      set_player(random_url());
    },
    clip: {
      url: url,
      provider: 'audio',
      live: true,
      autoPlay: count
    },
    plugins: {
      controls: {
        height: 30,
        fullscreen: false,
        autoHide: false
      },
      audio: {
        url: "flowplayer.audio-3.2.11.swf",
      }
    }
  });
}

function set_player(url) {
  if(!html5_audio) {
    html5_audio = document.getElementById('radio-control');
  }

  var local = self.audio_count ? self.audio_count : 0;

  $("#url").html(url);

  $("#flash-widget").hide();
  html5_audio.addEventListener('error', function() {
    $("#html5-widget").hide();
    set_fallback(url, local);
  });

  html5_audio.addEventListener('loadstart', function(){
    $("#html5-widget").show();
  });

  html5_audio.src = url;

  if(self.hasOwnProperty('audio_count')) {
    // Don't auto-play if it's the first
    if (audio_count > 0) {
      html5_audio.play();
    }

    audio_count ++;
  }

  return url;
}

function api(ep, params) {
  $.post(ep, params, function(res) {
    if(res.result) {

function random_url(){
  var
    station = random.station(),
    day = random.day(),
    what_time = random.time(),
    duration = random.duration();

  //if(random.num() == 0) {
    return 'http://indycast.net/' + station + '/live/' + what_time;
  //} else {
  //  return 'http://indycast.net/' + [station, day, random.time(), duration].join('/');
  //}
}

$(function() {
  $("#template-list > script").each(function(){
    var name = this.id.split('-').pop();
    tpl[name] = _.template(this.innerHTML);
  });
});

// #30: Da Goog!
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');

ga('create', 'UA-28399789-2', 'auto');
ga('send', 'pageview');

// mixpanel
(function(e,b){if(!b.__SV){var a,f,i,g;window.mixpanel=b;b._i=[];b.init=function(a,e,d){function f(b,h){var a=h.split(".");2==a.length&&(b=b[a[0]],h=a[1]);b[h]=function(){b.push([h].concat(Array.prototype.slice.call(arguments,0)))}}var c=b;"undefined"!==typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user".split(" ");
for(g=0;g<i.length;g++)f(c,i[g]);b._i.push([a,e,d])};b.__SV=1.2;a=e.createElement("script");a.type="text/javascript";a.async=!0;a.src="undefined"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:"file:"===e.location.protocol&&"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)?"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js":"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";f=e.getElementsByTagName("script")[0];f.parentNode.insertBefore(a,f)}})(document,window.mixpanel||[]);
mixpanel.init("4a4a5eed185198ed77fb5ca1edf90a27");
<|endoftext|>


<commit_before>var assert = require("assert");
var linesModule = require("./lines");
var types = require("./types");
var getFieldValue = types.getFieldValue;
var Node = types.namedTypes.Node;
var Expression = types.namedTypes.Expression;
var util = require("./util");
var comparePos = util.comparePos;
var NodePath = types.NodePath;
var isObject = types.builtInTypes.object;
var isArray = types.builtInTypes.array;
var isString = types.builtInTypes.string;

function Patcher(lines) {
    assert.ok(this instanceof Patcher);
    assert.ok(lines instanceof linesModule.Lines);

    var self = this,
        replacements = [];

    self.replace = function(loc, lines) {
        if (isString.check(lines))
            lines = linesModule.fromString(lines);

        replacements.push({
            lines: lines,
            start: loc.start,
            end: loc.end
        });
    };

    self.get = function(loc) {
        // If no location is provided, return the complete Lines object.
        loc = loc || {
            start: { line: 1, column: 0 },
            end: { line: lines.length,
                   column: lines.getLineLength(lines.length) }
        };

        var sliceFrom = loc.start,
            toConcat = [];

        function pushSlice(from, to) {
            assert.ok(comparePos(from, to) <= 0);
            toConcat.push(lines.slice(from, to));
        }

        replacements.sort(function(a, b) {
            return comparePos(a.start, b.start);
        }).forEach(function(rep) {
            if (comparePos(sliceFrom, rep.start) > 0) {
                // Ignore nested replacement ranges.
            } else {
                pushSlice(sliceFrom, rep.start);
                toConcat.push(rep.lines);
                sliceFrom = rep.end;
            }
        });

        pushSlice(sliceFrom, loc.end);

        return linesModule.concat(toConcat);
    };
}
exports.Patcher = Patcher;

exports.getReprinter = function(path) {
    assert.ok(path instanceof NodePath);

    // Make sure that this path refers specifically to a Node, rather than
    // some non-Node subproperty of a Node.
    if (path.node !== path.value)
        return;

    var orig = path.node.original;
    var origLoc = orig && orig.loc;
    var lines = origLoc && origLoc.lines;
    var reprints = [];

    if (!lines || !findReprints(path, reprints))
        return;

    return function(print) {
        var patcher = new Patcher(lines);

        reprints.forEach(function(reprint) {
            var old = reprint.oldPath.value;
            patcher.replace(
                old.loc,
                print(reprint.newPath).indentTail(old.loc.indent)
            );
        });

        return patcher.get(origLoc).indentTail(-orig.loc.indent);
    };
};

function findReprints(newPath, reprints) {
    var newNode = newPath.node;
    Node.assert(newNode);

    var oldNode = newNode.original;
    Node.assert(oldNode);

    assert.deepEqual(reprints, []);

    if (newNode.type !== oldNode.type) {
        return false;
    }

    var oldPath = new NodePath(oldNode);
    var canReprint = findChildReprints(newPath, oldPath, reprints);

    if (!canReprint) {
        // Make absolutely sure the calling code does not attempt to reprint
        // any nodes.
        reprints.length = 0;
    }

    return canReprint;
}

function findAnyReprints(newPath, oldPath, reprints) {
    var newNode = newPath.value;
    var oldNode = oldPath.value;

    if (newNode === oldNode)
        return true;

    if (isArray.check(newNode))
        return findArrayReprints(newPath, oldPath, reprints);

    if (isObject.check(newNode))
        return findObjectReprints(newPath, oldPath, reprints);

    return false;
}

function findArrayReprints(newPath, oldPath, reprints) {
    var newNode = newPath.value;
    var oldNode = oldPath.value;
    isArray.assert(newNode);
    var len = newNode.length;

    if (!(isArray.check(oldNode) &&
          oldNode.length === len))
        return false;

    for (var i = 0; i < len; ++i)
        if (!findAnyReprints(newPath.get(i), oldPath.get(i), reprints))
            return false;

    return true;
}

function findObjectReprints(newPath, oldPath, reprints) {
    var newNode = newPath.value;
    isObject.assert(newNode);

    var oldNode = oldPath.value;
    if (!isObject.check(oldNode))
        return false;

    if (Node.check(newNode)) {
        if (!Node.check(oldNode)) {
            return false;
        }

        // Here we need to decide whether the reprinted code for newNode
        // is appropriate for patching into the location of oldNode.

        if (newNode.type === oldNode.type) {
            var childReprints = [];

            if (findChildReprints(newPath, oldPath, childReprints)) {
                reprints.push.apply(reprints, childReprints);
            } else {
                reprints.push({
                    newPath: newPath,
                    oldPath: oldPath
                });
            }

            return true;
        }

        if (Expression.check(newNode) &&
            Expression.check(oldNode)) {

            // If both nodes are subtypes of Expression, then we should be
            // able to fill the location occupied by the old node with
            // code printed for the new node with no ill consequences.
            reprints.push({
                newPath: newPath,
                oldPath: oldPath
            });

            return true;
        }

        // The nodes have different types, and at least one of the types
        // is not a subtype of the Expression type, so we cannot safely
        // assume the nodes are syntactically interchangeable.
        return false;
    }

    return findChildReprints(newPath, oldPath, reprints);
}

function hasOpeningParen(oldPath) {
    assert.ok(oldPath instanceof NodePath);
    var oldNode = oldPath.value;
    var loc = oldNode.loc;
    var lines = loc && loc.lines;

    if (lines) {
        var pos = lines.skipSpaces(loc.start, true);
        if (pos && lines.prevPos(pos) && lines.charAt(pos) === "(") {
            var rootPath = oldPath;
            while (rootPath.parent)
                rootPath = rootPath.parent;
            // If we found an opening parenthesis but it occurred before
            // the start of the original subtree for this reprinting, then
            // we must not return true for hasOpeningParen(oldPath).
            return comparePos(rootPath.value.loc.start, pos) <= 0;
        }
    }

    return false;
}

function hasClosingParen(oldPath) {
    assert.ok(oldPath instanceof NodePath);
    var oldNode = oldPath.value;
    var loc = oldNode.loc;
    var lines = loc && loc.lines;

    if (lines) {
        var pos = lines.skipSpaces(loc.end);
        if (pos && lines.charAt(pos) === ")") {
            var rootPath = oldPath;
            while (rootPath.parent)
                rootPath = rootPath.parent;
            // If we found a closing parenthesis but it occurred after
            // the end of the original subtree for this reprinting, then
            // we must not return true for hasClosingParen(oldPath).
            return comparePos(pos, rootPath.value.loc.end) <= 0;
        }
    }

    return false;
}

function hasParens(oldPath) {
    // This logic can technically be fooled if the node has parentheses
    // but there are comments intervening between the parentheses and the
    // node. In such cases the node will be harmlessly wrapped in an
    // additional layer of parentheses.
    return hasOpeningParen(oldPath) && hasClosingParen(oldPath);
}

function findChildReprints(newPath, oldPath, reprints) {
    var newNode = newPath.value;
    var oldNode = oldPath.value;

    isObject.assert(newNode);
    isObject.assert(oldNode);

    // If this type of node cannot come lexically first in its enclosing
    // statement (e.g. a function expression or object literal), and it
    // seems to be doing so, then the only way we can ignore this problem
    // and save ourselves from falling back to the pretty printer is if an
    // opening parenthesis happens to precede the node.  For example,
    // (function(){ ... }()); does not need to be reprinted, even though
    // the FunctionExpression comes lexically first in the enclosing
    // ExpressionStatement and fails the hasParens test, because the
    // parent CallExpression passes the hasParens test. If we relied on
    // the path.needsParens() && !hasParens(oldNode) check below, the
    // absence of a closing parenthesis after the FunctionExpression would
    // trigger pretty-printing unnecessarily.
    if (!newPath.canBeFirstInStatement() &&
        newPath.firstInStatement() &&
        !hasOpeningParen(oldPath))
        return false;

    // If this node needs parentheses and will not be wrapped with
    // parentheses when reprinted, then return false to skip reprinting
    // and let it be printed generically.
    if (newPath.needsParens(true) && !hasParens(oldPath))
        return false;

    for (var k in util.getUnionOfKeys(newNode, oldNode)) {
        if (k === "loc")
            continue;

        if (!findAnyReprints(newPath.get(k), oldPath.get(k), reprints))
            return false;
    }

    return true;
}
<commit_msg>Avoid reprinting nodes without useful .loc information.<commit_after>var assert = require("assert");
var linesModule = require("./lines");
var types = require("./types");
var getFieldValue = types.getFieldValue;
var Node = types.namedTypes.Node;
var Expression = types.namedTypes.Expression;
var SourceLocation = types.namedTypes.SourceLocation;
var util = require("./util");
var comparePos = util.comparePos;
var NodePath = types.NodePath;
var isObject = types.builtInTypes.object;
var isArray = types.builtInTypes.array;
var isString = types.builtInTypes.string;

function Patcher(lines) {
    assert.ok(this instanceof Patcher);
    assert.ok(lines instanceof linesModule.Lines);

    var self = this,
        replacements = [];

    self.replace = function(loc, lines) {
        if (isString.check(lines))
            lines = linesModule.fromString(lines);

        replacements.push({
            lines: lines,
            start: loc.start,
            end: loc.end
        });
    };

    self.get = function(loc) {
        // If no location is provided, return the complete Lines object.
        loc = loc || {
            start: { line: 1, column: 0 },
            end: { line: lines.length,
                   column: lines.getLineLength(lines.length) }
        };

        var sliceFrom = loc.start,
            toConcat = [];

        function pushSlice(from, to) {
            assert.ok(comparePos(from, to) <= 0);
            toConcat.push(lines.slice(from, to));
        }

        replacements.sort(function(a, b) {
            return comparePos(a.start, b.start);
        }).forEach(function(rep) {
            if (comparePos(sliceFrom, rep.start) > 0) {
                // Ignore nested replacement ranges.
            } else {
                pushSlice(sliceFrom, rep.start);
                toConcat.push(rep.lines);
                sliceFrom = rep.end;
            }
        });

        pushSlice(sliceFrom, loc.end);

        return linesModule.concat(toConcat);
    };
}
exports.Patcher = Patcher;

exports.getReprinter = function(path) {
    assert.ok(path instanceof NodePath);

    // Make sure that this path refers specifically to a Node, rather than
    // some non-Node subproperty of a Node.
    if (path.node !== path.value)
        return;

    var orig = path.node.original;
    var origLoc = orig && orig.loc;
    var lines = origLoc && origLoc.lines;
    var reprints = [];

    if (!lines || !findReprints(path, reprints))
        return;

    return function(print) {
        var patcher = new Patcher(lines);

        reprints.forEach(function(reprint) {
            var old = reprint.oldPath.value;
            SourceLocation.assert(old.loc, true);
            patcher.replace(
                old.loc,
                print(reprint.newPath).indentTail(old.loc.indent)
            );
        });

        return patcher.get(origLoc).indentTail(-orig.loc.indent);
    };
};

function findReprints(newPath, reprints) {
    var newNode = newPath.node;
    Node.assert(newNode);

    var oldNode = newNode.original;
    Node.assert(oldNode);

    assert.deepEqual(reprints, []);

    if (newNode.type !== oldNode.type) {
        return false;
    }

    var oldPath = new NodePath(oldNode);
    var canReprint = findChildReprints(newPath, oldPath, reprints);

    if (!canReprint) {
        // Make absolutely sure the calling code does not attempt to reprint
        // any nodes.
        reprints.length = 0;
    }

    return canReprint;
}

function findAnyReprints(newPath, oldPath, reprints) {
    var newNode = newPath.value;
    var oldNode = oldPath.value;

    if (newNode === oldNode)
        return true;

    if (isArray.check(newNode))
        return findArrayReprints(newPath, oldPath, reprints);

    if (isObject.check(newNode))
        return findObjectReprints(newPath, oldPath, reprints);

    return false;
}

function findArrayReprints(newPath, oldPath, reprints) {
    var newNode = newPath.value;
    var oldNode = oldPath.value;
    isArray.assert(newNode);
    var len = newNode.length;

    if (!(isArray.check(oldNode) &&
          oldNode.length === len))
        return false;

    for (var i = 0; i < len; ++i)
        if (!findAnyReprints(newPath.get(i), oldPath.get(i), reprints))
            return false;

    return true;
}

function findObjectReprints(newPath, oldPath, reprints) {
    var newNode = newPath.value;
    isObject.assert(newNode);

    var oldNode = oldPath.value;
    if (!isObject.check(oldNode))
        return false;

    if (Node.check(newNode)) {
        if (!Node.check(oldNode)) {
            return false;
        }

        if (!oldNode.loc) {
            // If we have no .loc information for oldNode, then we won't
            // be able to reprint it.
            return false;
        }

        // Here we need to decide whether the reprinted code for newNode
        // is appropriate for patching into the location of oldNode.

        if (newNode.type === oldNode.type) {
            var childReprints = [];

            if (findChildReprints(newPath, oldPath, childReprints)) {
                reprints.push.apply(reprints, childReprints);
            } else {
                reprints.push({
                    newPath: newPath,
                    oldPath: oldPath
                });
            }

            return true;
        }

        if (Expression.check(newNode) &&
            Expression.check(oldNode)) {

            // If both nodes are subtypes of Expression, then we should be
            // able to fill the location occupied by the old node with
            // code printed for the new node with no ill consequences.
            reprints.push({
                newPath: newPath,
                oldPath: oldPath
            });

            return true;
        }

        // The nodes have different types, and at least one of the types
        // is not a subtype of the Expression type, so we cannot safely
        // assume the nodes are syntactically interchangeable.
        return false;
    }

    return findChildReprints(newPath, oldPath, reprints);
}

function hasOpeningParen(oldPath) {
    assert.ok(oldPath instanceof NodePath);
    var oldNode = oldPath.value;
    var loc = oldNode.loc;
    var lines = loc && loc.lines;

    if (lines) {
        var pos = lines.skipSpaces(loc.start, true);
        if (pos && lines.prevPos(pos) && lines.charAt(pos) === "(") {
            var rootPath = oldPath;
            while (rootPath.parent)
                rootPath = rootPath.parent;
            // If we found an opening parenthesis but it occurred before
            // the start of the original subtree for this reprinting, then
            // we must not return true for hasOpeningParen(oldPath).
            return comparePos(rootPath.value.loc.start, pos) <= 0;
        }
    }

    return false;
}

function hasClosingParen(oldPath) {
    assert.ok(oldPath instanceof NodePath);
    var oldNode = oldPath.value;
    var loc = oldNode.loc;
    var lines = loc && loc.lines;

    if (lines) {
        var pos = lines.skipSpaces(loc.end);
        if (pos && lines.charAt(pos) === ")") {
            var rootPath = oldPath;
            while (rootPath.parent)
                rootPath = rootPath.parent;
            // If we found a closing parenthesis but it occurred after
            // the end of the original subtree for this reprinting, then
            // we must not return true for hasClosingParen(oldPath).
            return comparePos(pos, rootPath.value.loc.end) <= 0;
        }
    }

    return false;
}

function hasParens(oldPath) {
    // This logic can technically be fooled if the node has parentheses
    // but there are comments intervening between the parentheses and the
    // node. In such cases the node will be harmlessly wrapped in an
    // additional layer of parentheses.
    return hasOpeningParen(oldPath) && hasClosingParen(oldPath);
}

function findChildReprints(newPath, oldPath, reprints) {
    var newNode = newPath.value;
    var oldNode = oldPath.value;

    isObject.assert(newNode);
    isObject.assert(oldNode);

    // If this type of node cannot come lexically first in its enclosing
    // statement (e.g. a function expression or object literal), and it
    // seems to be doing so, then the only way we can ignore this problem
    // and save ourselves from falling back to the pretty printer is if an
    // opening parenthesis happens to precede the node.  For example,
    // (function(){ ... }()); does not need to be reprinted, even though
    // the FunctionExpression comes lexically first in the enclosing
    // ExpressionStatement and fails the hasParens test, because the
    // parent CallExpression passes the hasParens test. If we relied on
    // the path.needsParens() && !hasParens(oldNode) check below, the
    // absence of a closing parenthesis after the FunctionExpression would
    // trigger pretty-printing unnecessarily.
    if (!newPath.canBeFirstInStatement() &&
        newPath.firstInStatement() &&
        !hasOpeningParen(oldPath))
        return false;

    // If this node needs parentheses and will not be wrapped with
    // parentheses when reprinted, then return false to skip reprinting
    // and let it be printed generically.
    if (newPath.needsParens(true) && !hasParens(oldPath))
        return false;

    for (var k in util.getUnionOfKeys(newNode, oldNode)) {
        if (k === "loc")
            continue;

        if (!findAnyReprints(newPath.get(k), oldPath.get(k), reprints))
            return false;
    }

    return true;
}
<|endoftext|>


<commit_before>var assert = require("assert");
var linesModule = require("./lines");
var types = require("./types");
var getFieldValue = types.getFieldValue;
var Node = types.namedTypes.Node;
var util = require("./util");
var comparePos = util.comparePos;
var NodePath = types.NodePath;
var isObject = types.builtInTypes.object;
var isArray = types.builtInTypes.array;
var isString = types.builtInTypes.string;

function Patcher(lines) {
    assert.ok(this instanceof Patcher);
    assert.ok(lines instanceof linesModule.Lines);

    var self = this,
        replacements = [];

    self.replace = function(loc, lines) {
        if (isString.check(lines))
            lines = linesModule.fromString(lines);

        replacements.push({
            lines: lines,
            start: loc.start,
            end: loc.end
        });
    };

    self.get = function(loc) {
        // If no location is provided, return the complete Lines object.
        loc = loc || {
            start: { line: 1, column: 0 },
            end: { line: lines.length,
                   column: lines.getLineLength(lines.length) }
        };

        var sliceFrom = loc.start,
            toConcat = [];

        function pushSlice(from, to) {
            assert.ok(comparePos(from, to) <= 0);
            toConcat.push(lines.slice(from, to));
        }

        replacements.sort(function(a, b) {
            return comparePos(a.start, b.start);
        }).forEach(function(rep) {
            if (comparePos(sliceFrom, rep.start) > 0) {
                // Ignore nested replacement ranges.
            } else {
                pushSlice(sliceFrom, rep.start);
                toConcat.push(rep.lines);
                sliceFrom = rep.end;
            }
        });

        pushSlice(sliceFrom, loc.end);

        return linesModule.concat(toConcat);
    };
}
exports.Patcher = Patcher;

exports.getReprinter = function(path) {
    assert.ok(path instanceof NodePath);

    // Make sure that this path refers specifically to a Node, rather than
    // some non-Node subproperty of a Node.
    if (path.node !== path.value)
        return;

    var orig = path.node.original;
    var origLoc = orig && orig.loc;
    var lines = origLoc && origLoc.lines;
    var reprints = [];

    if (!lines || !findReprints(path, reprints))
        return;

    return function(print) {
        var patcher = new Patcher(lines);

        reprints.forEach(function(reprint) {
            var old = reprint.oldPath.value;
            patcher.replace(
                old.loc,
                print(reprint.newPath).indentTail(
                    getIndent(old)));
        });

        return patcher.get(origLoc).indentTail(-getIndent(orig));
    };
};

// Get the indentation of the first ancestor node on a line with nothing
// before it but whitespace.
function getIndent(orig) {
    var naiveIndent = orig.loc.lines.getIndentAt(
        orig.loc.start.line);

    for (var loc, start, lines;
         orig &&
         (loc = orig.loc) &&
         (start = loc.start) &&
         (lines = loc.lines);
         orig = orig.parentNode)
    {
        if (lines.isPrecededOnlyByWhitespace(start)) {
            // The indent returned by lines.getIndentAt is the column of
            // the first non-space character in the line, but start.column
            // may fall before that character, as when a file begins with
            // whitespace but its start.column nevertheless must be 0.
            assert.ok(start.column <= lines.getIndentAt(start.line));
            return start.column;
        }
    }

    return naiveIndent;
}

function findReprints(newPath, reprints) {
    var newNode = newPath.node;
    Node.assert(newNode.original);
    assert.deepEqual(reprints, []);

    var oldPath = new NodePath(newNode.original);
    var canReprint = findChildReprints(newPath, oldPath, reprints);

    if (!canReprint) {
        // Make absolutely sure the calling code does not attempt to reprint
        // any nodes.
        reprints.length = 0;
    }

    return canReprint;
}

function findAnyReprints(newPath, oldPath, reprints) {
    var newNode = newPath.value;
    var oldNode = oldPath.value;

    if (newNode === oldNode)
        return true;

    if (isArray.check(newNode))
        return findArrayReprints(newPath, oldPath, reprints);

    if (isObject.check(newNode))
        return findObjectReprints(newPath, oldPath, reprints);

    return false;
}

function findArrayReprints(newPath, oldPath, reprints) {
    var newNode = newPath.value;
    var oldNode = oldPath.value;
    isArray.assert(newNode);
    var len = newNode.length;

    if (!(isArray.check(oldNode) &&
          oldNode.length === len))
        return false;

    for (var i = 0; i < len; ++i)
        if (!findAnyReprints(newPath.get(i), oldPath.get(i), reprints))
            return false;

    return true;
}

function findObjectReprints(newPath, oldPath, reprints) {
    var newNode = newPath.value;
    isObject.assert(newNode);

    var oldNode = oldPath.value;
    if (!isObject.check(oldNode))
        return false;

    var childReprints = [];
    var canReprintChildren = findChildReprints(newPath, oldPath, childReprints);

    if (Node.check(newNode)) {
        // TODO Decompose this check: if (!printTheSame(newNode, oldNode))
        if (newNode.type !== oldNode.type)
            return false;

        if (!canReprintChildren) {
            reprints.push({
                newPath: newPath,
                oldPath: oldPath
            });

            return true;
        }
    }

    reprints.push.apply(reprints, childReprints);
    return canReprintChildren;
}

function hasOpeningParen(oldPath) {
    assert.ok(oldPath instanceof NodePath);
    var oldNode = oldPath.value;
    var loc = oldNode.loc;
    var lines = loc && loc.lines;

    if (lines) {
        var pos = lines.skipSpaces(loc.start, true);
        if (pos && lines.prevPos(pos) && lines.charAt(pos) === "(")
            return true;
    }

    return false;
}

function hasClosingParen(oldPath) {
    assert.ok(oldPath instanceof NodePath);
    var oldNode = oldPath.value;
    var loc = oldNode.loc;
    var lines = loc && loc.lines;

    if (lines) {
        var pos = lines.skipSpaces(loc.end);
        if (pos && lines.charAt(pos) === ")")
            return true;
    }

    return false;
}

function hasParens(oldPath) {
    // This logic can technically be fooled if the node has parentheses
    // but there are comments intervening between the parentheses and the
    // node. In such cases the node will be harmlessly wrapped in an
    // additional layer of parentheses.
    return hasOpeningParen(oldPath) && hasClosingParen(oldPath);
}

function findChildReprints(newPath, oldPath, reprints) {
    var newNode = newPath.value;
    var oldNode = oldPath.value;

    isObject.assert(newNode);
    isObject.assert(oldNode);

    // If this node needs parentheses and will not be wrapped with
    // parentheses when reprinted, then return false to skip reprinting
    // and let it be printed generically.
    if (newPath.needsParens() && !hasParens(oldPath))
        return false;

    for (var k in util.getUnionOfKeys(newNode, oldNode)) {
        if (k === "loc")
            continue;

        if (!findAnyReprints(newPath.get(k), oldPath.get(k), reprints))
            return false;
    }

    return true;
}
<commit_msg>Fix parenthesization logic for function expressions and object literals.<commit_after>var assert = require("assert");
var linesModule = require("./lines");
var types = require("./types");
var getFieldValue = types.getFieldValue;
var Node = types.namedTypes.Node;
var util = require("./util");
var comparePos = util.comparePos;
var NodePath = types.NodePath;
var isObject = types.builtInTypes.object;
var isArray = types.builtInTypes.array;
var isString = types.builtInTypes.string;

function Patcher(lines) {
    assert.ok(this instanceof Patcher);
    assert.ok(lines instanceof linesModule.Lines);

    var self = this,
        replacements = [];

    self.replace = function(loc, lines) {
        if (isString.check(lines))
            lines = linesModule.fromString(lines);

        replacements.push({
            lines: lines,
            start: loc.start,
            end: loc.end
        });
    };

    self.get = function(loc) {
        // If no location is provided, return the complete Lines object.
        loc = loc || {
            start: { line: 1, column: 0 },
            end: { line: lines.length,
                   column: lines.getLineLength(lines.length) }
        };

        var sliceFrom = loc.start,
            toConcat = [];

        function pushSlice(from, to) {
            assert.ok(comparePos(from, to) <= 0);
            toConcat.push(lines.slice(from, to));
        }

        replacements.sort(function(a, b) {
            return comparePos(a.start, b.start);
        }).forEach(function(rep) {
            if (comparePos(sliceFrom, rep.start) > 0) {
                // Ignore nested replacement ranges.
            } else {
                pushSlice(sliceFrom, rep.start);
                toConcat.push(rep.lines);
                sliceFrom = rep.end;
            }
        });

        pushSlice(sliceFrom, loc.end);

        return linesModule.concat(toConcat);
    };
}
exports.Patcher = Patcher;

exports.getReprinter = function(path) {
    assert.ok(path instanceof NodePath);

    // Make sure that this path refers specifically to a Node, rather than
    // some non-Node subproperty of a Node.
    if (path.node !== path.value)
        return;

    var orig = path.node.original;
    var origLoc = orig && orig.loc;
    var lines = origLoc && origLoc.lines;
    var reprints = [];

    if (!lines || !findReprints(path, reprints))
        return;

    return function(print) {
        var patcher = new Patcher(lines);

        reprints.forEach(function(reprint) {
            var old = reprint.oldPath.value;
            patcher.replace(
                old.loc,
                print(reprint.newPath).indentTail(
                    getIndent(old)));
        });

        return patcher.get(origLoc).indentTail(-getIndent(orig));
    };
};

// Get the indentation of the first ancestor node on a line with nothing
// before it but whitespace.
function getIndent(orig) {
    var naiveIndent = orig.loc.lines.getIndentAt(
        orig.loc.start.line);

    for (var loc, start, lines;
         orig &&
         (loc = orig.loc) &&
         (start = loc.start) &&
         (lines = loc.lines);
         orig = orig.parentNode)
    {
        if (lines.isPrecededOnlyByWhitespace(start)) {
            // The indent returned by lines.getIndentAt is the column of
            // the first non-space character in the line, but start.column
            // may fall before that character, as when a file begins with
            // whitespace but its start.column nevertheless must be 0.
            assert.ok(start.column <= lines.getIndentAt(start.line));
            return start.column;
        }
    }

    return naiveIndent;
}

function findReprints(newPath, reprints) {
    var newNode = newPath.node;
    Node.assert(newNode.original);
    assert.deepEqual(reprints, []);

    var oldPath = new NodePath(newNode.original);
    var canReprint = findChildReprints(newPath, oldPath, reprints);

    if (!canReprint) {
        // Make absolutely sure the calling code does not attempt to reprint
        // any nodes.
        reprints.length = 0;
    }

    return canReprint;
}

function findAnyReprints(newPath, oldPath, reprints) {
    var newNode = newPath.value;
    var oldNode = oldPath.value;

    if (newNode === oldNode)
        return true;

    if (isArray.check(newNode))
        return findArrayReprints(newPath, oldPath, reprints);

    if (isObject.check(newNode))
        return findObjectReprints(newPath, oldPath, reprints);

    return false;
}

function findArrayReprints(newPath, oldPath, reprints) {
    var newNode = newPath.value;
    var oldNode = oldPath.value;
    isArray.assert(newNode);
    var len = newNode.length;

    if (!(isArray.check(oldNode) &&
          oldNode.length === len))
        return false;

    for (var i = 0; i < len; ++i)
        if (!findAnyReprints(newPath.get(i), oldPath.get(i), reprints))
            return false;

    return true;
}

function findObjectReprints(newPath, oldPath, reprints) {
    var newNode = newPath.value;
    isObject.assert(newNode);

    var oldNode = oldPath.value;
    if (!isObject.check(oldNode))
        return false;

    var childReprints = [];
    var canReprintChildren = findChildReprints(newPath, oldPath, childReprints);

    if (Node.check(newNode)) {
        // TODO Decompose this check: if (!printTheSame(newNode, oldNode))
        if (newNode.type !== oldNode.type)
            return false;

        if (!canReprintChildren) {
            reprints.push({
                newPath: newPath,
                oldPath: oldPath
            });

            return true;
        }
    }

    reprints.push.apply(reprints, childReprints);
    return canReprintChildren;
}

function hasOpeningParen(oldPath) {
    assert.ok(oldPath instanceof NodePath);
    var oldNode = oldPath.value;
    var loc = oldNode.loc;
    var lines = loc && loc.lines;

    if (lines) {
        var pos = lines.skipSpaces(loc.start, true);
        if (pos && lines.prevPos(pos) && lines.charAt(pos) === "(") {
            var rootPath = oldPath;
            while (rootPath.parent)
                rootPath = rootPath.parent;
            // If we found an opening parenthesis but it occurred before
            // the start of the original subtree for this reprinting, then
            // we must not return true for hasOpeningParen(oldPath).
            return comparePos(rootPath.value.loc.start, pos) <= 0;
        }
    }

    return false;
}

function hasClosingParen(oldPath) {
    assert.ok(oldPath instanceof NodePath);
    var oldNode = oldPath.value;
    var loc = oldNode.loc;
    var lines = loc && loc.lines;

    if (lines) {
        var pos = lines.skipSpaces(loc.end);
        if (pos && lines.charAt(pos) === ")") {
            var rootPath = oldPath;
            while (rootPath.parent)
                rootPath = rootPath.parent;
            // If we found a closing parenthesis but it occurred after
            // the end of the original subtree for this reprinting, then
            // we must not return true for hasClosingParen(oldPath).
            return comparePos(pos, rootPath.value.loc.end) <= 0;
        }
    }

    return false;
}

function hasParens(oldPath) {
    // This logic can technically be fooled if the node has parentheses
    // but there are comments intervening between the parentheses and the
    // node. In such cases the node will be harmlessly wrapped in an
    // additional layer of parentheses.
    return hasOpeningParen(oldPath) && hasClosingParen(oldPath);
}

function findChildReprints(newPath, oldPath, reprints) {
    var newNode = newPath.value;
    var oldNode = oldPath.value;

    isObject.assert(newNode);
    isObject.assert(oldNode);

    // If this type of node cannot come lexically first in its enclosing
    // statement (e.g. a function expression or object literal), and it
    // seems to be doing so, then the only way we can ignore this problem
    // and save ourselves from falling back to the pretty printer is if an
    // opening parenthesis happens to precede the node.  For example,
    // (function(){ ... }()); does not need to be reprinted, even though
    // the FunctionExpression comes lexically first in the enclosing
    // ExpressionStatement and fails the hasParens test, because the
    // parent CallExpression passes the hasParens test. If we relied on
    // the path.needsParens() && !hasParens(oldNode) check below, the
    // absence of a closing parenthesis after the FunctionExpression would
    // trigger pretty-printing unnecessarily.
    if (!newPath.canBeFirstInStatement() &&
        newPath.firstInStatement() &&
        !hasOpeningParen(oldPath))
        return false;

    // If this node needs parentheses and will not be wrapped with
    // parentheses when reprinted, then return false to skip reprinting
    // and let it be printed generically.
    if (newPath.needsParens(true) && !hasParens(oldPath))
        return false;

    for (var k in util.getUnionOfKeys(newNode, oldNode)) {
        if (k === "loc")
            continue;

        if (!findAnyReprints(newPath.get(k), oldPath.get(k), reprints))
            return false;
    }

    return true;
}
<|endoftext|>