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

NEW (Extension) @W-13991634@ Add class level PMD violations #91

Merged
merged 10 commits into from
Jun 10, 2024
Merged

Conversation

jag-j
Copy link
Collaborator

@jag-j jag-j commented Jun 5, 2024

This PR does the following:

  1. Injects PMD's class-level suppression syntax to silence a single rule for that class (i.e., @SuppressWarnings syntax).
  2. Identifies the specific rule to be suppressed and adds the appropriate syntax (Example: @SuppressWarnings('PMD.ApexDoc')).
  3. If a class-level suppression tag already exists, it changes the SuppressWarnings accordingly (Example: @SuppressWarnings('PMD.ApexDoc', 'PMD.UnusedMethod')), instead of adding a new tag.
  4. Adds the tag for Apex and Java appropriately (double-quotes for Java).
  5. Identifies the correct position to add this tag by regex parsing the file for 'class', but at the same type ignoring 'class' defined as a comment or as part of a string.


action.diagnostics = [this.diagnostic];
action.command = {
command: Constants.COMMAND_REMOVE_DIAGNOSTICS_ON_SELECTED_FILE,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

FYI, I clear the diagnostics for the whole file and this is not ideal when a file has multiple classes. Keeping track of the diagnostics that would be affected by the rule added on the specific class seemed too difficult and the AC for the story did not call for it. Since this PR is already doing too many things, I could still do it as part of a separate story.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Clarification: Does "multiple classes" mean multiple outer classes, or an outer class with inner classes? I know Apex can't have multiple outer classes, and I'm pretty sure that the annotation will only apply to outer classes, though I haven't checked that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Didn't realize apex can't have multiple outer classes. I have changed the code and now this applies only to the outer class.

src/lib/fixer.ts Outdated
public generateClassLevelSuppression(): vscode.CodeAction {
// Find the end-of-line position of the class declaration where the diagnostic is found.
const classStartPosition = this.findClassStartPosition(this.diagnostic, this.document);
// const classEndOfLinePosition = 0;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please remove commented out code.

Comment on lines +167 to +168
const existingRules = suppressionMatch[1].split(',').map(rule => rule.trim());
if (!existingRules.includes(suppressionRule)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just wondering about case sensitivity of the rules. If they are case insensitive, then we probably should add to the map a .toLowerCase() and then add it to the suppressionRule when checking for existence.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Follow-up question: Is it an error case if the rule is already present, or is it valid and nothing should happen?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have added case insensitivity in the regex and added tests. It is an error case when a rule is already present and violations are present. One scenario I can think of is when user ran the scanner without the rule and manually added the rule and then tried to run the suppress at the class level. I'm not handling that part.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Adding case insensitivity in the regex doesn't help with this line does it existingRules.includes(suppressionRule)) ?

I'd recommend something like
const existingRules = suppressionMatch[1].split(',').map(rule => rule.trim().toLowerCase());
if (!existingRules.includes(suppressionRule.toLowerCase())) {

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As discussed offline, this has an unintended side effect. It changes all existing rules to lower case since we concatenate the existing rules (which were made to be lower case for comparison) to new rules. So, we decided to leave this as is.

Comment on lines +211 to +216
/**
* Finds the start position of the class in the document.
* Assumes that the class declaration starts with the keyword "class".
* @returns The position at the start of the class.
*/
public findClassStartPosition(diagnostic: vscode.Diagnostic, document: vscode.TextDocument): vscode.Position {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I was confused at first by this... I didn't realize until I read the code that you always return column 0. This sounds like it is the position (line, col) exactly where you find the word "class". But in reality you are finding the starting line of the class and returning the zeroth column. I recommend you update your comment to make this clear. Otherwise, I was scratching my head earlier in the code wondering how you could put a newline character immediately before the word "class".

src/lib/fixer.ts Outdated Show resolved Hide resolved
public isWithinQuotes(line: string, matchIndex: number): boolean {
const beforeMatch = line.slice(0, matchIndex);
const singleQuotesBefore = (beforeMatch.match(/'/g) || []).length;
const doubleQuotesBefore = (beforeMatch.match(/"/g) || []).length;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does apex allow double quotes? I thought strings must be single quoted? Not sure if you need to worry about double quotes.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Strings have to be single-quoted. I genuinely don't remember if you're allowed to use double-quotes as string boundaries.
BUT you might want to check whether the string containing escaped single-quotes throws off the algorithm. E.g., 'this is a string. it\'s got a single-quote inside it'.

src/lib/fixer.ts Outdated
Comment on lines 296 to 299
// Check if the number of quotes before the match is odd (inside quotes)
if (singleQuotesBefore % 2 !== 0 || doubleQuotesBefore % 2 !== 0) return true;

return false;
Copy link
Collaborator

Choose a reason for hiding this comment

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

lower golf score:
return <conditional_statement>

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

src/lib/fixer.ts Outdated

// Determine the appropriate suppression rule based on the type of diagnostic.code
let suppressionRule: string;
if (typeof this.diagnostic.code === 'object' && this.diagnostic.code !== null && 'value' in this.diagnostic.code) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you need both the === 'object' and !== null check? Seems like the first would be sufficient. Also, in JS/TS it's typical to do == null instead of === null, because undefined == null and usually you want to catch both null and undefined.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

Comment on lines +167 to +168
const existingRules = suppressionMatch[1].split(',').map(rule => rule.trim());
if (!existingRules.includes(suppressionRule)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Follow-up question: Is it an error case if the rule is already present, or is it valid and nothing should happen?


action.diagnostics = [this.diagnostic];
action.command = {
command: Constants.COMMAND_REMOVE_DIAGNOSTICS_ON_SELECTED_FILE,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Clarification: Does "multiple classes" mean multiple outer classes, or an outer class with inner classes? I know Apex can't have multiple outer classes, and I'm pretty sure that the annotation will only apply to outer classes, though I haven't checked that.

src/lib/fixer.ts Outdated Show resolved Hide resolved
public isWithinQuotes(line: string, matchIndex: number): boolean {
const beforeMatch = line.slice(0, matchIndex);
const singleQuotesBefore = (beforeMatch.match(/'/g) || []).length;
const doubleQuotesBefore = (beforeMatch.match(/"/g) || []).length;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Strings have to be single-quoted. I genuinely don't remember if you're allowed to use double-quotes as string boundaries.
BUT you might want to check whether the string containing escaped single-quotes throws off the algorithm. E.g., 'this is a string. it\'s got a single-quote inside it'.

src/lib/fixer.ts Outdated
const doubleQuotesBefore = (beforeMatch.match(/"/g) || []).length;

// Check if the number of quotes before the match is odd (inside quotes)
if (singleQuotesBefore % 2 !== 0 || doubleQuotesBefore % 2 !== 0) return true;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Pet peeve: I'm broadly opposed to single-line/braceless ifs.

Comment on lines +167 to +168
const existingRules = suppressionMatch[1].split(',').map(rule => rule.trim());
if (!existingRules.includes(suppressionRule)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Adding case insensitivity in the regex doesn't help with this line does it existingRules.includes(suppressionRule)) ?

I'd recommend something like
const existingRules = suppressionMatch[1].split(',').map(rule => rule.trim().toLowerCase());
if (!existingRules.includes(suppressionRule.toLowerCase())) {

@jag-j jag-j merged commit 54363ff into dev Jun 10, 2024
7 checks passed
@jag-j jag-j deleted the jj/W-13991634 branch June 10, 2024 18:45
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 this pull request may close these issues.

3 participants