Repository: psf/black

Issue:
Trailing Comma in Dictionary not working if a comment line is at the end
**Describe the bug**

When running black on a dictionary with a comment, a trailing comma is not set correctly
The example 
```
{
    "a": fun(msg, "ts"),
    "longggggggggggggggid": ...,
    "longgggggggggggggggggggkey": ..., "created": ...
    # "longkey": ...
}
```
is being formatted to 
```
{
    "a": fun(msg, "ts"),
    "longggggggggggggggid": ...,
    "longgggggggggggggggggggkey": ...,
    "created": ...
    # "longkey": ...
}
```
where the `,` is missing after ` "created": ...`

**To Reproduce**

The bug can be reproduced using [black.vercel.app](https://black.vercel.app/?version=main&state=_Td6WFoAAATm1rRGAgAhARYAAAB0L-Wj4ADmAHRdAD2IimZxl1N_Wm_vzTQ-P7e88-HPpthubCyLPup10IjvULL9Te6KZ1NodUgQMtz1NLUGAc5rjTKK_QogZJuLtXRH2sY5WOk6D72XAqkz1z1husCxzNbP5ioWHtGyI7H0HAVp5zntQmX8C62vRP-twtlIMnwtAB0bycSlKUlRAAGQAecBAACY-DmpscRn-wIAAAAABFla)


**Expected behavior**
The expected behavior would create a format with the trailing comma set at the end of the last dictionary entry:
```
{
    "a": fun(msg, "ts"),
    "longggggggggggggggid": ...,
    "longgggggggggggggggggggkey": ...,
    "created": ...,
    # "longkey": ...
}
```

**Environment**

<!-- Please complete the following information: -->

- Black's version: 22.3.0 <!-- e.g. [main] -->
- OS and Python version: Linux/Python 3.9.7 <!-- e.g. [Linux/Python 3.7.4rc1] -->

**Additional context**

<!-- Add any other context about the problem here. -->
I had a look at the code yesterday, and I got the black dev environment locally up and running (great job with that btw, worked on the first try) and set up the test case that is failing, and  went into `black._format_str_once` with a debugger to understand what is happening.

I landed at `black.linegen.delimiter_split()` where I think the source of the issue lies, but I am not sure about that.

I got to this chunk of code, where the `current_line` is checked and always the last leave is selected and checked. If I understand correctly by some backwards looping through the line leaves.

![image](https://user-images.githubusercontent.com/32700350/168259040-bfd9b517-079c-47e0-9cb9-136a2e738100.png)

When I let the debugger iterate and always check, what `current_line.leaves[-1]` prints, it always prints the last comment line `# "longkey": ...`. Not sure if I am on the right track though.


Discussion:

Resolving patch:
diff --git a/src/black/linegen.py b/src/black/linegen.py
--- a/src/black/linegen.py
+++ b/src/black/linegen.py
@@ -520,7 +520,7 @@ def transform_line(
     else:
 
         def _rhs(
-            self: object, line: Line, features: Collection[Feature]
+            self: object, line: Line, features: Collection[Feature], mode: Mode
         ) -> Iterator[Line]:
             """Wraps calls to `right_hand_split`.
 
@@ -604,7 +604,9 @@ class _BracketSplitComponent(Enum):
     tail = auto()
 
 
-def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> Iterator[Line]:
+def left_hand_split(
+    line: Line, _features: Collection[Feature], mode: Mode
+) -> Iterator[Line]:
     """Split line into many lines, starting with the first matching bracket pair.
 
     Note: this usually looks weird, only use this for function definitions.
@@ -940,16 +942,39 @@ def dont_increase_indentation(split_func: Transformer) -> Transformer:
     """
 
     @wraps(split_func)
-    def split_wrapper(line: Line, features: Collection[Feature] = ()) -> Iterator[Line]:
-        for split_line in split_func(line, features):
+    def split_wrapper(
+        line: Line, features: Collection[Feature], mode: Mode
+    ) -> Iterator[Line]:
+        for split_line in split_func(line, features, mode):
             normalize_prefix(split_line.leaves[0], inside_brackets=True)
             yield split_line
 
     return split_wrapper
 
 
+def _get_last_non_comment_leaf(line: Line) -> Optional[int]:
+    for leaf_idx in range(len(line.leaves) - 1, 0, -1):
+        if line.leaves[leaf_idx].type != STANDALONE_COMMENT:
+            return leaf_idx
+    return None
+
+
+def _safe_add_trailing_comma(safe: bool, delimiter_priority: int, line: Line) -> Line:
+    if (
+        safe
+        and delimiter_priority == COMMA_PRIORITY
+        and line.leaves[-1].type != token.COMMA
+        and line.leaves[-1].type != STANDALONE_COMMENT
+    ):
+        new_comma = Leaf(token.COMMA, ",")
+        line.append(new_comma)
+    return line
+
+
 @dont_increase_indentation
-def delimiter_split(line: Line, features: Collection[Feature] = ()) -> Iterator[Line]:
+def delimiter_split(
+    line: Line, features: Collection[Feature], mode: Mode
+) -> Iterator[Line]:
     """Split according to delimiters of the highest priority.
 
     If the appropriate Features are given, the split will add trailing commas
@@ -989,7 +1014,8 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]:
             )
             current_line.append(leaf)
 
-    for leaf in line.leaves:
+    last_non_comment_leaf = _get_last_non_comment_leaf(line)
+    for leaf_idx, leaf in enumerate(line.leaves):
         yield from append_to_line(leaf)
 
         for comment_after in line.comments_after(leaf):
@@ -1006,6 +1032,15 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]:
                     trailing_comma_safe and Feature.TRAILING_COMMA_IN_CALL in features
                 )
 
