Skip to content

Instantly share code, notes, and snippets.

@not-an-aardvark
Last active February 19, 2019 20:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save not-an-aardvark/169bede8072c31a500e018ed7d6a8915 to your computer and use it in GitHub Desktop.
Save not-an-aardvark/169bede8072c31a500e018ed7d6a8915 to your computer and use it in GitHub Desktop.

Overview of options and tradeoffs for throwing an error on duplicate plugin names

The main blocker for eslint/eslint#3458 (allowing shareable configs to manage their own plugin dependencies) has been a concern that two shareable configs could depend on two different versions of a plugin. Currently, ESLint's mechanism for referring to a rule from a config (with pluginName/ruleName) implicitly assumes plugin names are globally unique. This has led to proposals like eslint/rfcs#5 that attempt to remove the assumption that plugin names are globally unique, by giving config authors a way to disambiguate plugins with the same name.

The case where two plugins have the same name seems like it would be somewhat rare. As an alternative to a disambiguation mechanism, a few people have suggested simply raising an error when a name conflict happens (i.e. declaring that we don't support that case), which would avoid the complexity of proposals like eslint/rfcs#5. In this overview, I'll explain why "simply raising an error" here still creates some nontrivial design questions, and summarize some approaches that have been suggested/discussed for how to resolve those questions.

(Note: The examples below use .eslintrc syntax and terminology, but all of the design decisions would also apply to the proposed new config format in eslint/rfcs#7.)

Constraint: Robustness Guarantee

Overall, I think it's completely fine for us to decide not to support rare cases like this. The important thing is that we continue to provide the following "robustness guarantee":

A user should be able to reliably avoid unsupported cases when creating a config.

In other words, we shouldn't have a situation where a user's config is initially valid, then it suddenly moves into unsupported territory later due to events outside of the user's control (e.g. a new patch release of a dependency). This is the difference between "ESLint doesn't support some cases" and "ESLint randomly breaks sometimes" -- I think the former is acceptable, but the latter is unacceptable for a stable tool even if the random breakage only happens rarely. In this overview, I'm assuming that the robustness guarantee is uncontroversial.

How to define plugin conflicts

If we decide to throw an error when plugin conflicts happen, the robustness guarantee is relevant to how we define a "plugin conflict". There are a few options for defining plugin conflicts:

  1. Raise an error whenever two different extends clauses result in loading plugins with the same name.

    This strategy appears to be overly strict in the case of plugin-provided configs. Sometimes, the two extends clauses are actually intending to load the same plugin, rather than potentially-different versions of a plugin. For example, this strategy would result in a fatal error given the following config, because a plugin called foo is getting loaded twice:

    {
        "extends": [
            "plugin:foo/ruleset-a",
            "plugin:foo/ruleset-b"
        ]
    }
  2. Raise an error whenever two different extends clauses result in loading distinct plugins with the same name. (In other words, tolerate multiple plugins references to with same name, as long as they resolve to the same plugin.)

    This strategy is an attempt to resolve the issue from strategy (1), and is the approach used by eslint/rfcs#7 at the time of writing. Unfortunately, this strategy doesn't provide the robustness guarantee. Consider the following case:

    // .eslintrc.js
    module.exports = {
        extends: ['configA', 'configB']
    };
    // eslint-config-configA/package.json
    {
        "dependencies": {
            "eslint-plugin-foo": "^1.0.0"
        }
    }
    // eslint-config-configA/index.js
    module.exports = {
      plugins: ['foo'],
      rules: { /* some rules from eslint-plugin-foo */ }
    };
    // eslint-config-configB/package.json
    {
        "dependencies": {
            "eslint-plugin-foo": "=1.0.0"
        }
    }
    // eslint-config-configB/index.js
    module.exports = {
      plugins: ['foo'],
      rules: { /* some other rules from eslint-plugin-foo */ }
    };

    If 1.0.0 is the latest version of eslint-config-foo, and the user's package manager flattens duplicate packages, then the end user's config will work correctly because both eslint-config-configA and eslint-config-configB load the same version of eslint-plugin-foo. However, if eslint-plugin-foo@1.0.1 is released later to fix a bug, then eslint-config-configA and eslint-config-configB will start loading different versions of eslint-plugin-foo, causing the user's config to suddenly break. This violates the robustness guarantee. (Depending on the implementation details of the user's package manager, it might also be necessary for eslint-config-configA to switch to eslint-plugin-foo@^1.0.1 to trigger this issue.)

  3. Raise an error whenever two different extends clauses in different configs result in loading plugins with the same name

    In other words, the example from (1) would be allowed because it's obvious that both plugin:foo/ruleset-a and plugin:foo/ruleset-b are intended to refer to the same version of eslint-plugin-foo, given that they appear in the same config. But if the user's shareable config has extends: plugin:foo/ruleset-a, the user wouldn't be able to use extends: plugin:foo/ruleset-b in their own config.

  4. Raise an error whenever two different extends clauses in configs which are not ancestors of each other result in loading plugins with the same name

    In other words, this would allow everything from (3), and also the case where a user extends a plugin config, and the user's shareable config also extends a config from that plugin. This seems to support most of the necessary use cases, and also the robustness guarantee. However, it could lead to some additional details to work out (for example, could a sharable config reconfigure plugin rules from its sibling if it didn't introduce that plugin? What happens if a user's config tries to reintroduce a plugin that was already introduced by one of its descendants?)

  5. ...Other ideas?

    I don't think we've fully explored this design space yet. It's possible that there are other mechanisms for identifying conflicts that still obey the robustness guarantee and would create fewer perceived false positives.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment