Repository: jinxdash/prettier-plugin-rust (language: rust)

Issue:
Incorrect formatting of `#[generator(yield(u8))]`
### Input code

```rust
#[generator(yield(i32))]
fn nums() {
    yield_!(3);
}
```


### Output code

```rust
#[generator({
    yield i32;
})]
fn nums() {
    yield_!(3);
}
```


### Additional context

The macro `generator` is from the crate *next-gen*.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65c83e0..7a2e985 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,12 +2,14 @@
 
 ## Unreleased
 
+- fix: clear some issues related to formatting `#[attr]` like full-on expressions ([#25](jinxdash/prettier-plugin-rust/issues/25))
+
 ## 0.1.9
 
 - feat: format `cfg_if!` macros
 - feat: format `@` character in `macro_rules`
 - fix: format `<number>.` and `<number>e0` to float instead of int ([#14](https://github.com/jinxdash/prettier-plugin-rust/issues/14), [#16](https://github.com/jinxdash/prettier-plugin-rust/issues/16))
-- fix: always end files with a newline ([#21](https://github.com/jinxdash/prettier-plugin-rust/issues/21)
+- fix: always end files with a newline ([#21](https://github.com/jinxdash/prettier-plugin-rust/issues/21))
 - fix: avoid removing optional semi in rare [rust compiler bug](https://github.com/rust-lang/rust/issues/70844) ([#22](https://github.com/jinxdash/prettier-plugin-rust/issues/22))
 
 ## 0.1.8
diff --git a/src/format/comments.ts b/src/format/comments.ts
index 0a270f3..f6d9719 100644
--- a/src/format/comments.ts
+++ b/src/format/comments.ts
@@ -30,28 +30,28 @@ import {
 	ownStart,
 	start,
 } from "jinx-rust/utils";
-import { assert, exit, iLast, last_of, maybe_last_of, Narrow } from "../utils/common";
+import { is_CallExpression_or_CallLikeMacroInvocation } from "../transform";
+import { Narrow, assert, exit, iLast, last_of, maybe_last_of } from "../utils/common";
 import { is_MemberAccessLike, is_xVariableEqualishLike } from "./core";
 import {
 	AnyComment,
-	breakParent,
-	cursor,
 	CustomOptions,
 	DCM,
 	Doc,
+	MutatedAttribute,
+	NodeWithComments,
+	PrettierCommentInfo,
+	breakParent,
+	cursor,
 	hardline,
 	indent,
 	join,
 	line,
 	lineSuffix,
 	literalline,
-	MutatedAttribute,
-	NodeWithComments,
-	PrettierCommentInfo,
 } from "./external";
 import { assertPathAtNode, canAttachComment, getAllComments, getContext, getNode, getOptions, pathCallEach } from "./plugin";
 import { shouldPrintOuterAttributesAbove } from "./styling";
-import { is_CallExpression_or_CallLikeMacroInvocation } from "./transform";
 
 function addCommentHelper(node: Node, comment: AnyComment, leading = false, trailing = false) {
 	__DEV__: assert(!handled(comment));
diff --git a/src/format/core.ts b/src/format/core.ts
index 3723177..09a1bfe 100644
--- a/src/format/core.ts
+++ b/src/format/core.ts
@@ -49,7 +49,6 @@ import {
 	RangeNode,
 	RestPattern,
 	ReturnExpression,
-	SourceFile,
 	StructDeclaration,
 	StructLiteral,
 	StructLiteralPropertySpread,
@@ -151,6 +150,7 @@ import {
 	ownStart,
 	start,
 } from "jinx-rust/utils";
+import { BlockLikeMacroInvocation, CallLikeMacroInvocation, is_CallExpression_or_CallLikeMacroInvocation } from "../transform";
 import { AssertTypesEq, Identity, Map_get, Narrow, assert, exit, find_last, flat, iLast, last_of, spread } from "../utils/common";
 import {
 	CF,
@@ -216,7 +216,6 @@ import {
 	print,
 } from "./plugin";
 import { canInlineBlockBody, emptyContent, needsParens, shouldFlatten } from "./styling";
-import { BlockLikeMacroInvocation, CallLikeMacroInvocation, is_CallExpression_or_CallLikeMacroInvocation } from "./transform";
 
 export function isNoopExpressionStatement(node: Node) {
 	return is_ExpressionStatement(node) && undefined === node.expression && !hasAttributes(node) && !hasComment(node);
diff --git a/src/format/macros/utils.ts b/src/format/macros/utils.ts
deleted file mode 100644
index 26b678e..0000000
--- a/src/format/macros/utils.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { DelimGroup, DelimKind, Identifier, LocArray, PunctuationToken, TK } from "jinx-rust";
-import { isTK, is_DelimGroup, is_Identifier } from "jinx-rust/utils";
-
-export function isIdent(node: any, name: string): node is Identifier {
-	return !!node && is_Identifier(node) && node.name === name;
-}
-export function isToken(node: any, tk: TK): node is PunctuationToken {
-	return !!node && isTK(node, tk);
-}
-export function isGroup<D extends DelimKind>(node: any, dk: D): node is DelimGroup & { segments: LocArray<any, D> } {
-	return !!node && is_DelimGroup(node) && node.segments.dk === dk;
-}
diff --git a/src/format/plugin.ts b/src/format/plugin.ts
index 73e6318..9d9438d 100644
--- a/src/format/plugin.ts
+++ b/src/format/plugin.ts
@@ -21,6 +21,7 @@ import {
 	is_UnionPattern,
 	start,
 } from "jinx-rust/utils";
+import { getCommentChildNodes, isTransformed, transform_ast } from "../transform";
 import { Narrow, assert, color, each, exit, iLast, is_array, map_tagged_template, print_string } from "../utils/common";
 import {
 	CF,
@@ -41,7 +42,6 @@ import { isNoopExpressionStatement, maybeEmptyLine } from "./core";
 import { AstPath, CustomOptions, Doc, Plugin, Symbol_comments, group, hardline, indent, line, softline } from "./external";
 import { printer } from "./printer";
 import { needsInnerParens, needsOuterSoftbreakParens, shouldPrintOuterAttributesAbove } from "./styling";
-import { getCommentChildNodes, isTransformed, transform_ast } from "./transform";
 
 export function is_printing_macro() {
 	return getContext().path.stack.some((node) => is_Node(node) && (is_MacroInvocation(node) || is_Attribute(node)));
diff --git a/src/format/printer.ts b/src/format/printer.ts
index c305e28..98ea887 100644
--- a/src/format/printer.ts
+++ b/src/format/printer.ts
@@ -11,6 +11,13 @@ import {
 	is_StructLiteral,
 	start,
 } from "jinx-rust/utils";
+import {
+	BlockLikeMacroInvocation,
+	CallLikeMacroInvocation,
+	is_BlockLikeMacroInvocation,
+	is_CallLikeMacroInvocation,
+	isTransformed,
+} from "../transform";
 import { exit } from "../utils/common";
 import { hasComment, print_comment } from "./comments";
 import { isSimpleType } from "./complexity";
@@ -54,13 +61,6 @@ import {
 import { DCM, Doc, group, hardline, ifBreak, indent, join, line, softline, willBreak } from "./external";
 import { f, getOptions, getParentNode, pathCall, sg_duo, sg_single, type print } from "./plugin";
 import { needsParens, stmtNeedsSemi } from "./styling";
-import {
-	BlockLikeMacroInvocation,
-	CallLikeMacroInvocation,
-	is_BlockLikeMacroInvocation,
-	is_CallLikeMacroInvocation,
-	isTransformed,
-} from "./transform";
 
 type nPrint<T extends Node> = (print: print<T>, node: T) => Doc | never;
 
diff --git a/src/format/styling.ts b/src/format/styling.ts
index 9394df9..c462ee7 100644
--- a/src/format/styling.ts
+++ b/src/format/styling.ts
@@ -92,6 +92,7 @@ import {
 	is_bitshiftOperator,
 	is_multiplicativeOperator,
 } from "jinx-rust/utils";
+import { BlockLikeMacroInvocation, is_BlockLikeMacroInvocation, is_CallExpression_or_CallLikeMacroInvocation } from "../transform";
 import { exit, last_of } from "../utils/common";
 import { CF, hasBreaklineAfter, hasComment } from "./comments";
 import { flowControlExpressionNeedsOuterParens } from "./core";
@@ -109,7 +110,6 @@ import {
 	pathCallParentOf,
 	stackIncludes,
 } from "./plugin";
-import { BlockLikeMacroInvocation, is_BlockLikeMacroInvocation, is_CallExpression_or_CallLikeMacroInvocation } from "./transform";
 
 export function needsOuterSoftbreakParens(node: Node) {
 	const parent = getParentNode(node);
diff --git a/src/transform/custom/attribute.ts b/src/transform/custom/attribute.ts
new file mode 100644
index 0000000..abd1b0c
--- /dev/null
+++ b/src/transform/custom/attribute.ts
@@ -0,0 +1,116 @@
+import {
+	AttrSegment,
+	CallExpression,
+	DelimKind,
+	ExpressionPath,
+	Identifier,
+	Literal,
+	LocArray,
+	MacroInvocation,
+	NodeType,
+	PunctuationToken,
+	ReassignmentExpression,
+	TK,
+	rs,
+} from "jinx-rust";
+import { isTK, start } from "jinx-rust/utils";
+import { assert, exit } from "../../utils/common";
+import { isIdent } from "./utils";
+
+type SimpleAttrItem =
+	| Identifier //
+	| Literal
+	| ExpressionPath
+	| CallExpression
+	| ReassignmentExpression
+	| MacroInvocation;
+
+export function transform_simpleAttrSyntax(segments: MacroInvocation["segments"]) {
+	assert(segments.length !== 0, segments.loc.url());
+	return transform_segments(segments, false);
+
+	function transform_segments<N extends boolean>(
+		seq: LocArray<AttrSegment>,
+		nestedCall: N
+	): N extends true ? LocArray<SimpleAttrItem, "()"> : SimpleAttrItem {
+		let i = 0;
+
+		if (nestedCall) {
+			const args = rs.createLocArray<SimpleAttrItem, any>(DelimKind["()"], seq.loc.clone());
+			while (i !== seq.length) {
+				args.push(read(true));
+				if (i === seq.length) break;
+				assert(isTK(seq[i++], TK[","]));
+			}
+			return args as any;
+		} else {
+			const res = read(true);
+			assert(i === seq.length, res.loc.url());
+			return res as any;
+		}
+
+		function read(allowEq: boolean): SimpleAttrItem {
+			let lhs: Identifier | ExpressionPath;
+
+			switch (seq[i].nodeType) {
+				case NodeType.Literal:
+					return seq[i++] as Literal;
+				case NodeType.Identifier:
+					lhs = seq[i++] as Identifier;
+					break;
+				case NodeType.PunctuationToken:
+					assert((seq[i] as PunctuationToken).tk === TK["::"], seq[i].loc.url());
+					lhs = eatPathSegment(undefined);
+					break;
+				default:
+					exit.never();
+			}
+
+			while (true) {
+				if (i === seq.length) return lhs;
+				const seg = seq[i];
+				switch (seg.nodeType) {
+					case NodeType.PunctuationToken:
+						switch (seg.tk) {
+							case TK[","]:
+								assert(nestedCall);
+								return lhs;
+							case TK["="]: {
+								assert(allowEq);
+								const right = (i++, read(false));
+								return rs.mockNode(NodeType.ReassignmentExpression, right.loc.cloneFrom(start(lhs)), {
+									tk: TK["="],
+									kind: DelimKind["="],
+									left: lhs,
+									right,
+								});
+							}
+							case TK["::"]:
+								lhs = eatPathSegment(lhs);
+								continue;
+							default:
+								exit.never();
+						}
+					case NodeType.DelimGroup:
+						assert(seg.segments.dk === DelimKind["()"]);
+						return rs.mockNode(NodeType.CallExpression, seq[i++].loc.cloneFrom(start(lhs)), {
+							callee: lhs,
+							typeArguments: undefined,
+							method: undefined,
+							arguments: transform_segments(seg.segments, true),
+						});
+					default:
+						exit.never();
+				}
+			}
+		}
+
+		function eatPathSegment(left: undefined | Identifier | ExpressionPath) {
+			const segment = seq[i + 1];
+			assert(isIdent(segment));
+			const res = rs.mockNode(NodeType.ExpressionPath, segment.loc.cloneFrom(start(left ?? seq[i])), { namespace: left, segment });
+			i += 2;
+			return res;
+		}
+	}
+}
diff --git a/src/format/macros/cfg_if.ts b/src/transform/custom/cfg_if.ts
similarity index 100%
rename from src/format/macros/cfg_if.ts
rename to src/transform/custom/cfg_if.ts
diff --git a/src/transform/custom/utils.ts b/src/transform/custom/utils.ts
new file mode 100644
index 0000000..6aad32f
--- /dev/null
+++ b/src/transform/custom/utils.ts
@@ -0,0 +1,16 @@
+import { DelimGroup, DelimKind, Identifier, LocArray, PunctuationToken, Segment, TK } from "jinx-rust";
+import { isTK, is_DelimGroup, is_Identifier, is_PunctuationToken } from "jinx-rust/utils";
+
+export function isIdent(node: Segment | undefined, name?: string): node is Identifier {
+	return !!node && is_Identifier(node) && (null == name || node.name === name);
+}
+export function isToken(node: Segment | undefined, tk?: TK): node is PunctuationToken {
+	return !!node && (null == tk ? is_PunctuationToken(node) : isTK(node, tk));
+}
+export function isGroup<D extends DelimKind>(node: Segment | undefined, dk?: D): node is DelimGroup & { segments: LocArray<any, D> } {
+	return !!node && is_DelimGroup(node) && (null == dk || node.segments.dk === dk);
+}
+
+export function isCallLike(tk_1: Segment | undefined, tk_2: Segment | undefined): boolean {
+	return !!tk_1 && !!tk_2 && is_Identifier(tk_1) && is_DelimGroup(tk_2) && tk_2.segments.dk === DelimKind["()"];
+}
diff --git a/src/format/transform.ts b/src/transform/index.ts
similarity index 96%
rename from src/format/transform.ts
rename to src/transform/index.ts
index f0d90ab..7762205 100644
--- a/src/format/transform.ts
+++ b/src/transform/index.ts
@@ -48,13 +48,11 @@ import {
 	is_ExpressionStatement,
 	is_ExpressionWithBodyOrCases,
 	is_FlowControlExpression,
-	is_Identifier,
 	is_IfBlockExpression,
 	is_MacroInvocation,
 	is_Node,
 	is_NodeWithBodyNoBody,
 	is_NodeWithBodyOrCases,
-	is_PathNode,
 	is_Program,
 	is_PunctuationToken,
 	is_ReassignmentNode,
@@ -69,6 +67,10 @@ import {
 	transferAttributes,
 	unsafe_set_nodeType,
 } from "jinx-rust/utils";
+import { isPrettierIgnoreAttribute, setPrettierIgnoreTarget } from "../format/comments";
+import { is_StructSpread } from "../format/core";
+import { CustomOptions } from "../format/external";
+import { getOptions } from "../format/plugin";
 import {
 	Array_replace,
 	Array_splice,
@@ -82,11 +84,8 @@ import {
 	spliceAll,
 	try_eval,
 } from "../utils/common";
-import { isPrettierIgnoreAttribute, setPrettierIgnoreTarget } from "./comments";
-import { is_StructSpread } from "./core";
-import { CustomOptions } from "./external";
-import { transform_macro_cfg_if } from "./macros/cfg_if";
-import { getOptions } from "./plugin";
+import { transform_simpleAttrSyntax } from "./custom/attribute";
+import { transform_macro_cfg_if } from "./custom/cfg_if";
 
 export interface ExpressionLikeAttribute extends Attribute {
 	segments: LocArray<any, "[]">;
@@ -213,13 +212,12 @@ export function isTransformed(node: Node) {
 
 const transform: { [K in NodeType]?: (node: NTMap[K]) => void } = {
 	[NodeType.Attribute](node) {
-		maybe_transform_node(
-			node as ExpressionLikeAttribute,
-			() => rs.toCallExpressionArguments(node.segments),
-			(node, snippet) => {
-				node.segments = snippet.ast;
-			}
-		);
+		try_eval(() => {
+			node.segments = rs.createLocArray(node.segments.dk, node.segments.loc.clone(), [
+				transform_simpleAttrSyntax(node.segments),
+			]) as any;
+			transformed.add(node);
+		});
 	},
 	[NodeType.MacroInlineRuleDeclaration](node) {
 		node.match.dk = DelimKind["()"];
@@ -540,7 +538,7 @@ export function getCommentChildNodes(n: any): Node[] {
 	return children;
 
 	function getTransformedNodeChildren(node: Node) {
-		if (is_Program(node)) node.comments ??= []; // prettier deletes it
+		if (is_Program(node)) node.comments ??= []; // prettier core deletes this property
 		const children = getNodeChildren(node);
 
 		if (is_NodeWithBodyNoBody(node)) {


Test patch:
diff --git a/tests/output/issues/25.f.rs b/tests/output/issues/25.f.rs
new file mode 100644
index 0000000..fa4c439
--- /dev/null
+++ b/tests/output/issues/25.f.rs
@@ -0,0 +1,35 @@
+#[generator(yield(i32))]
+fn nums() {
+  yield_!(3);
+}
+
+// some extra samples to track changes
+#[// 0
+generator(
+  // 1
+  // 2
+  yield(
+    // 3
+    // 4
+    i32
+    // 5
+  )
+  // 6
+)]
+// 7
+
+// non-conventional syntax (does not format)
+#[#[a] generator(  #[b] yield(  #[c] i32  ))]
+#[generator  (a(  #[generator(b(i32))]   i32  )  )]
+#[generator  (a(  #[generator(yield(i32))]   i32  )  )]
+#[generator  (yield(  #[generator(b(i32))]   i32  )  )]
+#[generator  (yield(  #[generator(yield(i32))]   i32  )  )]
+fn f() {
+  yield_!(3);
+}
+
+// macros in attr (does not format)
+#[attr(foo!(   ))]
+fn f() {}
+
+// source: "../../samples/issues/25.rs"
\ No newline at end of file
diff --git a/tests/samples/issues/25.rs b/tests/samples/issues/25.rs
new file mode 100644
index 0000000..5c009e9
--- /dev/null
+++ b/tests/samples/issues/25.rs
@@ -0,0 +1,37 @@
+#[ generator(  yield(  i32  )  )]
+fn nums() {
+    yield_!(3);
+}
+
+// some extra samples to track changes
+#[
+// 0    
+generator
+// 1
+(
+// 2
+yield
+// 3
+(
+// 4
+i32
+// 5
+)
+// 6
+)
+// 7
+]
+
+// non-conventional syntax (does not format)
+#[  #[a] generator(  #[b] yield(  #[c] i32  ))]
+#[generator  (a(  #[generator(b(i32))]   i32  )  )  ]
+#[generator  (a(  #[generator(yield(i32))]   i32  )  )  ]
+#[generator  (yield(  #[generator(b(i32))]   i32  )  )  ]
+#[generator  (yield(  #[generator(yield(i32))]   i32  )  )  ]
+fn f() {
+    yield_!(3);
+}
+
+// macros in attr (does not format)
+#[attr(foo!(   ))]
+fn f() {}
\ No newline at end of file