+        if (
+            Preview.add_trailing_comma_consistently in mode
+            and last_leaf.type == STANDALONE_COMMENT
+            and leaf_idx == last_non_comment_leaf
+        ):
+            current_line = _safe_add_trailing_comma(
+                trailing_comma_safe, delimiter_priority, current_line
+            )
+
         leaf_priority = bt.delimiters.get(id(leaf))
         if leaf_priority == delimiter_priority:
             yield current_line
@@ -1014,20 +1049,15 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]:
                 mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets
             )
     if current_line:
-        if (
-            trailing_comma_safe
-            and delimiter_priority == COMMA_PRIORITY
-            and current_line.leaves[-1].type != token.COMMA
-            and current_line.leaves[-1].type != STANDALONE_COMMENT
-        ):
-            new_comma = Leaf(token.COMMA, ",")
-            current_line.append(new_comma)
+        current_line = _safe_add_trailing_comma(
+            trailing_comma_safe, delimiter_priority, current_line
+        )
         yield current_line
 
 
 @dont_increase_indentation
 def standalone_comment_split(
-    line: Line, features: Collection[Feature] = ()
+    line: Line, features: Collection[Feature], mode: Mode
 ) -> Iterator[Line]:
     """Split standalone comments from the rest of the line."""
     if not line.contains_standalone_comments(0):
@@ -1480,7 +1510,7 @@ def run_transformer(
     if not line_str:
         line_str = line_to_string(line)
     result: List[Line] = []
-    for transformed_line in transform(line, features):
+    for transformed_line in transform(line, features, mode):
         if str(transformed_line).strip("\n") == line_str:
             raise CannotTransform("Line transformer returned an unchanged result")
 
diff --git a/src/black/mode.py b/src/black/mode.py
--- a/src/black/mode.py
+++ b/src/black/mode.py
@@ -153,6 +153,7 @@ def supports_feature(target_versions: Set[TargetVersion], feature: Feature) -> b
 class Preview(Enum):
     """Individual preview style features."""
 
+    add_trailing_comma_consistently = auto()
     hex_codes_in_unicode_sequences = auto()
     prefer_splitting_right_hand_side_of_assignments = auto()
     # NOTE: string_processing requires wrap_long_dict_values_in_parens
diff --git a/src/black/trans.py b/src/black/trans.py
--- a/src/black/trans.py
+++ b/src/black/trans.py
@@ -32,7 +32,7 @@
 
 from black.comments import contains_pragma_comment
 from black.lines import Line, append_leaves
-from black.mode import Feature
+from black.mode import Feature, Mode
 from black.nodes import (
     CLOSING_BRACKETS,
     OPENING_BRACKETS,
@@ -63,7 +63,7 @@ class CannotTransform(Exception):
 # types
 T = TypeVar("T")
 LN = Union[Leaf, Node]
-Transformer = Callable[[Line, Collection[Feature]], Iterator[Line]]
+Transformer = Callable[[Line, Collection[Feature], Mode], Iterator[Line]]
 Index = int
 NodeType = int
 ParserState = int
@@ -81,7 +81,9 @@ def TErr(err_msg: str) -> Err[CannotTransform]:
     return Err(cant_transform)
 
 
-def hug_power_op(line: Line, features: Collection[Feature]) -> Iterator[Line]:
+def hug_power_op(
+    line: Line, features: Collection[Feature], mode: Mode
+) -> Iterator[Line]:
     """A transformer which normalizes spacing around power operators."""
 
     # Performance optimization to avoid unnecessary Leaf clones and other ops.
@@ -228,7 +230,9 @@ def do_transform(
             yield an CannotTransform after that point.)
         """
 
-    def __call__(self, line: Line, _features: Collection[Feature]) -> Iterator[Line]:
+    def __call__(
+        self, line: Line, _features: Collection[Feature], _mode: Mode
+    ) -> Iterator[Line]:
         """
         StringTransformer instances have a call signature that mirrors that of
         the Transformer type.


Test patch:
diff --git a/tests/data/preview/trailing_comma.py b/tests/data/preview/trailing_comma.py
new file mode 100644
--- /dev/null
+++ b/tests/data/preview/trailing_comma.py
@@ -0,0 +1,55 @@
+e = {
+    "a": fun(msg, "ts"),
+    "longggggggggggggggid": ...,
+    "longgggggggggggggggggggkey": ..., "created": ...
+    # "longkey": ...
+}
+f = [
+    arg1,
+    arg2,
+    arg3, arg4
+    # comment
+]
+g = (
+    arg1,
+    arg2,
+    arg3, arg4
+    # comment
+)
+h = {
+    arg1,
+    arg2,
+    arg3, arg4
+    # comment
+}
+
+# output
+
+e = {
+    "a": fun(msg, "ts"),
+    "longggggggggggggggid": ...,
+    "longgggggggggggggggggggkey": ...,
+    "created": ...,
+    # "longkey": ...
+}
+f = [
+    arg1,
+    arg2,
+    arg3,
+    arg4,
+    # comment
+]
+g = (
+    arg1,
+    arg2,
+    arg3,
+    arg4,
+    # comment
+)
+h = {
+    arg1,
+    arg2,
+    arg3,
+    arg4,
+    # comment
+}
diff --git a/tests/data/preview_context_managers/targeting_py39.py b/tests/data/preview_context_managers/targeting_py39.py
--- a/tests/data/preview_context_managers/targeting_py39.py
+++ b/tests/data/preview_context_managers/targeting_py39.py
@@ -84,7 +84,7 @@
     # First comment.
     new_new_new1() as cm1,
     # Second comment.
-    new_new_new2()
+    new_new_new2(),
     # Last comment.
 ):
     pass