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

feat: Suppression support #82

Merged
merged 11 commits into from Nov 24, 2021
Merged

Conversation

Yiwei-Ding
Copy link
Contributor

Summary

Violations can be suppressed by inline directive comments. We propose to add an --output-suppression CLI option for adding suppression information, including its kind and justification, in the output of ESLint.

Related Issues

eslint/eslint#14784 Add suppression information in the output of ESLint

@nzakas nzakas added Initial Commenting This RFC is in the initial feedback stage and removed triage labels Aug 14, 2021
Copy link
Member

@nzakas nzakas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is very helpful. I think this is a potentially useful feature. I left some comments on the areas where I think we need some clarification. We will also need some feedback from the rest of the team.

designs/2021-suppression-support/README.md Outdated Show resolved Hide resolved
designs/2021-suppression-support/README.md Outdated Show resolved Hide resolved
designs/2021-suppression-support/README.md Outdated Show resolved Hide resolved
designs/2021-suppression-support/README.md Outdated Show resolved Hide resolved
@Yiwei-Ding Yiwei-Ding requested a review from nzakas August 24, 2021 01:36
Copy link
Member

@nzakas nzakas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I left a few more comments after reviewing this a bit more.

designs/2021-suppression-support/README.md Outdated Show resolved Hide resolved
designs/2021-suppression-support/README.md Outdated Show resolved Hide resolved
designs/2021-suppression-support/README.md Outdated Show resolved Hide resolved
@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented Aug 31, 2021

CLA Signed

The committers are authorized under a signed CLA.

@Yiwei-Ding Yiwei-Ding requested a review from nzakas August 31, 2021 06:25

## Summary

Violations can be suppressed by inline directive comments and external configuration files. We propose to add an `--track-suppressions` CLI option for adding suppression information, including its kind and justification, in the output of ESLint.
Copy link
Sponsor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think the word "suppressed" or "suppression" is appropriate here. inline comments can also enable rules, or reconfigure them.

What happens with this CLI option when an inline comment causes an error to appear that wouldn't have otherwise done so?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are only tracking when a rule is disabled for this RFC. Enabling a rule with a directive doesn’t change how this behaves.

Is there a better word you can suggest?

Copy link
Sponsor

@ljharb ljharb Aug 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So If i reconfigure an enabled rule such that it provides fewer errors, that would not be caught by this arg, despite that being conceptually the same kind of "suppression" as disabling a rule entirely?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. We care only about rules that are completely turned off

Copy link
Sponsor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then this isn't about warning suppression at all, it's just about disabled rules (which, incidentally, seems much much less useful than caring about "a comment that reduced the number of observed warnings).

Perhaps --track-inline-disabled-rules, since that's all it's doing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s also tracking rules disabled in configs, not just inline.

And really, it’s not tracking that the rule is disabled but rather the messages that would have been produced by a disabled rule. “Suppression” refers to the message being squashed rather than the rule being disabled.

Copy link
Sponsor

@ljharb ljharb Sep 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know it’s not just inline - in that case it’s only about disabled rules - meaning, eslint somehow tracks which rules are configured to be on, with an “off” trampling it later?

--track-disabled-rules still seems like a better option to me in that case.

What about rules that would error but are set to warn instead, when --quiet is enabled?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eslint somehow tracks which rules are configured to be on, with an “off” trampling it later?

You may want to read the rest of the RFC. 😄

I just don’t think “track disabled rules” is an accurate phrase for what this is doing.

Copy link
Sponsor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have, altho it’s long and i may have missed something, but what i read implies that disabling a rule in a config file isn’t something eslint tracks. Is this rfc attempting to change that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might have to change the logic of skipping rules disabled in configuration files, as described in "Reserve external suppression" section of the RFC.


## Alternatives

No.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an alternative: you can create an eslint rule to report the usage of eslint-disable-*. eslint-plugin-eslint-comments sounds like a good place to have.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd like to track external suppressions as well. Reporting the usage of eslint-disable-* seems not all we want exactly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as external suppressions, you can just read the eslintrc, to find which rules were disabled:

config.rules.filter([rulename, ruleOptions] => ruleOptions === "off" || ruleOptions === 0);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to clarify that we are not going to track what rules are suppressed (or switched to 'off'). What we want is tracking violations (or warnings/errors) in the code. For example:

a = 1;
console.log('hello');
b = 2;
rules: {
  "no-undef": "error"
}

We'd like to get the suppression info of line 1 (a = 1) and line 3 (b = 2) instead of the message that the rule 'no-undef' is switched to 'off'. No errors or warnings would be output, but these suppressions are expected to be recorded. As described in the RFC, ESLint would not skip rules disabled in configuration files if --track-suppressions is applied.

Copy link
Sponsor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Yiwei-Ding when what switches it to off? another config, or only an inline override comment?

Copy link
Contributor Author

@Yiwei-Ding Yiwei-Ding Sep 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For inline suppressions, we would keep the current logic basically. Violations (or problems in ESLint) disabled by inline directive comments are expected to be tracked but would not cause errors/warnings if '--track-suppressions'.

For external suppressions (where rules are switched to 'off' in configuration files), ESLint would not skip rules switched to 'off' if '--track-suppressions' is used. All rules would be ran and the generated problems belonging to disabled rules would be tagged 'Globally disabled'. In this RFC, when we talk about something is switched to 'off', we are talking about external suppressions (rules are switched to 'off' in configuration files).

if '--track-suppressions' is not used, we'd like to keep the current behavior and logic of ESLint.

Does it answer your questions?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, I see.

so, the alternative can be creating a new eslintrc based the original:

const base = require('./eslintrc.js');  // this is your original config

base.rules = find_disabled_rules(base);  // find global-disabled rules in the config, and set to "error"
base.rules['no-eslint-disable'] = "error"; // an eslint rule to report all the usage "eslint-disable-*"

modules.exports = base;

thus, you can run eslint -c to get all suppressions. does it match your use case?

Copy link
Sponsor

@ljharb ljharb Sep 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when a rule is switched off (from a shared config) in a config file because it crashes eslint, or because it's very slow?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aladdin-add I didn't get your idea..

Engineers prefer to suppress warnings or errors that would break CI rather than fix them. These suppressions are inevitable but we want to reduce them as much as possible to improve code quality. That is why we are going to track these suppressions.

So in this RFC, suppressions are expected to output from formatters (JSON, SARIF or any else) instead of causing warnings/errors to break pipelines. "no-eslint-disable": "error" would report inline directive comments as errors, right? That is not what we want.

Copy link
Member

@btmills btmills left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can also see this being useful. I can see how tracking inline comment directives will work, but I have some reservations about rules that are disabled via config files and CLI flags.

designs/2021-suppression-support/README.md Show resolved Hide resolved
designs/2021-suppression-support/README.md Outdated Show resolved Hide resolved
Copy link
Sponsor

@ljharb ljharb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this change solve your use case? A developer could still use an override to set a rule to “off”, but now track-suppressions won’t track it.

designs/2021-suppression-support/README.md Outdated Show resolved Hide resolved
@Yiwei-Ding
Copy link
Contributor Author

Thanks for all comments and suggestions! We decided to remove external suppression in this RFC. Will update the doc soon.

@ljharb
Copy link
Sponsor

ljharb commented Oct 19, 2021

With the latest changes, what does this give you that https://www.npmjs.com/package/eslint-plugin-eslint-comments does not?

@Yiwei-Ding
Copy link
Contributor Author

Yiwei-Ding commented Oct 19, 2021

@ljharb Thanks for the concern.

We are not going to analyze directive comments. Our goal is to track suppressions for auditing purposes. The action that directive comments work on violations is considered as suppressions.

If a rule is turned off in configuration files, the related violations (problems) will not be created, where the corresponding directive comments will not work as well. In this case, no suppressions happened and nothing should be considered as suppressions to be recorded.

@ljharb
Copy link
Sponsor

ljharb commented Oct 19, 2021

Right - but that eslint plugin demonstrates how you can track the occurrence of inline override comments, so can’t you assume that N comments means N suppressions? (there’s a chance a comment won’t be suppressing anything, but there’s already a config switch to show those warnings, so you can collect them too and subtract them out)

@Yiwei-Ding
Copy link
Contributor Author

That's not what we expect. Only there's a violation, there could be a suppression. If there's not a violation, there would not be a suppression.

As described in the RFC, if multiple directive comments happen to work on the same line and the same rule, the output suppression info should be integrated into one suppression list. In this case, one violation is suppressed once by the list.

"messages":[
      {
        "ruleId":"no-undef",
        "severity":2,
        "message":"'a' is not defined.",
        "line":1,
        "column":1,
        "nodeType":"Identifier",
        "suppressions":[
          { "kind":"directive", "justification":"Sample justification message." },
          { "kind":"directive", "justification":"Another justification message." }
        ],
        "messageId":"undef",
        "endLine":1,
        "endColumn":2
      }
    ]

And I believe the new feature could be helpful to ESLint users :)

