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

module: warn on using unfinished circular dependency #29935

Closed
wants to merge 3 commits into from

Conversation

addaleax
Copy link
Member

Warn when a non-existent property of an unfinished module.exports
object is being accessed, as that very often indicates the presence
of a hard-to-detect and hard-to-debug problem.

This mechanism is only used if module.exports is still a
regular object at the point at which the second, circular require()
happens.

The downside is that, temporarily, module.exports will have a
prototype other than Object.prototype, and that there may
be valid uses of accessing non-existent properties of unfinished
module.exports objects.

Performance of circular require calls in general is not
noticeably impacted.

                                           confidence improvement accuracy (*)   (**)  (***)
 module/module-loader-circular.js n=10000                 3.96 %       ±5.12% ±6.82% ±8.89%

Example:

$ cat a.js 
'use strict';
const b = require('./b.js');

exports.fn = () => {};
$ cat b.js 
'use strict';
const a = require('./a.js');

a.fn();
$ node a.js 
(node:1617) Warning: Accessing non-existent property 'fn' of module exports inside circular dependency
/tmp/b.js:4
a.fn();
  ^

TypeError: a.fn is not a function
    at Object.<anonymous> (/tmp/b.js:4:3)
    [...]
Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • commit message follows commit guidelines

Warn when a non-existent property of an unfinished module.exports
object is being accessed, as that very often indicates the presence
of a hard-to-detect and hard-to-debug problem.

This mechanism is only used if `module.exports` is still a
regular object at the point at which the second, circular `require()`
happens.

The downside is that, temporarily, `module.exports` will have a
prototype other than `Object.prototype`, and that there may
be valid uses of accessing non-existent properties of unfinished
`module.exports` objects.

Performance of circular require calls in general is not
noticeably impacted.

                                               confidence improvement accuracy (*)   (**)  (***)
     module/module-loader-circular.js n=10000                 3.96 %       ±5.12% ±6.82% ±8.89%

Example:

    $ cat a.js
    'use strict';
    const b = require('./b.js');

    exports.fn = () => {};
    $ cat b.js
    'use strict';
    const a = require('./a.js');

    a.fn();
    $ node a.js
    (node:1617) Warning: Accessing non-existent property 'fn' of module exports inside circular dependency
    /tmp/b.js:4
    a.fn();
      ^

    TypeError: a.fn is not a function
        at Object.<anonymous> (/tmp/b.js:4:3)
        [...]
@addaleax addaleax added the module Issues and PRs related to the module subsystem. label Oct 11, 2019
@devsnek
Copy link
Member

devsnek commented Oct 11, 2019

maybe we could start with warning if module.loaded is false, and look into more advanced messages in future changes?

@addaleax
Copy link
Member Author

@devsnek This does only warn if module.loaded is false, so I’m not quite sure what you’re saying?

If you want to warn every time that there’s a cyclic dependency, that would be a non-starter to me because I assume it’s going to be very, very annoying to have a ton of warnings about something that’s perfectly fine if done right.

@devsnek
Copy link
Member

devsnek commented Oct 11, 2019

@addaleax yeah i mean starting with just detecting all circulars. I think most of it is probably working by chance, not by intention. (I'm not blocking anything here though)

@addaleax
Copy link
Member Author

addaleax commented Oct 11, 2019

@devsnek So … maybe this is just my experience, but I think circular dependencies aren’t all that uncommon, and if they are working, it’s often not just by chance but because somebody figured out that they didn’t work on the first try and then spent a while fixing them. (I definitely had that experience in the past, and iirc the same thing happened with circular dependencies in Node’s internal modules – e.g. 413fcad or 355523f)

So if we do want to warn on all circular modules, I would not make that the default mode, and I would like to have something that warns about potential issues that is on by default.

@nodejs-github-bot
Copy link
Collaborator

nodejs-github-bot commented Oct 11, 2019

@boneskull
Copy link
Contributor

would this somehow impact code that clears the contents the require cache?

I don’t really have time rn to think of and/or show examples of how to break this, but I am a bit concerned about unintended consequences of changing the prototype, and somewhat less concerned about false positives. mainly around modules like proxyquire, rewire, esm, etc

@guybedford
Copy link
Contributor

I suspect that these warnings may be more common than we expect in running code adding to noise on existing apps, but apart from that this approach seems fine to me.

@targos
Copy link
Member

targos commented Oct 11, 2019

@addaleax have you tried running node with this change on an app, or using some CLI tools?

@targos
Copy link
Member

targos commented Oct 11, 2019

If there was a node --throw-warnings option, we could run CITGM with it... How about we try with an alternative branch that throws to test the impact on the ecosystem?

@addaleax
Copy link
Member Author

would this somehow impact code that clears the contents the require cache?

@boneskull I don't think so. The only interaction with the require cache that I can see is what would happen if a module deleted itself from the require cache while it is being loaded (whyever one would do that), but even in that scenario all that this PR would change is that it would not print a warning.

I don’t really have time rn to think of and/or show examples of how to break this, but I am a bit concerned about unintended consequences of changing the prototype, and somewhat less concerned about false positives. mainly around modules like proxyquire, rewire, esm, etc

I can understand that, and I was a bit worried about this when writing the code, but after thinking it through I'm not really seeing this as a major concern. The prototype is always reset after the module is loaded, and only if it hasn't been changed in the meantime anyway, and all methods on it are still there during the whole process. The main way this is observable, besides the warnings, would be when somebody checks whether module.exports.__proto__ === Object.prototype explicitly, but then ... why would one do that?

I suspect that these warnings may be more common than we expect in running code adding to noise on existing apps, but apart from that this approach seems fine to me.

@guybedford Yes, that would be my biggest concern here too. I've run CITGM with that in mind, and I also like @targos' suggestion of turning this a throw and then running CITGM with that.

@addaleax have you tried running node with this change on an app, or using some CLI tools?

Not yet, no.

@addaleax
Copy link
Member Author

Actually, it looks like this interferes with TypeScript enums, where something like

export const enum Foo { A = 1, B = 2 };

can result in something like

(function (Foo) {
    Foo[Foo["A"] = 1] = "A";
    Foo[Foo["B"] = 2] = "B";
})(Foo = exports.Foo || (exports.Foo = {}));

so that might actually require getting a bit fancier here… maybe we need to exclude the module file itself from these warnings.

@addaleax
Copy link
Member Author

Thinking about it, I think it’s a good idea to exclude transpiled TS/ESM code altogether, at least for now. ESM kind of has a different idea of how to deal with cyclic dependencies anyway, and already throws an explicit error for TDZ accesses.

Here’s a CITGM for this code + throwing instead of warning so that we get meaningful results: https://ci.nodejs.org/view/Node.js-citgm/job/citgm-smoker/2051/

addaleax added a commit to addaleax/cli that referenced this pull request Oct 11, 2019
`lib/pack.js` and `lib/config/figgy-config.js` load each other,
making `figgy-config.js` grab the original `module.exports` value
and not the intended one. In particular, this always sets the
`dirPacker` value to `undefined` in the config generation step.

Fix this by setting `module.exports` early.

Refs: nodejs/node#29935
@addaleax
Copy link
Member Author

So … the bad news is, CITGM is failing because npm pack always encounters this condition.

The good news is: it’s not a false positive, but rather a genuine bug with npm of the exact kind that this PR is supposed to catch. Yay? :)

Here’s another CITGM with a hack to silence the warning for that instance in npm: https://ci.nodejs.org/view/Node.js-citgm/job/citgm-smoker/2052/

@DerekNonGeneric
Copy link
Contributor

I would gladly welcome a warning like this. People can easily pass the --no-warnings to silence it.

Here's an issue that I'm not sure how to resolve, though. Say someone is already using --no-warnings to silence all the warnings from --experimental-modules et al. Would it then be impossible to opt-in to this particular warning?

@addaleax
Copy link
Member Author

Here's an issue that I'm not sure how to resolve, though. Say someone is already using --no-warnings to silence all the warnings from --experimental-modules et al. Would it then be impossible to opt-in to this particular warning?

@DerekNonGeneric So … yes, for the moment that would be the case – --no-warnings does what it says on the tin, it disables all warnings.

I think that brings up an interesting point though, namely whether we should have a way to disable specific kinds of warnings… maybe that would be helpful?

@addaleax
Copy link
Member Author

addaleax commented Oct 18, 2019

Sifting through the CITGM results:

  • bluebird: shows genuine bug in outdated version of sinon dependency
  • q: shows genuine bug in outdated version of sinon dependency
  • sax: non-bug in shelljs dependency
  • yeoman-generator: non-bug in shelljs dependency

The other failures seem like unrelated, pre-existing CITGM failures.

I will open a PR against shelljs that addresses this as far as their side is concerned, and suggest maybe landing this as a semver-major PR to be on the safe side and give us time to look into other false positives.

@nodejs/tsc Thoughts on that approach?

addaleax added a commit to addaleax/shelljs that referenced this pull request Oct 18, 2019
Node.js is currently considering printing a warning when a non-existent
property of `module.exports` is accessed while in a circular `require()`
dependency, in order to make it easier to catch issues with circular
dependencies.

In order to avoid printing these warnings for shelljs, checking for the
property’s existence rather than its truthiness suffices.

Refs: nodejs/node#29935
@tniessen
Copy link
Member

This would definitely be nice to have, at the very least as an opt-in feature.

nfischer pushed a commit to shelljs/shelljs that referenced this pull request Oct 20, 2019
Node.js is currently considering printing a warning when a non-existent
property of `module.exports` is accessed while in a circular `require()`
dependency, in order to make it easier to catch issues with circular
dependencies.

In order to avoid printing these warnings for shelljs, checking for the
property’s existence rather than its truthiness suffices.

Refs: nodejs/node#29935
@addaleax addaleax added the semver-major PRs that contain breaking changes and should be released in the next major version. label Oct 23, 2019
@nodejs-github-bot
Copy link
Collaborator

@addaleax
Copy link
Member Author

Can I get another @nodejs/tsc review? I’ve labelled this semver-major, and if we do run into trouble in the next 6 months, we can still put this behind a flag if necessary.

mikemimik pushed a commit to npm/cli that referenced this pull request Oct 29, 2019
`lib/pack.js` and `lib/config/figgy-config.js` load each other,
making `figgy-config.js` grab the original `module.exports` value
and not the intended one. In particular, this always sets the
`dirPacker` value to `undefined` in the config generation step.

Fix this by setting `module.exports` early.

Refs: nodejs/node#29935

PR-URL: #266
Credit: @addaleax
Close: #266
Reviewed-by: @mikemimik
@addaleax addaleax added the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Oct 29, 2019
@nodejs-github-bot
Copy link
Collaborator

addaleax added a commit that referenced this pull request Nov 1, 2019
Warn when a non-existent property of an unfinished module.exports
object is being accessed, as that very often indicates the presence
of a hard-to-detect and hard-to-debug problem.

This mechanism is only used if `module.exports` is still a
regular object at the point at which the second, circular `require()`
happens.

The downside is that, temporarily, `module.exports` will have a
prototype other than `Object.prototype`, and that there may
be valid uses of accessing non-existent properties of unfinished
`module.exports` objects.

Performance of circular require calls in general is not
noticeably impacted.

                                               confidence improvement accuracy (*)   (**)  (***)
     module/module-loader-circular.js n=10000                 3.96 %       ±5.12% ±6.82% ±8.89%

Example:

    $ cat a.js
    'use strict';
    const b = require('./b.js');

    exports.fn = () => {};
    $ cat b.js
    'use strict';
    const a = require('./a.js');

    a.fn();
    $ node a.js
    (node:1617) Warning: Accessing non-existent property 'fn' of module exports inside circular dependency
    /tmp/b.js:4
    a.fn();
      ^

    TypeError: a.fn is not a function
        at Object.<anonymous> (/tmp/b.js:4:3)
        [...]

PR-URL: #29935
Reviewed-By: Michaël Zasso <[email protected]>
Reviewed-By: Colin Ihrig <[email protected]>
@addaleax
Copy link
Member Author

addaleax commented Nov 1, 2019

Landed in d7452b7

@addaleax addaleax closed this Nov 1, 2019
@addaleax addaleax deleted the circular-dependency-warning branch November 1, 2019 19:15
addaleax added a commit to addaleax/node that referenced this pull request Apr 24, 2020
Since this property access is performed by generated code, and not
used for accessing the actual exports of a module (and because
transpilers generally define it as the first key of `module.exports`
when it *is* present), it should be okay to allow it.

Refs: nodejs#29935
Fixes: nodejs#33046
nfischer pushed a commit to shelljs/shelljs that referenced this pull request Apr 25, 2020
Node.js is currently considering printing a warning when a non-existent
property of `module.exports` is accessed while in a circular `require()`
dependency, in order to make it easier to catch issues with circular
dependencies.

In order to avoid printing these warnings for shelljs, checking for the
property’s existence rather than its truthiness suffices.

Refs: nodejs/node#29935
addaleax added a commit that referenced this pull request Apr 27, 2020
Since this property access is performed by generated code, and not
used for accessing the actual exports of a module (and because
transpilers generally define it as the first key of `module.exports`
when it *is* present), it should be okay to allow it.

Refs: #29935
Fixes: #33046

PR-URL: #33048
Reviewed-By: Guy Bedford <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Zeyu Yang <[email protected]>
BethGriggs pushed a commit that referenced this pull request Apr 27, 2020
Since this property access is performed by generated code, and not
used for accessing the actual exports of a module (and because
transpilers generally define it as the first key of `module.exports`
when it *is* present), it should be okay to allow it.

Refs: #29935
Fixes: #33046

PR-URL: #33048
Reviewed-By: Guy Bedford <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Zeyu Yang <[email protected]>
BethGriggs pushed a commit that referenced this pull request Apr 28, 2020
Since this property access is performed by generated code, and not
used for accessing the actual exports of a module (and because
transpilers generally define it as the first key of `module.exports`
when it *is* present), it should be okay to allow it.

Refs: #29935
Fixes: #33046

PR-URL: #33048
Reviewed-By: Guy Bedford <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Zeyu Yang <[email protected]>
BethGriggs pushed a commit that referenced this pull request Apr 28, 2020
Since this property access is performed by generated code, and not
used for accessing the actual exports of a module (and because
transpilers generally define it as the first key of `module.exports`
when it *is* present), it should be okay to allow it.

Refs: #29935
Fixes: #33046

PR-URL: #33048
Reviewed-By: Guy Bedford <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Zeyu Yang <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
author ready PRs that have at least one approval, no pending requests for changes, and a CI started. module Issues and PRs related to the module subsystem. semver-major PRs that contain breaking changes and should be released in the next major version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants