Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow tests for shebang rule #119

Closed
piranna opened this issue Sep 16, 2023 · 14 comments · Fixed by #172
Closed

Allow tests for shebang rule #119

piranna opened this issue Sep 16, 2023 · 14 comments · Fixed by #172

Comments

@piranna
Copy link

piranna commented Sep 16, 2023

shebang rule only check for the bin field in package.json, but with the new Node.js built-in test test runner, it's valid that they are run as standalone executables, with a shebang and an executable bit.

My suggestion is that files that import test or node:test can have a shebang, and if so, check and enforce that they have enabled the executable permission bit. In the other way could not work, importing test built-in and have enabled the executable permission bit is NOT enough to consider it should have a shebang, because Windows don't have permission bits and the file can be importing the test runner but not intended to be used standalone, except is the test script on package.json file is running the test file as if would be an executable, in that case file must have the shebang.

@scagood
Copy link

scagood commented Sep 21, 2023

🤔 We could test if the file is executable?

Something like:

import { access, constants } from 'node:fs/promises';

export async function isExecutable(path) {
  try {
    await access(path, constants.X_OK);
    return true;
  } catch (error) {
    if (error.code === 'EACCES') {
      return false;
    }

    throw error;
  }
}

// Then just do this in the rule
if (hasShebang && !isExecutable) {
  context.report();
}

@piranna
Copy link
Author

piranna commented Sep 21, 2023

Yes, but that's not enough, we would also need to check for the import of test or node:test, and maybe also check if it's being run from the test script, both directly as an executable or as an argument of the test runner, or if filename has one of the names from the default test runner files.

@scagood
Copy link

scagood commented Sep 21, 2023

Assuming { "type": "module" }, with the following test cases:

executable.js

#!/usr/bin/env node
console.info('executable')

test.js

#!/usr/bin/env node
import { equal } from 'node:assert/strict';
import { test } from 'node:test';

test('some test', () => {
  equal(true, true);
});

image

In that example:

  • The only valid shebang is the one in executable.js.
  • The shebang in the test.js file is invalid.

I do agree that the current rule is probably not right, but I think its for a different reason 👀

@piranna
Copy link
Author

piranna commented Sep 21, 2023

  • The shebang in the test.js file is invalid.

The shebang is valid... if you add executable permissions :-) So the rule should check and fix it for the test.js file.

@scagood
Copy link

scagood commented Sep 21, 2023

mmm, I see!

@scagood
Copy link

scagood commented Sep 25, 2023

So, my thoughts on this are we could add the following:

{
  "n/shebang": [
    "error",
    // Defaults for the rule (mostly to prevent a breaking change)
    {
      // Leave the default check
      checkPackageBin: true,
      // Add the ability to enable test files
      checkTestFiles: false,
      // Check all executable files
      checkExecutable: false,
    }
  ]
}

For each of the options, if the following is matched we should check the shebang:

checkPackageBin:

  1. Is the file referenced in the package.json#bin
  2. Is the file executable - not sure about this one

checkTestFiles:

  1. Check for ImportDeclaration, ImportExpression, and require calls
  2. Check the file is executable.

checkExecutable:

  1. Check if the file is executable
👨‍💻 A quick code example for checking for `node:test`, and `test`

I roughly validated this in https://astexplorer.net/
image

export default {
  create: function (context) {
    let foundTest = false;
    let fileExecutable = false;
    let validShebang = true;

    return {
      "Program:exit"(node) {
        if (foundTest === false) {
          return;
        }

        if (fileExecutable === false) {
          return;
        }

        if (validShebang === true) {
          return;
        }

        context.report({ node, message: "Program needs a valid shebang" });
      },

      /* import 'test'; */
      'ImportDeclaration[source.value="test"]'(node) {
        foundTest = true;
      },
      /* import 'node:test'; */
      'ImportDeclaration[source.value="node:test"]'(node) {
        foundTest = true;
      },
      /* await import('test'); */
      'ImportExpression[source.value="test"]'(node) {
        foundTest = true;
      },
      /* await import('node:test'); */
      'ImportExpression[source.value="node:test"]'(node) {
        foundTest = true;
      },
      /* require('test'); */
      'CallExpression[callee.name="require"][arguments.0.value="test"]'(node) {
        foundTest = true;
      },
      /* require('node:test'); */
      'CallExpression[callee.name="require"][arguments.0.value="node:test"]'(node) {
        foundTest = true;
      },

      /* Hacks for babel */

      /* await import('test'); */
      'CallExpression[callee.name="import"][arguments.0.value="test"]'(node) {
        foundTest = true;
      },
      /* await import('node:test'); */
      'CallExpression[callee.name="import"][arguments.0.value="node:test"]'(node) {
        foundTest = true;
      },
      /* await import('test'); */
      'CallExpression[callee.type="Import"][arguments.0.value="test"]'(node) {
        foundTest = true;
      },
      /* await import('node:test'); */
      'CallExpression[callee.type="Import"][arguments.0.value="node:test"]'(node) {
        foundTest = true;
      }
    };
  }
};

@aladdin-add What do you think?

@piranna
Copy link
Author

piranna commented Sep 25, 2023

checkPackageBin:

  1. Is the file referenced in the package.json#bin
  2. Is the file executable - not sure about this one

If file is in package.json#bin, it MUST has a shebang AND be executable.

checkTestFiles:

  1. Check for ImportDeclaration, ImportExpression, and require calls
  2. Check the file is executable.

Also 3. check it's run from package.json#scripts.test as an standalone executable

checkExecutable:

  1. Check if the file is executable

Not sure what could be this checking...

@aladdin-add
Copy link

aladdin-add commented Sep 26, 2023

it's something like guessing the users' intent - IMHO, not all the tests are executable. i would suggest to adding a new option additionalExcutable (open to the option name 😄 ):

{
  "n/shebang": [
    "error",
    {
       "additionalExcutable": ["tests/*.js"]
    }
  ]
}

@piranna
Copy link
Author

piranna commented Sep 26, 2023

Exactly, not all tests would be executable, with executions bit and shebang, only the ones that are run directly as commands on the test script.

@piranna
Copy link
Author

piranna commented Jan 12, 2024

Somewhat related to this, I usually have some deploy scripts that I want them to be executable as part of my package.json file scripts object, but i DON'T want them to be included when publishing the package. I think that ignoring if a file has or not an executable bit if it's excluded by the files field or the .npmignore file would do the job too, in a more broad and generic way.

@scagood
Copy link

scagood commented Jan 13, 2024

🤔 I think I like that approach.

@aladdin-add
Copy link

aladdin-add commented Jan 29, 2024

well after thinking for a while, I'd more prefer the way mentioned earlier: #119 (comment)

There is a difference that additionalExcutable is not ignoring; it's to enforce some additional files having the correct shebangs. Seems more in line with user expectations?

@aladdin-add
Copy link

@scagood wdyt?

@scagood
Copy link

scagood commented Jan 29, 2024

We can add a couple of settings I think 🤔

@scagood scagood linked a pull request Feb 2, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants