Repository: dprint/dprint-plugin-typescript (rust)

Issue:
Bug on formatting comment after trailing comma with `"trailingCommas": "always"`
**Describe the bug**

dprint-plugin-typescript version: 0.75.0

**Config:**
```json
{
  "typescript": {
    "indentWidth": 2,
    "quoteStyle": "alwaysSingle",
    "semiColons": "always",
    "trailingCommas": "always"
  },
  "includes": ["**/*.{ts,tsx,js,jsx,cjs,mjs}"],
  "excludes": [
    "**/node_modules",
    "**/templates",
    "**/test"
  ],
  "plugins": [
    "https://plugins.dprint.dev/typescript-0.75.0.wasm"
  ]
}
```

**Input Code**

```javascript
const { readFileSync, writeFileSync, existsSync } = require('fs')
const elementTree = require('elementtree')
const { relative } = require('path')
const { red, green } = require('kolorist')

const { resolveDir } = require('../utils/app-paths')
const { log, warn } = require('../utils/logger')
const spawnSync = require('../utils/spawn-sync')

const cordovaConfigXml = resolveDir('src-cordova/config.xml')
const srcCordovaDir = resolveDir('src-cordova')

const platformList = [ 'cordova-android', 'cordova-ios' ]
const generatorList = [ 'png', 'splashscreen' ]

function getNode (root, tag, selector) {
  return (
    root.find(`${tag}${selector}`) ||
    elementTree.SubElement(root, tag)
  )
}

function hasNode (root, tag, selector) {
  return root.find(`${tag}${selector}`)
}

function isCordovaFile (file) {
  return platformList.includes(file.platform) &&
    generatorList.includes(file.generator)
}

function getCordovaFiles (files) {
  const cordovaFiles = []

  files.forEach(file => {
    if (isCordovaFile(file)) {
      cordovaFiles.push(file)
    }
  })

  return cordovaFiles
}

function updateConfigXml (cordovaFiles, hasSplashscreen) {
  const doc = elementTree.parse(readFileSync(cordovaConfigXml, 'utf-8'))
  const rootNode = doc.getroot()

  if (hasSplashscreen && !rootNode.find('preference[@name="SplashMaintainAspectRatio"]')) {
    const prefNode = elementTree.SubElement(rootNode, 'preference')
    prefNode.set('name', 'SplashMaintainAspectRatio')
    prefNode.set('value', 'true')
  }

  const androidNode = getNode(rootNode, 'platform', '[@name="android"]')
  const iosNode = getNode(rootNode, 'platform', '[@name="ios"]')

  cordovaFiles.forEach(file => {
    const isAndroid = file.platform === 'cordova-android'
    const node = isAndroid ? androidNode : iosNode
    const src = relative(srcCordovaDir, file.absoluteName)
      .replace(/\\/g, '/') // Windows support

    if (file.generator === 'splashscreen') {
      // <splash src="res/screen/android/splash-land-hdpi.png" density="land-hdpi"/>
      // <splash src="res/screen/ios/Default@2x~ipad~comany.png" />

      const entry = getNode(
        node,
        'splash',
        isAndroid ? `[@density="${file.density}"]` : `[@src="${src}"]`
      )

      entry.set('src', src)

      if (isAndroid) {
        entry.set('density', file.density)
      }
    }
    else if (file.generator === 'png') {
      // <icon src="res/android/ldpi.png" density="ldpi" />
      // <icon src="res/ios/icon-60@3x.png" width="180" height="180" />

      const entry = getNode(
        node,
        'icon',
        isAndroid
          ? `[@density="${file.density}"]`
          : `[@width="${file.width}"][@height="${file.height}"]`
      )

      entry.set('src', src)

      if (isAndroid) {
        entry.set('density', file.density)
      }
      else {
        entry.set('width', file.width)
        entry.set('height', file.height)
      }
    }
  })

  writeFileSync(cordovaConfigXml, doc.write({ indent: 4 }), 'utf-8')
  log(`Updated src-cordova/config.xml`)
}

function hasDeepProp (target /* , param1, param2, ... */) {
  let obj = target

  for (let i = 1; i < arguments.length; i++) {
    const prop = arguments[i]
    obj = obj[prop]

    if (obj === void 0) {
      return false
    }
  }

  return true
}

function installSplashscreenPlugin () {
  const pkgPath = resolveDir('src-cordova/package.json')

  if (!existsSync(pkgPath)) {
    // malformed /src-cordova...
    return
  }

  const pkg = require(pkgPath)

  if (
    hasDeepProp(pkg, 'dependencies', 'cordova-plugin-splashscreen') ||
    hasDeepProp(pkg, 'cordova', 'plugins', 'cordova-plugin-splashscreen')
  ) {
    // it's already installed, so nothing to do
    return
  }

  log(`Installing cordova-plugin-splashscreen...`)

  spawnSync('cordova', [ 'plugin', 'add', 'cordova-plugin-splashscreen' ], {
    cwd: srcCordovaDir
  }, () => {
    warn()
    warn('Failed to install cordova-plugin-splashscreen. Please do it manually.')
    console.log(' -> /src-cordova: $ cordova plugin add cordova-plugin-splashscreen\n')
  })

  console.log()
}

module.exports.mountCordova = function mountCordova (files) {
  if (existsSync(cordovaConfigXml)) {
    const cordovaFiles = getCordovaFiles(files)

    if (cordovaFiles.length > 0) {
      const hasSplashscreen = cordovaFiles.some(file => file.generator === 'splashscreen')

      if (hasSplashscreen) {
        installSplashscreenPlugin()
      }

      updateConfigXml(cordovaFiles, hasSplashscreen)
    }
  }
}

module.exports.isCordovaFile = isCordovaFile

module.exports.verifyCordova = function verifyCordova (file) {
  if (isCordovaFile(file) && existsSync(cordovaConfigXml)) {
    const doc = elementTree.parse(readFileSync(cordovaConfigXml, 'utf-8'))
    const isAndroid = file.platform === 'cordova-android'

    const node = doc.getroot()
      .find(`platform[@name="${isAndroid ? 'android' : 'ios'}"]`)

    // verify that the platform is installed
    if (!node) {
      return red('ERROR: platform not installed!')
    }

    const src = relative(srcCordovaDir, file.absoluteName)

    if (file.generator === 'splashscreen') {
      const selector = isAndroid
        ? `[@density="${file.density}"]`
        : `[@src="${src}"]`

      if (!hasNode(node, 'splash', selector)) {
        return red('ERROR: no entry for it in src-cordova/config.xml')
      }
    }
    else {
      const selector = isAndroid
        ? `[@density="${file.density}"]`
        : `[@width="${file.width}"][@height="${file.height}"]`

      if (!hasNode(node, 'icon', selector)) {
        return red('ERROR: no entry for it in src-cordova/config.xml')
      }
    }

    return green('mounted')
  }
}

```

**Actual Output**

```bash
Error formatting ../quasar_dprint/icongenie/lib/mount/mount-cordova.js. Message: Formatting not stable. Bailed after 5 tries. This indicates a bug in the plugin where it formats the file differently each time.
```

### BUT! If you put "never" in `"trailingCommas"` - it will not error.


Pull request description:
fix: correct formatting comment after tailing comma on single line with `"trailingCommas": "always"`
Closes #440

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 153e353..b37499c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -221,9 +221,9 @@ dependencies = [
 
 [[package]]
 name = "dprint-development"
-version = "0.9.1"
+version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9742cd442ec98a5a9a7ec3fe8c9e0baecf48794cce2d702ffd5d442c910bd6e1"
+checksum = "156dcd0b16dd5424774ccc56863c2990a7fb337aa0c9ac293bbe3cf6f632e297"
 dependencies = [
  "anyhow",
  "console",
diff --git a/Cargo.toml b/Cargo.toml
index e0c6f33..cd0fdb1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -34,5 +34,5 @@ serde_json = { version = "1.0", optional = true }
 
 [dev-dependencies]
 debug-here = "0.2"
-dprint-development = "0.9.1"
+dprint-development = "0.9.2"
 serde_json = { version = "1.0" }
diff --git a/src/generation/generate.rs b/src/generation/generate.rs
index 47bd979..09d6952 100644
--- a/src/generation/generate.rs
+++ b/src/generation/generate.rs
@@ -7050,6 +7050,7 @@ where
           Signal::NewLine.into(),
         ));
       } else {
+        let last_comma_token = nodes.last().and_then(|n| context.token_finder.get_next_token_if_comma(n));
         items.extend(gen_separated_values(
           GenSeparatedValuesParams {
             nodes: nodes.into_iter().map(NodeOrSeparator::Node).collect(),
@@ -7066,6 +7067,10 @@ where
           },
           context,
         ));
+        // the comma may disappear, so generate any trailing comments on the same line
+        if let Some(last_comma_token) = last_comma_token {
+          items.extend(gen_trailing_comments_same_line(&last_comma_token.range(), context));
+        }
       }
 
       items


Test patch:
diff --git a/tests/specs/general/Parameters_All.txt b/tests/specs/general/Parameters_All.txt
index 5b5fbe0..6de0e95 100644
--- a/tests/specs/general/Parameters_All.txt
+++ b/tests/specs/general/Parameters_All.txt
@@ -70,3 +70,27 @@ export async function acceptWebSocket(req = {
     other: 7,
 }): Promise<WebSocket> {
 }
+
+== should keep comment and remove trailing comma on single line ==
+function test(test, /* test */) {
+}
+
+[expect]
+function test(test /* test */) {
+}
+
+== should handle comments on next line ==
+function test(
+    test, /* test */
+    /* test */
+    // test
+) {
+}
+
+[expect]
+function test(
+    test, /* test */
+    /* test */
+    // test
+) {
+}
diff --git a/tests/specs/issues/issue0440.txt b/tests/specs/issues/issue0440.txt
new file mode 100644
index 0000000..6f836ae
--- /dev/null
+++ b/tests/specs/issues/issue0440.txt
@@ -0,0 +1,14 @@
+~~ indentWidth: 2, quoteStyle: alwaysSingle, semiColons: always, trailingCommas: always ~~
+== should handle comment in place of parameter ==
+function hasDeepProp(target /* , param1, param2, ... */) {
+}
+
+function hasDeepProp(target, /* , param1, param2, ... */) {
+}
+
+[expect]
+function hasDeepProp(target, /* , param1, param2, ... */) {
+}
+
+function hasDeepProp(target, /* , param1, param2, ... */) {
+}