Repository: eslint-community/eslint-plugin-promise
Pull request #509 (2024-07-24 11:07:10)

ci: add pr title format validation
**What is the purpose of this pull request?**

To assist #508 / #506 by ensuring that PR-title's are always valid conventional commit format before merging

**What changes did you make? (Give an overview)**

Added a pinned [mtfoley/pr-compliance-action](https://github.com/mtfoley/pr-compliance-action) similar to what I use at eg. neostandard: https://github.com/neostandard/neostandard/blob/main/.github/workflows/compliance.yml


Patch:
diff --git a/.github/release-please/config.json b/.github/release-please/config.json
new file mode 100644
index 0000000..336265d
--- /dev/null
+++ b/.github/release-please/config.json
@@ -0,0 +1,21 @@
+{
+  "$schema": "https://raw.githubusercontent.com/googleapis/release-please/v16.12.0/schemas/config.json",
+  "release-type": "node",
+  "include-component-in-tag": false,
+  "changelog-sections": [
+    { "type": "feat", "section": "🌟 Features", "hidden": false },
+    { "type": "fix", "section": "🩹 Fixes", "hidden": false },
+    { "type": "docs", "section": "📚 Documentation", "hidden": false },
+
+    { "type": "chore", "section": "🧹 Chores", "hidden": false },
+    { "type": "perf", "section": "🧹 Chores", "hidden": false },
+    { "type": "refactor", "section": "🧹 Chores", "hidden": false },
+    { "type": "test", "section": "🧹 Chores", "hidden": false },
+
+    { "type": "build", "section": "🤖 Automation", "hidden": false },
+    { "type": "ci", "section": "🤖 Automation", "hidden": true }
+  ],
+  "packages": {
+    ".": {}
+  }
+}
diff --git a/.github/release-please/manifest.json b/.github/release-please/manifest.json
new file mode 100644
index 0000000..8cd0d4e
--- /dev/null
+++ b/.github/release-please/manifest.json
@@ -0,0 +1,1 @@
+{ ".": "7.0.0" }
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7903ca7..79db47f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,13 +2,7 @@ name: CI
 on:
   push:
     branches:
-      # default semantic-release branches
-      - +([0-9])?(.{+([0-9]),x}).x
       - main
-      - next
-      - next-major
-      - beta
-      - alpha
   pull_request:
 
 concurrency:
@@ -26,10 +20,10 @@ jobs:
       - name: ⎔ Setup Node
         uses: actions/setup-node@v4
         with:
-          node-version: 18
+          node-version: lts/*
 
       - name: 📥 Install dependencies
-        run: npm install
+        run: npm ci
 
       - name: ▶️ Run lint script
         run: npm run lint
@@ -81,40 +75,3 @@ jobs:
 
       - name: ⬆️ Upload coverage report
         uses: codecov/codecov-action@v4
-
-  release:
-    name: 🚀 Release
-    needs: [lint, test]
-    runs-on: ubuntu-latest
-    if:
-      github.repository == 'eslint-community/eslint-plugin-promise' &&
-      contains('refs/heads/main,refs/heads/next,refs/heads/beta,refs/heads/alpha',
-      github.ref) && github.event_name == 'push'
-    steps:
-      - name: ⬇️ Checkout repo
-        uses: actions/checkout@v4
-
-      - name: ⎔ Setup node
-        uses: actions/setup-node@v4
-        with:
-          node-version: 18
-
-      - name: 📥 Install dependencies
-        run: npm install
-
-      - name: 🚀 Release
-        uses: cycjimmy/semantic-release-action@v4
-        with:
-          semantic_version: 19
-          branches: |
-            [
-              '+([0-9])?(.{+([0-9]),x}).x',
-              'main',
-              'next',
-              'next-major',
-              {name: 'beta', prerelease: true},
-              {name: 'alpha', prerelease: true}
-            ]
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/compliance.yml b/.github/workflows/compliance.yml
new file mode 100644
index 0000000..141216a
--- /dev/null
+++ b/.github/workflows/compliance.yml
@@ -0,0 +1,18 @@
+name: Compliance
+
+on:
+  pull_request_target:
+    types: [opened, edited, reopened]
+
+permissions:
+  pull-requests: write
+
+jobs:
+  compliance:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: mtfoley/pr-compliance-action@11b664f0fcf2c4ce954f05ccfcaab6e52b529f86
+        with:
+          body-auto-close: false
+          body-regex: '.*'
+          ignore-team-members: false
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
index 0b3cffd..727900e 100644
--- a/.github/workflows/format.yml
+++ b/.github/workflows/format.yml
@@ -21,10 +21,10 @@ jobs:
       - name: ⎔ Setup node
         uses: actions/setup-node@v4
         with:
-          node-version: 18
+          node-version: lts/*
 
       - name: 📥 Install deps
-        run: npm install
+        run: npm ci
 
       - name: 👔 Format
         run: npm run format
diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
new file mode 100644
index 0000000..cab30d2
--- /dev/null
+++ b/.github/workflows/release-please.yml
@@ -0,0 +1,45 @@
+name: Release Please
+
+on:
+  push:
+    branches:
+      - main
+
+permissions:
+  contents: read
+
+jobs:
+  release_please:
+    name: release-please
+    runs-on: ubuntu-latest
+    outputs:
+      releaseCreated: ${{ steps.release.outputs.release_created }}
+    permissions:
+      contents: write
+      pull-requests: write
+    steps:
+      - uses: googleapis/release-please-action@v4
+        id: release
+        with:
+          config-file: .github/release-please/config.json
+          manifest-file: .github/release-please/manifest.json
+
+  npm_publish:
+    name: Publish to npm
+    runs-on: ubuntu-latest
+    needs: release_please
+    if: needs.release_please.outputs.releaseCreated
+    permissions:
+      id-token: write
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          show-progress: false
+      - uses: actions/setup-node@v4
+        with:
+          node-version: lts/*
+          registry-url: 'https://registry.npmjs.org'
+      - run: npm ci
+      - run: npm publish --provenance --access public
+        env:
+          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/README.md b/README.md
index e4e8164..b5fa663 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 Enforce best practices for JavaScript promises.
 
-[![CI](https://github.com/eslint-community/eslint-plugin-promise/actions/workflows/CI.yml/badge.svg)](https://github.com/eslint-community/eslint-plugin-promise/actions/workflows/CI.yml)
+[![CI](https://github.com/eslint-community/eslint-plugin-promise/actions/workflows/ci.yml/badge.svg)](https://github.com/eslint-community/eslint-plugin-promise/actions/workflows/ci.yml)
 [![npm version](https://badge.fury.io/js/eslint-plugin-promise.svg)](https://www.npmjs.com/package/eslint-plugin-promise)
 [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
 
@@ -115,6 +115,7 @@ or start with the recommended rule set:
 | [param-names](docs/rules/param-names.md)                             | Enforce consistent param names and ordering when creating new promises.                    | ✅  |     |     |     |
 | [prefer-await-to-callbacks](docs/rules/prefer-await-to-callbacks.md) | Prefer `async`/`await` to the callback pattern.                                            |     |     |     |     |
 | [prefer-await-to-then](docs/rules/prefer-await-to-then.md)           | Prefer `await` to `then()`/`catch()`/`finally()` for reading Promise values.               |     |     |     |     |
+| [spec-only](docs/rules/spec-only.md)                                 | Disallow use of non-standard Promise static methods.                                       |     |     |     |     |
 | [valid-params](docs/rules/valid-params.md)                           | Enforces the proper number of arguments are passed to Promise functions.                   |     | ✅  |     |     |
 
 <!-- end auto-generated rules list -->
diff --git a/docs/rules/spec-only.md b/docs/rules/spec-only.md
new file mode 100644
index 0000000..d46228c
--- /dev/null
+++ b/docs/rules/spec-only.md
@@ -0,0 +1,24 @@
+# Disallow use of non-standard Promise static methods (`promise/spec-only`)
+
+<!-- end auto-generated rule header -->
+
+It may become difficult to migrate code depending on non-standard Promise
+extensions. This rule reports any such method usage.
+
+## Valid
+
+```js
+const x = Promise.resolve('good')
+```
+
+## Invalid
+
+```js
+const x = Promise.done('bad')
+```
+
+## Options
+
+### `allowedMethods`
+
+An array of allowed non-standard methods. Defaults to an empty array.
diff --git a/index.js b/index.js
index 41ff2f3..79eeae9 100644
--- a/index.js
+++ b/index.js
@@ -32,6 +32,7 @@ const pluginPromise = {
     'no-return-in-finally': require('./rules/no-return-in-finally'),
     'valid-params': require('./rules/valid-params'),
     'no-multiple-resolved': require('./rules/no-multiple-resolved'),
+    'spec-only': require('./rules/spec-only'),
   },
   rulesConfig: {
     'param-names': 1,
diff --git a/package.json b/package.json
index 39fdb27..f6003dc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-promise",
-  "version": "0.0.0-semantically-released",
+  "version": "7.0.0",
   "description": "Enforce best practices for JavaScript promises",
   "keywords": [
     "eslint",
diff --git a/rules/catch-or-return.js b/rules/catch-or-return.js
index a22da31..c12b82f 100644
--- a/rules/catch-or-return.js
+++ b/rules/catch-or-return.js
@@ -8,6 +8,7 @@
 
 const getDocsUrl = require('./lib/get-docs-url')
 const isPromise = require('./lib/is-promise')
+const isMemberCallWithObjectName = require('./lib/is-member-call-with-object-name')
 
 module.exports = {
   meta: {
@@ -98,6 +99,11 @@ module.exports = {
         return true
       }
 
+      // cy.get().then(a, b);
+      if (isMemberCallWithObjectName('cy', expression)) {
+        return true
+      }
+
       return false
     }
 
diff --git a/rules/lib/is-inside-callback.js b/rules/lib/is-inside-callback.js
index fd91595..b1bb80e 100644
--- a/rules/lib/is-inside-callback.js
+++ b/rules/lib/is-inside-callback.js
@@ -3,7 +3,7 @@
 const isInsidePromise = require('./is-inside-promise')
 
 function isInsideCallback(node) {
-  const isCallExpression =
+  const isFunction =
     node.type === 'FunctionExpression' ||
     node.type === 'ArrowFunctionExpression' ||
     node.type === 'FunctionDeclaration' // this may be controversial
@@ -13,7 +13,7 @@ function isInsideCallback(node) {
 
   const name = node.params && node.params[0] && node.params[0].name
   const firstArgIsError = name === 'err' || name === 'error'
-  const isInACallback = isCallExpression && firstArgIsError
+  const isInACallback = isFunction && firstArgIsError
   return isInACallback
 }
 
diff --git a/rules/lib/is-member-call-with-object-name b/rules/lib/is-member-call-with-object-name
new file mode 100644
index 0000000..60107e0
--- /dev/null
+++ b/rules/lib/is-member-call-with-object-name
@@ -0,0 +1,18 @@
+'use strict'
+
+/**
+ * @param {string} objectName
+ * @param {Node} node
+ * @returns {node is CallExpression}
+ */
+function isMemberCallWithObjectName(objectName, node) {
+  return (
+    node.type === 'CallExpression' &&
+    node.callee.type === 'MemberExpression' &&
+    ((node.callee.object.type === 'Identifier' &&
+      node.callee.object.name === objectName) ||
+      isMemberCallWithObjectName(objectName, node.callee.object))
+  )
+}
+
+module.exports = isMemberCallWithObjectName
diff --git a/rules/lib/is-promise.js b/rules/lib/is-promise.js
index 857e346..840dfb2 100644
--- a/rules/lib/is-promise.js
+++ b/rules/lib/is-promise.js
@@ -29,7 +29,8 @@ function isPromise(expression) {
       expression.callee.type === 'MemberExpression' &&
       expression.callee.object.type === 'Identifier' &&
       expression.callee.object.name === 'Promise' &&
-      PROMISE_STATICS[expression.callee.property.name])
+      PROMISE_STATICS[expression.callee.property.name] &&
+      expression.callee.property.name !== 'withResolvers')
   )
 }
 
diff --git a/rules/lib/promise-statics.js b/rules/lib/promise-statics.js
index 6aa8646..e42205a 100644
--- a/rules/lib/promise-statics.js
+++ b/rules/lib/promise-statics.js
@@ -7,4 +7,5 @@ module.exports = {
   race: true,
   reject: true,
   resolve: true,
+  withResolvers: true,
 }
diff --git a/rules/prefer-await-to-then.js b/rules/prefer-await-to-then.js
index 3c2fab7..0953247 100644
--- a/rules/prefer-await-to-then.js
+++ b/rules/prefer-await-to-then.js
@@ -7,6 +7,7 @@
 
 const { getAncestors, getScope } = require('./lib/eslint-compat')
 const getDocsUrl = require('./lib/get-docs-url')
+const isMemberCallWithObjectName = require('./lib/is-member-call-with-object-name')
 
 module.exports = {
   meta: {
@@ -40,6 +41,15 @@ module.exports = {
       })
     }
 
+    /** Returns true if node is inside a constructor */
+    function isInsideConstructor(node) {
+      return getAncestors(context, node).some((parent) => {
+        return (
+          parent.type === 'MethodDefinition' && parent.kind === 'constructor'
+        )
+      })
+    }
+
     /**
      * Returns true if node is created at the top-level scope.
      * Await statements are not allowed at the top level,
@@ -53,7 +63,12 @@ module.exports = {
 
     return {
       'CallExpression > MemberExpression.callee'(node) {
-        if (isTopLevelScoped(node) || (!strict && isInsideYieldOrAwait(node))) {
+        if (
+          isTopLevelScoped(node) ||
+          (!strict && isInsideYieldOrAwait(node)) ||
+          (!strict && isInsideConstructor(node)) ||
+          isMemberCallWithObjectName('cy', node.parent)
+        ) {
           return
         }
 
diff --git a/rules/spec-only.js b/rules/spec-only.js
new file mode 100644
index 0000000..e5ab90a
--- /dev/null
+++ b/rules/spec-only.js
@@ -0,0 +1,51 @@
+'use strict'
+
+const PROMISE_STATICS = require('./lib/promise-statics')
+const getDocsUrl = require('./lib/get-docs-url')
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'Disallow use of non-standard Promise static methods.',
+      url: getDocsUrl('spec-only'),
+    },
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          allowedMethods: {
+            type: 'array',
+            items: {
+              type: 'string',
+            },
+          },
+        },
+        additionalProperties: false,
+      },
+    ],
+    messages: {
+      avoidNonStandard: "Avoid using non-standard 'Promise.{{ name }}'",
+    },
+  },
+  create(context) {
+    const { allowedMethods = [] } = context.options[0] || {}
+
+    return {
+      MemberExpression(node) {
+        if (
+          node.object.type === 'Identifier' &&
+          node.object.name === 'Promise' &&
+          !(node.property.name in PROMISE_STATICS) &&
+          !allowedMethods.includes(node.property.name)
+        ) {
+          context.report({
+            node,
+            messageId: 'avoidNonStandard',
+            data: { name: node.property.name },
+          })
+        }
+      },
+    }
+  },
+}


Test patch:
diff --git a/__tests__/catch-or-return.js b/__tests__/catch-or-return.js
index c4426d5..324f148 100644
--- a/__tests__/catch-or-return.js
+++ b/__tests__/catch-or-return.js
@@ -22,6 +22,10 @@ ruleTester.run('catch-or-return', rule, {
     'Promise.resolve(frank)["catch"](jail)',
     'frank.then(to).finally(fn).catch(jail)',
 
+    // Cypress
+    'cy.get(".myClass").then(go)',
+    'cy.get("button").click().then()',
+
     // arrow function use case
     'postJSON("/smajobber/api/reportJob.json")\n\t.then(()=>this.setState())\n\t.catch(()=>this.setState())',
 
diff --git a/__tests__/no-new-statics.js b/__tests__/no-new-statics.js
index 96abe1e..4ced5e8 100644
--- a/__tests__/no-new-statics.js
+++ b/__tests__/no-new-statics.js
@@ -10,6 +10,7 @@ ruleTester.run('no-new-statics', rule, {
     'Promise.reject()',
     'Promise.all()',
     'Promise.race()',
+    'Promise.withResolvers()',
     'new Promise(function (resolve, reject) {})',
     'new SomeClass()',
     'SomeClass.resolve()',
@@ -46,6 +47,11 @@ ruleTester.run('no-new-statics', rule, {
       output: 'Promise.race()',
       errors: [{ message: "Avoid calling 'new' on 'Promise.race()'" }],
     },
+    {
+      code: 'new Promise.withResolvers()',
+      output: 'Promise.withResolvers()',
+      errors: [{ message: "Avoid calling 'new' on 'Promise.withResolvers()'" }],
+    },
     {
       code: [
         'function foo() {',
diff --git a/__tests__/prefer-await-to-then.js b/__tests__/prefer-await-to-then.js
index 2770c7b..917f641 100644
--- a/__tests__/prefer-await-to-then.js
+++ b/__tests__/prefer-await-to-then.js
@@ -16,6 +16,11 @@ ruleTester.run('prefer-await-to-then', rule, {
     'async function hi() { await thing().then() }',
     'async function hi() { await thing().catch() }',
     'async function hi() { await thing().finally() }',
+
+    // Cypress
+    'function hi() { cy.get(".myClass").then(go) }',
+    'function hi() { cy.get("button").click().then() }',
+
     'function * hi() { yield thing().then() }',
     'a = async () => (await something())',
     `a = async () => {
@@ -29,6 +34,11 @@ ruleTester.run('prefer-await-to-then', rule, {
     `function isThenable(obj) {
       return obj && typeof obj.then === 'function';
     }`,
+    `class Foo {
+      constructor () {
+        doSomething.then(abc);
+      }
+    }`,
   ],
 
   invalid: [
@@ -65,6 +75,19 @@ ruleTester.run('prefer-await-to-then', rule, {
         },
       ],
     },
+    {
+      code: `class Foo {
+        constructor () {
+          doSomething.then(abc);
+        }
+      }`,
+      errors: [{ message }],
+      options: [
+        {
+          strict: true,
+        },
+      ],
+    },
     {
       code: 'async function hi() { await thing().catch() }',
       errors: [{ message }],
diff --git a/__tests__/spec-only.js b/__tests__/spec-only.js
new file mode 100644
index 0000000..ea93a37
--- /dev/null
+++ b/__tests__/spec-only.js
@@ -0,0 +1,57 @@
+'use strict'
+
+const rule = require('../rules/spec-only')
+const { RuleTester } = require('./rule-tester')
+const ruleTester = new RuleTester()
+
+ruleTester.run('spec-only', rule, {
+  valid: [
+    'Promise.resolve()',
+    'Promise.reject()',
+    'Promise.all()',
+    'Promise.race()',
+    'Promise.withResolvers()',
+    'new Promise(function (resolve, reject) {})',
+    'SomeClass.resolve()',
+    'doSomething(Promise.all)',
+    {
+      code: 'Promise.permittedMethod()',
+      options: [
+        {
+          allowedMethods: ['permittedMethod'],
+        },
+      ],
+    },
+  ],
+  invalid: [
+    {
+      code: 'Promise.done()',
+      errors: [{ message: "Avoid using non-standard 'Promise.done'" }],
+    },
+    {
+      code: 'Promise.something()',
+      errors: [{ message: "Avoid using non-standard 'Promise.something'" }],
+    },
+    {
+      code: 'new Promise.done()',
+      errors: [{ message: "Avoid using non-standard 'Promise.done'" }],
+    },
+    {
+      code: `
+        function foo() {
+          var a = getA()
+          return Promise.done(a)
+        }
+      `,
+      errors: [{ message: "Avoid using non-standard 'Promise.done'" }],
+    },
+    {
+      code: `
+        function foo() {
+          getA(Promise.done)
+        }
+      `,
+      errors: [{ message: "Avoid using non-standard 'Promise.done'" }],
+    },
+  ],
+})