Repository: rollup/rollup
File: src/ast/nodes/ChainExpression.ts (TypeScript)

Code before:
import type MagicString from 'magic-string';
import type { RenderOptions } from '../../utils/renderHelpers';
import type { DeoptimizableEntity } from '../DeoptimizableEntity';
import type { HasEffectsContext } from '../ExecutionContext';
import { SHARED_RECURSION_TRACKER } from '../utils/PathTracker';
import { EMPTY_PATH } from '../utils/PathTracker';
import type CallExpression from './CallExpression';
import type MemberExpression from './MemberExpression';
import type * as NodeType from './NodeType';
import type { LiteralValueOrUnknown } from './shared/Expression';
import { UnknownValue } from './shared/Expression';
import { NodeBase } from './shared/Node';

const unset = Symbol('unset');

export default class ChainExpression extends NodeBase implements DeoptimizableEntity {
	declare expression: CallExpression | MemberExpression;
	declare type: NodeType.tChainExpression;
	private objectValue: LiteralValueOrUnknown | typeof unset = unset;

	deoptimizeCache(): void {
		this.objectValue = UnknownValue;
	}

	getLiteralValueAtPath(): LiteralValueOrUnknown {
		if (this.getObjectValue() == null) return undefined;
		return UnknownValue;
	}

	hasEffects(context: HasEffectsContext): boolean {
		if (this.getObjectValue() == null) return false;
		return this.expression.hasEffects(context);
	}

	render(code: MagicString, options: RenderOptions): void {
		if (this.getObjectValue() == null) {
			code.remove(this.start, this.end);
		} else super.render(code, options);
	}

	private getObjectValue() {
		if (this.objectValue === unset) {
			let object =
				this.expression.type === 'CallExpression' ? this.expression.callee : this.expression.object;
			if (object.type === 'MemberExpression') object = (object as MemberExpression).object;

Review comment (suggestion):
This one is actually hard to cover in a test and I am fine if it is not covered. To hit this, you have to use a variable further up in the chain that is initialized as `undefined` but later gets another value assigned, invalidating the `object.getLiteralValueAtPath` in line 46.
The problem is to find a suitable "later". Sometimes it works to do this in a function but as I said, it is tricky.

Code after:
import type { DeoptimizableEntity } from '../DeoptimizableEntity';
import type { HasEffectsContext } from '../ExecutionContext';
import { SHARED_RECURSION_TRACKER } from '../utils/PathTracker';
import { EMPTY_PATH } from '../utils/PathTracker';
import type CallExpression from './CallExpression';
import type MemberExpression from './MemberExpression';
import type * as NodeType from './NodeType';
import type { LiteralValueOrUnknown } from './shared/Expression';
import { UnknownValue } from './shared/Expression';
import { NodeBase } from './shared/Node';

const unset = Symbol('unset');

export default class ChainExpression extends NodeBase implements DeoptimizableEntity {
	declare expression: CallExpression | MemberExpression;
	declare type: NodeType.tChainExpression;
	private objectValue: LiteralValueOrUnknown | typeof unset = unset;

	deoptimizeCache(): void {
		this.objectValue = UnknownValue;
	}

	getLiteralValueAtPath(): LiteralValueOrUnknown {
		if (this.getObjectValue() == null) return undefined;
		return UnknownValue;
	}

	hasEffects(context: HasEffectsContext): boolean {
		if (this.getObjectValue() == null) return false;
		return this.expression.hasEffects(context);
	}

	private getObjectValue() {
		if (this.objectValue === unset) {
			let object =
				this.expression.type === 'CallExpression' ? this.expression.callee : this.expression.object;
			if (object.type === 'MemberExpression') object = (object as MemberExpression).object;
			this.objectValue = object.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, this);
		}
		return this.objectValue;
	}
}