isPure(expr, commentsStartPos) {
		if (!expr) return true;
		const result = this.hooks.isPure
			.for(expr.type)
			.call(expr, commentsStartPos);
		if (typeof result === "boolean") return result;
		switch (expr.type) {
			// TODO handle more cases
			case "ClassDeclaration":
			case "ClassExpression": {
				if (expr.body.type !== "ClassBody") return false;
				if (
					expr.superClass &&
					!this.isPure(expr.superClass, /** @type {Range} */ (expr.range)[0])
				) {
					return false;
				}
				const items = expr.body.body;
				return items.every(item => {
					if (item.type === "StaticBlock") {
						return false;
					}

					if (
						item.computed &&
						item.key &&
						!this.isPure(
							item.key,
							/** @type {Range} */
							(item.range)[0]
						)
					) {
						return false;
					}

					if (
						item.static &&
						item.value &&
						!this.isPure(
							item.value,
							item.key
								? /** @type {Range} */ (item.key.range)[1]
								: /** @type {Range} */ (item.range)[0]
						)
					) {
						return false;
					}

					if (
						expr.superClass &&
						item.type === "MethodDefinition" &&
						item.kind === "constructor"
					) {
						return false;
					}

					return true;
				});
			}

			case "FunctionDeclaration":
			case "FunctionExpression":
			case "ArrowFunctionExpression":
			case "ThisExpression":
			case "Literal":
			case "TemplateLiteral":
			case "Identifier":
			case "PrivateIdentifier":
				return true;

			case "VariableDeclaration":
				return expr.declarations.every(decl =>
					this.isPure(decl.init, /** @type {Range} */ (decl.range)[0])
				);

			case "ConditionalExpression":
				return (
					this.isPure(expr.test, commentsStartPos) &&
					this.isPure(
						expr.consequent,
						/** @type {Range} */ (expr.test.range)[1]
					) &&
					this.isPure(
						expr.alternate,
						/** @type {Range} */ (expr.consequent.range)[1]
					)
				);

			case "LogicalExpression":
				return (
					this.isPure(expr.left, commentsStartPos) &&
					this.isPure(expr.right, /** @type {Range} */ (expr.left.range)[1])
				);

			case "SequenceExpression":
				return expr.expressions.every(expr => {
					const pureFlag = this.isPure(expr, commentsStartPos);
					commentsStartPos = /** @type {Range} */ (expr.range)[1];
					return pureFlag;
				});

			case "CallExpression": {
				const pureFlag =
					/** @type {Range} */ (expr.range)[0] - commentsStartPos > 12 &&
					this.getComments([
						commentsStartPos,
						/** @type {Range} */ (expr.range)[0]
					]).some(
						comment =>
							comment.type === "Block" &&
							/^\s*(#|@)__PURE__\s*$/.test(comment.value)
					);
				if (!pureFlag) return false;
				commentsStartPos = /** @type {Range} */ (expr.callee.range)[1];
				return expr.arguments.every(arg => {
					if (arg.type === "SpreadElement") return false;
					const pureFlag = this.isPure(arg, commentsStartPos);
					commentsStartPos = /** @type {Range} */ (arg.range)[1];
					return pureFlag;
				});
			}
		}
		const evaluated = this.evaluateExpression(expr);
		return !evaluated.couldHaveSideEffects();
	}
@param {Expression | Declaration | PrivateIdentifier | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration | null | undefined} expr an expression
@param {number} commentsStartPos source position from which annotation comments are checked
@returns {boolean} true, when the expression is pure