function read_attribute(parser) {
	const start = parser.index;

	if (parser.eat('{')) {
		parser.allow_whitespace();

		if (parser.eat('@attach')) {
			parser.require_whitespace();

			const expression = read_expression(parser);
			parser.allow_whitespace();
			parser.eat('}', true);

			/** @type {AST.AttachTag} */
			const attachment = {
				type: 'AttachTag',
				start,
				end: parser.index,
				expression,
				metadata: {
					expression: create_expression_metadata()
				}
			};

			return attachment;
		}

		if (parser.eat('...')) {
			const expression = read_expression(parser);

			parser.allow_whitespace();
			parser.eat('}', true);

			/** @type {AST.SpreadAttribute} */
			const spread = {
				type: 'SpreadAttribute',
				start,
				end: parser.index,
				expression,
				metadata: {
					expression: create_expression_metadata()
				}
			};

			return spread;
		} else {
			const value_start = parser.index;
			let name = parser.read_identifier();

			if (name === null) {
				if (
					parser.loose &&
					(parser.match('#') || parser.match('/') || parser.match('@') || parser.match(':'))
				) {
					// We're likely in an unclosed opening tag and did read part of a block.
					// Return null to not crash the parser so it can continue with closing the tag.
					return null;
				} else if (parser.loose && parser.match('}')) {
					// Likely in the middle of typing, just created the shorthand
					name = '';
				} else {
					e.attribute_empty_shorthand(start);
				}
			}

			parser.allow_whitespace();
			parser.eat('}', true);

			/** @type {AST.ExpressionTag} */
			const expression = {
				type: 'ExpressionTag',
				start: value_start,
				end: value_start + name.length,
				expression: {
					start: value_start,
					end: value_start + name.length,
					type: 'Identifier',
					name
				},
				metadata: {
					expression: create_expression_metadata()
				}
			};

			return create_attribute(name, start, parser.index, expression);
		}
	}

	const name = parser.read_until(regex_token_ending_character);
	if (!name) return null;

	let end = parser.index;

	parser.allow_whitespace();

	const colon_index = name.indexOf(':');
	const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index));

	/** @type {true | AST.ExpressionTag | Array<AST.Text | AST.ExpressionTag>} */
	let value = true;
	if (parser.eat('=')) {
		parser.allow_whitespace();

		if (parser.template[parser.index] === '/' && parser.template[parser.index + 1] === '>') {
			const char_start = parser.index;
			parser.index++; // consume '/'
			value = [
				{
					start: char_start,
					end: char_start + 1,
					type: 'Text',
					raw: '/',
					data: '/'
				}
			];
			end = parser.index;
		} else {
			value = read_attribute_value(parser);
			end = parser.index;
		}
	} else if (parser.match_regex(regex_starts_with_quote_characters)) {
		e.expected_token(parser.index, '=');
	}

	if (type) {
		const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');

		if (directive_name === '') {
			e.directive_missing_name({ start, end: start + colon_index + 1 }, name);
		}

		if (type === 'StyleDirective') {
			return {
				start,
				end,
				type,
				name: directive_name,
				modifiers: /** @type {Array<'important'>} */ (modifiers),
				value,
				metadata: {
					expression: create_expression_metadata()
				}
			};
		}

		const first_value = value === true ? undefined : Array.isArray(value) ? value[0] : value;

		/** @type {Expression | null} */
		let expression = null;

		if (first_value) {
			const attribute_contains_text =
				/** @type {any[]} */ (value).length > 1 || first_value.type === 'Text';
			if (attribute_contains_text) {
				e.directive_invalid_value(/** @type {number} */ (first_value.start));
			} else {
				// TODO throw a parser error in a future version here if this `[ExpressionTag]` instead of `ExpressionTag`,
				// which means stringified value, which isn't allowed for some directives?
				expression = first_value.expression;
			}
		}

		/** @type {AST.Directive} */
		const directive = {
			start,
			end,
			type,
			name: directive_name,
			expression,
			metadata: {
				expression: create_expression_metadata()
			}
		};

		// @ts-expect-error we do this separately from the declaration to avoid upsetting typescript
		directive.modifiers = modifiers;

		if (directive.type === 'TransitionDirective') {
			const direction = name.slice(0, colon_index);
			directive.intro = direction === 'in' || direction === 'transition';
			directive.outro = direction === 'out' || direction === 'transition';
		}

		// Directive name is expression, e.g. <p class:isRed />
		if (
			(directive.type === 'BindDirective' || directive.type === 'ClassDirective') &&
			!directive.expression
		) {
			directive.expression = /** @type {any} */ ({
				start: start + colon_index + 1,
				end,
				type: 'Identifier',
				name: directive.name
			});
		}

		return directive;
	}

	return create_attribute(name, start, end, value);
}
@param {Parser} parser
@returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null}