Comment on lines 268 to 275
If a violation is inline-suppressed multiple times, all suppression entries will be recorded, according to [suppressions property](https://docs.oasis-open.org/sarif/sarif/v2.0/csprd02/sarif-v2.0-csprd02.html#_Toc10127852). So `unusedDisableDirectives` might always be empty. A suppression list would be preferred to gather all suppression information of a violation:

```json
suppressions: [
{"kind": "directive", "justification": "Justification message 1"},
{"kind": "directive", "justification": "Justification message 2"}
]
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently don't have this info - only one directive applies, while the others are marked and reported as unused. I think this is an edge case that doesn't warrant the added complexity and confusion that would be caused by directives that are at the same time used (as their justifications appear here) and unused (as they're reported as such).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, currently if a violation is suppressed by multiple directives, the first suppression will work while the rest of them will be treated as unusedDisableDirectives, as described in the section Suppression in ESLint.

What we propose to do is that when using the new option --track-suppressions, these suppressions for one violation could be gathered into a suppression list. Without the option, everything works as it is currently. This changes in code would be in function applyDirectives(options).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ESLint already marks redundant directives as "unused", and that's observable when the --report-unused-disable-directives flag is used. Furthermore, they cause exit code 1 if this flag is used.

What should be the behavior when both flags are used at the same time? It doesn't make sense to report a directive as unused and to include the same directive in the list of directives that have applied.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If --track-suppression used, directives that do suppress violations will be considered as "used". But those don't suppress violations will still be tagged as "unused".

Fox example:

/* eslint-disable eqeqeq -- Justification 1 */
// eslint-disable-next-line no-undef -- Justification 2
a=1; // eslint-disable-line no-undef -- Justification 3

In this code, // eslint-disable-next-line no-undef -- Justification 2 and // eslint-disable-line no-undef -- Justification 3 are used to suppress the violation "'a' is not defined". /* eslint-disable eqeqeq -- Justification 1 */ does nothing so it will be "unused" and be reported when --report-unused-disable-directives is used.

I'll update the RFC to clarify it if you are good to this design.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the purpose of this new flag should only be to provide more information.

Consider this example:

// eslint-disable-next-line no-undef -- Justification 1
a=1; // eslint-disable-line no-undef -- Justification 2

If we run eslint foo.js --report-unused-disable-directives, the outcome is exit code 1.

If we run eslint foo.js --report-unused-disable-directives --track-suppressions, the outcome would be exit code 0.

So, a flag used to output additional info about the linting has side-effects on linting: it removes an error (the error on unused disable directive) and alters the linting outcome.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @mdjermanovic , we will keep the current behavior. In this example:

// eslint-disable-next-line no-undef -- Justification 1
a=1; // eslint-disable-line no-undef -- Justification 2

the unused-error should still be throw out if we run eslint foo.js --report-unused-disable-directives --track-suppressions.

And we'd like to gather both used and unused suppressions, i.e., we will get a suppression list with 2 items for the above code if we run eslint foo.js --track-suppressions:

"suppressions": [
  { "kind": "directive", "justification": "Justification 1" },
  { "kind": "directive", "justification": "Justification 2" }
]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// eslint-disable-next-line no-undef -- Justification 1
a=1; // eslint-disable-line no-undef -- Justification 2

the unused-error should still be throw out if we run eslint foo.js --report-unused-disable-directives --track-suppressions.

What would be in "suppressions": [] in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as eslint foo.js --track-suppressions.

--report-unused-disable-directives will report the "unused" error, --track-suppressions will gather and output suppressions. They would be independent, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--report-unused-disable-directives will report the "unused" error, --track-suppressions will gather and output suppressions. They would be independent, right?

Yes, in that case the options would work independently of each other, which looks good.

What still looks confusing is that the same directive appears as both used and unused in the same output.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The directive is suppressing but is unused at the same time 😄

## Drawbacks

- Additional contexts must be transferred across modules, such as `justification` in `directives` and `suppressions` in `LintMessage`.
- Related formatters, such as stylish, should be modified to keep the current behavior.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide more details on how it should be modified? Should it filter out suppressed problems from the results?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, formatters should filter out those suppressed problems.

Take stylish as an example, messages passed into stylish could have a suppressions property when using --track-suppressions. Currently, stylish just gets the messages and displays them on CLI, regardless they are suppressed or not. We have to modify it to filter out suppressed ones.


## Backwards Compatibility Analysis

Basically, current users would not feel any difference, since the proposed feature would only work with the new option. Without the option `--track-suppressions`, any current behaviors would not be changed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Third-party formatters might become incompatible, depending on how we implement this feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on how third-party formatters process LintMessage. Only using the new option --track-suppressions, there could be incompatible problems. Without the option, nothing will be affected.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If user runs eslint with --track-suppressions and an existing third-party formatter, it will show suppressed errors as if they were not suppressed. I think this is a compatibility issue we should address.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a support formatter list for --track-suppressions and drive the owners of third-party formatters to support the suppressions property?

When using a formatter that --track-suppressions not supports, ESLint would throw a warning message like "The formatter is not supported by the option --track-suppressions" and keep working as the incompatible mode.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what would be the best approach.

Some thoughts:

  • Instead of mixing suppressed and non-suppressed problems in messages, maybe we could add a new property suppressedMessages (array). In that case, an option might even be unnecessary? (we could just always provide them and it would be up to the formatter to merge and show all, or entirely ignore suppressedMessages as all the existing formatters are doing now).
  • Adding an ability for formatters to define features they support, and throw an error if the given formatter doesn't support suppressions. If the formatter doesn't support supressions, that wouldn't mean that it is incompatible and that it needs to release a new version, just that it isn't designed to be used with --track-suppressions.

Copy link
Contributor Author

@Yiwei-Ding Yiwei-Ding Nov 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm a JSON formatter user, I would feel strange that so many suppressed messages come to my output after updating ESLint... I suppressed so I don't want to see those violations.

Maybe an explicit option could give users a clear idea whether tracking suppressions is working.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it’s okay to always return the suppressed messages. The JSON formatter is primarily used by automation to transform the results output into another format so it’s unlikely to cause problems.

Copy link

@DSIE63 DSIE63 Nov 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So seems like we have an agreement here, which is we will not need to add the --track-suppressions switch and always output the suppressions to a separate property (suppressedMessages). Formatters that know about this new property can merge and handle the messages accordingly, and for formatters that don't then it is an NoOp.

Agreed?

If we all agree, we will move forward to making the code changes and submit PR for reviews when we are ready.

Thanks,

Daniel Sie

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @mdjermanovic @nzakas @DSIE63 , the RFC is updated and the option related things are removed now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DSIE63 please wait until the RFC has been approved before submitting code.

@mdjermanovic mdjermanovic changed the title New: Suppression support feat: Suppression support Oct 26, 2021
@Yiwei-Ding
Copy link
Contributor Author

It is nearly 90 days since the RFC created. Could we move forward?

Copy link
Member

@nzakas nzakas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m happy with where we are with this RFC now. I’d like to suggest we move to the final commenting period.

@@ -422,7 +422,7 @@ NOTE that the kind `directive` should be converted to `inSource` for SARIF accor

### Return `SuppressedLintMessage` in `Linter#verify` and `Linter#verifyAndFix`

Since we have a new property `suppressedMessages` in `LintResult`, the output of `Linter#verify` and `Linter#verifyAndFix` will also have `suppressedMessages`.
Since we have a new property `suppressedMessages` in `LintResult`, the output of `Linter#verify` and `Linter#verifyAndFix` will also have `suppressedMessages`. That is, `Linter#verify` will return `{messages: LintMessage[], suppressedMessages: SuppressedLintMessage[]}`, and `Linter#verifyAndFix` will return `{fixed: boolean, messages: LintMessage[], output: string, suppressedMessages: SuppressedLintMessage[]}`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can’t change these methods to return an object. That would be a significant breaking change to the API. The options are:

  1. Return an array with an added suppressedMessages property.
  2. Add a getSupressedMessages() method to Linter that can retrieve the suppressed messages from the previous run.

Copy link
Sponsor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arrays with added properties are very weird; aside from regexp match objects, they rarely happen, and that's a good thing - out of those two options, the second seems far better (and also avoids penalizing the common case of not needing this info)

Copy link
Member

@nzakas nzakas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it. 👍

@Yiwei-Ding
Copy link
Contributor Author

Could we move to the Final Commenting period now?😊

@nzakas
Copy link
Member

nzakas commented Nov 18, 2021

Moving to final commenting.

@nzakas nzakas added Final Commenting This RFC is in the final week of commenting and removed Initial Commenting This RFC is in the initial feedback stage labels Nov 18, 2021
Copy link
Member

@btmills btmills left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m happy with where this ended up!

@nzakas
Copy link
Member

nzakas commented Nov 24, 2021

Approved! Thanks for everyone’s patience on this. I know it took a while but we ultimately ended up with what I think is a good solution.

@nzakas nzakas merged commit 12fe14b into eslint:main Nov 24, 2021
aladdin-add pushed a commit to aladdin-add/rfcs that referenced this pull request Jul 12, 2022
* New: Suppression support

* Some clarifications

* Modify the statement of NOTE

* Update to better fit with ESLint terminology

* Add new severity suppress

* Remove external suppression

* Clarify unused issue and Add SuppressedMessage

* Remove the option since it is by default

* Remove formatter related and add Linter API description

* Clarify Linter API

* Add getSuppressedMessages
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Final Commenting This RFC is in the final week of commenting
Projects
None yet
7 participants