npm-groovy-lint
Advanced tools
Comparing version 3.0.0-beta.4 to 3.0.0-beta.5
@@ -8,3 +8,13 @@ # Changelog | ||
- Local microservice "CodeNarcServer" called via Http by npm-groovy-lint, to avoid loading all groovy/java classes at each lint request. This microservice autokills itself after one hour idle. | ||
- Test classes for rules fix (before / after fix defined in rule definitions) | ||
- Add debug logs (use it by setting DEBUG env variable , ex: `DEBUG=npm-groovy-lint npm-groovy-lint args...`) | ||
- Update lines and ranges of other errors after a fix updated the number of lines | ||
## Changes | ||
- Split rules definition into files instead of all in a huge single file | ||
- New lib utils.js that can be used by rules definition | ||
- Fix: Crash when there was no error found in a file | ||
- Fix: Remove Promise error display in log after launching CodeNarcServer | ||
## [2.2.0] 2020-02-28 | ||
@@ -11,0 +21,0 @@ |
@@ -46,3 +46,4 @@ #! /usr/bin/env node | ||
// Fix errors using codenarc result and groovy lint rules | ||
async run(errorIds = null) { | ||
async run(optns = { errorIds: null, propagate: false }) { | ||
debug(`<<<<<< NpmGroovyLintFix.run START >>>>>>`); | ||
// Start progress bar | ||
@@ -59,4 +60,4 @@ this.bar = new cliProgress.SingleBar( | ||
// Parse fixes and process them | ||
await this.parseFixableErrors(errorIds); | ||
await this.fixErrors(); | ||
await this.parseFixableErrors(optns.errorIds); | ||
await this.fixErrors(optns.propagate); | ||
@@ -67,3 +68,3 @@ this.updateResultCounters(); | ||
this.bar.stop(); | ||
debug(`>>>>>> NpmGroovyLintFix.run END <<<<<<`); | ||
return this; | ||
@@ -118,2 +119,3 @@ } | ||
} | ||
debug(`Parsed fixable errors: ${JSON.stringify(this.fixableErrors)}`); | ||
} | ||
@@ -134,3 +136,3 @@ | ||
// Fix errors in files using fix rules | ||
async fixErrors() { | ||
async fixErrors(propagate) { | ||
// Process files in parallel | ||
@@ -151,9 +153,12 @@ await Promise.all( | ||
if (fileFixableError.rule.scope === "file") { | ||
const allLinesNew = this.tryApplyFixRule(allLines, lineNb, fileFixableError).slice(); // copy result lines | ||
if (JSON.stringify(allLinesNew) !== JSON.stringify(allLines.toString)) { | ||
allLines = allLinesNew; | ||
const allLinesNew = this.tryApplyFixRule([...allLines], lineNb, fileFixableError).slice(); // copy result lines | ||
if (JSON.stringify(allLinesNew) !== JSON.stringify(allLines)) { | ||
fixedInFileNb = fixedInFileNb + 1; | ||
this.fixedErrorsNumber = this.fixedErrorsNumber + 1; | ||
this.fixedErrorsIds.push(fileFixableError.id); | ||
this.updateLintResult(fileNm, fileFixableError.id, { fixed: true }); | ||
this.updateLintResult(fileNm, fileFixableError.id, { fixed: true }, propagate, { | ||
beforeFix: allLines, | ||
afterFix: allLinesNew | ||
}); | ||
allLines = allLinesNew; | ||
} | ||
@@ -170,3 +175,3 @@ } | ||
this.fixedErrorsIds.push(fileFixableError.id); | ||
this.updateLintResult(fileNm, fileFixableError.id, { fixed: true }); | ||
this.updateLintResult(fileNm, fileFixableError.id, { fixed: true }, propagate); | ||
} | ||
@@ -240,23 +245,62 @@ } | ||
// Update lint result of an identified error | ||
updateLintResult(fileNm, errId, errDataToSet) { | ||
updateLintResult(fileNm, errId, errDataToSet, propagate, compareInfo = {}) { | ||
const errIndex = this.updatedLintResult.files[fileNm].errors.findIndex(error => error.id === errId); | ||
if (errIndex < 0) { | ||
// No error to update in case of fix from triggers of another rule | ||
return; | ||
// Update error in lint result {mostly fixed: true} | ||
// It not in list of errors, it means it's from a triggered error | ||
if (errIndex > -1) { | ||
const error = this.updatedLintResult.files[fileNm].errors[errIndex]; | ||
Object.assign(error, errDataToSet); | ||
this.updatedLintResult.files[fileNm].errors[errIndex] = error; | ||
if (errDataToSet.fixed === true) { | ||
switch (error.severity) { | ||
case "error": | ||
this.updatedLintResult.summary.totalFixedErrorNumber++; | ||
break; | ||
case "warning": | ||
this.updatedLintResult.summary.totalFixedWarningNumber++; | ||
break; | ||
case "info": | ||
this.updatedLintResult.summary.totalFixedInfoNumber++; | ||
break; | ||
} | ||
} | ||
} | ||
const error = this.updatedLintResult.files[fileNm].errors[errIndex]; | ||
Object.assign(error, errDataToSet); | ||
this.updatedLintResult.files[fileNm].errors[errIndex] = error; | ||
if (errDataToSet.fixed === true) { | ||
switch (error.severity) { | ||
case "error": | ||
this.updatedLintResult.summary.totalFixedErrorNumber++; | ||
// If the number of lines has changes, update lines after | ||
if (propagate && compareInfo && compareInfo.beforeFix && compareInfo.afterFix) { | ||
// Propagate only if number of lines is different | ||
if (compareInfo.beforeFix.length === compareInfo.afterFix.length) { | ||
return; | ||
} | ||
let diffLinesNb = compareInfo.afterFix.length - compareInfo.beforeFix.length; | ||
let firstAddedOrRemovedLineNb; | ||
// Find first updated line number | ||
for (let i = 0; i < compareInfo.beforeFix.length; i++) { | ||
if (compareInfo.beforeFix[i] !== compareInfo.afterFix[i]) { | ||
firstAddedOrRemovedLineNb = i; | ||
break; | ||
case "warning": | ||
this.updatedLintResult.summary.totalFixedWarningNumber++; | ||
break; | ||
case "info": | ||
this.updatedLintResult.summary.totalFixedInfoNumber++; | ||
break; | ||
} | ||
} | ||
// Recalculate line positions if line number has changed | ||
if ((firstAddedOrRemovedLineNb || firstAddedOrRemovedLineNb === 0) & (diffLinesNb !== 0)) { | ||
// Update lint results | ||
this.updatedLintResult.files[fileNm].errors = this.updatedLintResult.files[fileNm].errors.map(err => { | ||
if (err.range && err.range.start.line >= firstAddedOrRemovedLineNb) { | ||
err.range.start.line = err.range.start.line + diffLinesNb; | ||
err.range.end.line = err.range.end.line + diffLinesNb; | ||
} | ||
if (err.line && err.line >= firstAddedOrRemovedLineNb) { | ||
err.line = err.line + diffLinesNb; | ||
} | ||
return err; | ||
}); | ||
// Update fixable Errors | ||
this.fixableErrors[fileNm] = this.fixableErrors[fileNm].map(fixableError => { | ||
if ((fixableError.lineNb || fixableError.lineNb === 0) && fixableError.lineNb >= firstAddedOrRemovedLineNb) { | ||
fixableError.lineNb = fixableError.lineNb + diffLinesNb; | ||
} | ||
return fixableError; | ||
}); | ||
} | ||
} | ||
@@ -263,0 +307,0 @@ } |
@@ -5,7 +5,13 @@ // Additional definition for codenarc rules ( get position & available fix) | ||
/* | ||
RuleName: { | ||
const rule = { | ||
// add scope = "file" if the fix impacts more than the identified line (If fix defined) | ||
scope: "file", | ||
// Define a priority at the top of groovy-lint-rules.js (If fix defined) | ||
priority: getPriority("RuleName"), | ||
scope: "file", // default: line | ||
// add isCodeNarcRule: false if the rule is not part of CodeNarc list of supported rules ( but triggered by another rule with trigger property) | ||
isCodeNarcRule: false, // default: true | ||
// List of other rules fix that this rule fix must trigger (if fix defined) | ||
triggers: ["SomeOtherRule","AnotherRule"], | ||
// Extract variables from CodeNarc error message (optional) | ||
@@ -25,2 +31,3 @@ variables: [ | ||
], | ||
// Return range for UI . Input: errorLine, errorItem, evaluated variables | ||
@@ -37,2 +44,3 @@ range: { | ||
}, | ||
// Fix if scope = file | ||
@@ -56,5 +64,20 @@ fix: { | ||
} | ||
} | ||
}, | ||
}, | ||
// Definition for automated tests (mocha). | ||
// If a fix is defined, define at least one test with sourceBefore and sourceAfter expected value | ||
tests: [ | ||
{ | ||
sourceBefore: ` | ||
def str = "lelamanul" | ||
`, | ||
sourceAfter: ` | ||
str = "lelamanul" | ||
` | ||
} | ||
] | ||
} | ||
module.exports = { rule } | ||
*/ | ||
@@ -64,7 +87,4 @@ | ||
const decodeHtml = require("decode-html"); | ||
const fse = require("fs-extra"); | ||
// Default indent length | ||
const indentLength = 4; | ||
// If you add a new global rule, it's very important to think about their order. | ||
@@ -87,3 +107,2 @@ // Rules modifiyng the number of lines must arrive last ! | ||
// Rule that can change the numbers of lines, so they must be processed after line scope rules | ||
"IfStatementBraces", | ||
@@ -98,733 +117,16 @@ "ElseBlocktBraces", | ||
const npmGroovyLintRules = { | ||
// Braces for if else | ||
BracesForIfElse: { | ||
range: { | ||
type: "function", | ||
func: (_errLine, errItem, _evaluatedVars, allLines) => { | ||
return findRangeBetweenStrings(allLines, errItem, "if", "{"); | ||
} | ||
} | ||
}, | ||
const RULES_FOLDER = __dirname + "/rules"; | ||
// Exception type must not be used | ||
CatchException: { | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem) => { | ||
return getStringRange(errLine, "Exception", errItem); | ||
} | ||
} | ||
}, | ||
// Closing brace not alone | ||
ClosingBraceNotAlone: { | ||
scope: "file", | ||
priority: getPriority("ClosingBraceNotAlone"), | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem) => { | ||
const closingBracePos = errLine.lastIndexOf("}"); | ||
return { | ||
start: { line: errItem.line, character: closingBracePos }, | ||
end: { line: errItem.line, character: closingBracePos + 1 } | ||
}; | ||
} | ||
}, | ||
fix: { | ||
type: "function", | ||
func: allLines => { | ||
const newFileLines = []; | ||
let prevLine = ""; | ||
for (const line of allLines) { | ||
const newLine = line.replace("{{{NEWLINECLOSINGBRACE}}}", ""); | ||
const prevLineIndent = prevLine.search(/\S/); | ||
newFileLines.push(newLine); | ||
if (newLine !== line) { | ||
newFileLines.push(" ".repeat(prevLineIndent) + "}"); | ||
} | ||
prevLine = newLine; | ||
} | ||
return newFileLines; | ||
} | ||
} | ||
}, | ||
// Consecutive blank lines | ||
ConsecutiveBlankLines: { | ||
scope: "file", | ||
priority: getPriority("ConsecutiveBlankLines"), | ||
fix: { | ||
label: "Remove consecutive blank lines", | ||
type: "function", | ||
func: allLines => { | ||
const newFileLines = []; | ||
let prevLine = "none"; | ||
for (const line of allLines) { | ||
if (!(line.trim() === "" && prevLine.trim() === "")) { | ||
// Check if previous line is empty: if not do not add line | ||
newFileLines.push(line); | ||
prevLine = line; | ||
} | ||
} | ||
return newFileLines; | ||
} | ||
} | ||
}, | ||
// Missing else braces | ||
ElseBlockBraces: { | ||
scope: "file", | ||
unitary: true, | ||
triggers: ["ClosingBraceNotAlone"], | ||
priority: getPriority("ElseBlocktBraces"), | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem) => { | ||
return getStringRangeMultiline(errLine, "else", errItem); | ||
} | ||
}, | ||
fix: { | ||
label: "Add braces", | ||
type: "function", | ||
func: (allLines, variables) => { | ||
const lineNumber = getVariable(variables, "lineNb", { mandatory: true }); | ||
// If next line is also a if/else, this rule can not autofix for now, it has to be done manually | ||
if (allLines[lineNumber + 1] && lineNumber[lineNumber + 1].includes("else")) { | ||
return allLines; | ||
} | ||
let line = allLines[lineNumber]; | ||
line = line.trimEnd() + " {"; | ||
allLines[lineNumber] = line; | ||
// next line | ||
let match = false; | ||
let pos = 0; | ||
let level = 0; | ||
while (!match && pos < allLines.length) { | ||
let nextLine = allLines[lineNumber + pos + 1]; | ||
if (isValidCodeLine(nextLine) && level === 0) { | ||
if (!nextLine.trim().startsWith("if") && !nextLine.includes("{")) { | ||
nextLine = nextLine + "{{{NEWLINECLOSINGBRACE}}}"; | ||
allLines[lineNumber + pos + 1] = nextLine; | ||
match = true; | ||
} else if (nextLine.includes("}") && !nextLine.includes("{")) { | ||
level--; | ||
} else { | ||
level++; | ||
} | ||
} | ||
pos++; | ||
} | ||
return allLines; | ||
} | ||
} | ||
}, | ||
// File ends without new line | ||
FileEndsWithoutNewline: { | ||
scope: "file", | ||
priority: getPriority("FileEndsWithoutNewline"), | ||
fix: { | ||
label: "Add new line at the end of file", | ||
type: "function", | ||
func: allLines => { | ||
return (allLines.join("\r\n") + "\r\n").split("\r\n"); | ||
} | ||
} | ||
}, | ||
// nvuillam: Fix not working, especially when embedded missing If statements ... | ||
// let's let people correct that manually for now :) | ||
// Missing if braces | ||
IfStatementBraces: { | ||
scope: "file", | ||
unitary: true, | ||
triggers: ["ClosingBraceNotAlone"], | ||
priority: getPriority("IfStatementBraces"), | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem) => { | ||
return getStringRange(errLine, "if", errItem); | ||
} | ||
}, | ||
fix: { | ||
label: "Add if statement braces", | ||
type: "function", | ||
func: (allLines, variables) => { | ||
const lineNumber = getVariable(variables, "lineNb", { mandatory: true }); | ||
// If next line is also a if/else, this rule can not autofix for now, it has to be done manually | ||
if (allLines[lineNumber + 1] && (allLines[lineNumber + 1].includes("if") || allLines[lineNumber + 1].includes("else"))) { | ||
return allLines; | ||
} | ||
// If line | ||
let line = allLines[lineNumber]; | ||
line = line.trimEnd() + " {"; | ||
allLines[lineNumber] = line; | ||
// next line | ||
let match = false; | ||
let pos = 0; | ||
let level = 0; | ||
while (!match && pos < allLines.length) { | ||
let nextLine = allLines[lineNumber + pos + 1]; | ||
if (isValidCodeLine(nextLine) && level === 0) { | ||
if (!nextLine.trim().startsWith("if") && !nextLine.includes("{")) { | ||
nextLine = nextLine + "{{{NEWLINECLOSINGBRACE}}}"; | ||
allLines[lineNumber + pos + 1] = nextLine; | ||
match = true; | ||
} else if (nextLine.includes("}") && !nextLine.includes("{")) { | ||
level--; | ||
} else { | ||
level++; | ||
} | ||
} | ||
pos++; | ||
} | ||
return allLines; | ||
} | ||
} | ||
}, | ||
// Indentation | ||
Indentation: { | ||
triggers: ["IndentationClosingBraces", "IndentationComments"], | ||
priority: getPriority("Indentation"), | ||
variables: [ | ||
{ | ||
name: "EXPECTED", | ||
regex: /The (.*) is at the incorrect indent level: Expected column (.*) but was (.*)/, | ||
regexPos: 2, | ||
type: "number" | ||
}, | ||
{ | ||
name: "FOUND", | ||
regex: /The (.*) is at the incorrect indent level: Expected column (.*) but was (.*)/, | ||
regexPos: 3, | ||
type: "number" | ||
} | ||
], | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem, evaluatedVars) => { | ||
return { | ||
start: { line: errItem.line, character: 0 }, | ||
end: { line: errItem.line, character: getVariable(evaluatedVars, "FOUND") - 1 } | ||
}; | ||
} | ||
}, | ||
fix: { | ||
label: "Fix indentation", | ||
type: "function", | ||
func: (line, evaluatedVars) => { | ||
const expectedCol = parseInt(getVariable(evaluatedVars, "EXPECTED", { mandatory: true, line: line }), 10); | ||
// const foundIndent = parseInt(getVariable(evaluatedVars, "FOUND", { mandatory: true, line: line })); | ||
/* if (line.trim() === "}") { | ||
// Manage Wrong info from codeNarc :/ { | ||
line = " ".repeat(expectedIndent + (indentLength * 2)) + line.trimStart(); | ||
} else { */ | ||
const startSpaces = expectedCol === 0 ? 0 : expectedCol - 1; | ||
line = " ".repeat(startSpaces) + line.trimStart(); | ||
return line; | ||
} | ||
} | ||
}, | ||
// Indentation comments | ||
IndentationComments: { | ||
scope: "file", | ||
priority: getPriority("IndentationComments"), | ||
fix: { | ||
label: "Fix indentation", | ||
type: "function", | ||
func: allLines => { | ||
const newFileLines = []; | ||
for (let i = 0; i < allLines.length; i++) { | ||
let line = allLines[i]; | ||
// Detect comment line | ||
if (line.trimStart().startsWith("//")) { | ||
// Find indentation of next line (which is not blank or a comment) | ||
let j = 1; | ||
let nextLineIndent = null; | ||
while (allLines[i + j] && nextLineIndent == null) { | ||
if (!/^\s*$/.test(allLines[i + j]) && !allLines[i + j].trimStart().startsWith("//")) { | ||
nextLineIndent = allLines[i + j].search(/\S/); // find first non blank character | ||
} | ||
j++; | ||
} | ||
// Set new indentation it on this comment line | ||
if (nextLineIndent) { | ||
line = " ".repeat(nextLineIndent) + line.trimStart(); | ||
} | ||
} | ||
newFileLines.push(line); | ||
} | ||
return newFileLines; | ||
} | ||
} | ||
}, | ||
// Indentation closing braces | ||
IndentationClosingBraces: { | ||
scope: "file", | ||
priority: getPriority("IndentationClosingBraces"), | ||
fix: { | ||
label: "Fix indentation", | ||
type: "function", | ||
func: allLines => { | ||
const newFileLines = []; | ||
for (let i = 0; i < allLines.length; i++) { | ||
let line = allLines[i] + ""; | ||
// Detect closing brace line | ||
if (line.trim() === "}") { | ||
// Find indentation of matching brace (CodeNarc Indentation rule does not always work well :/ ) | ||
let j = 1; | ||
let matchingLineIndent = null; | ||
let level = 1; | ||
while ((allLines[i - j] || allLines[i - j] === "") && matchingLineIndent == null) { | ||
const prevLine = allLines[i - j]; | ||
if (prevLine.includes("}") && !prevLine.includes("${")) { | ||
level++; | ||
} | ||
if (prevLine.includes("{") && !prevLine.includes("${")) { | ||
level--; | ||
if (level === 0) { | ||
matchingLineIndent = prevLine.search(/\S/); | ||
} | ||
} | ||
j++; | ||
} | ||
// Set new indentation it on this comment line | ||
if (matchingLineIndent) { | ||
line = (" ".repeat(matchingLineIndent) + line.trimStart()).replace(/\t/g, ""); | ||
} | ||
} | ||
newFileLines.push(line); | ||
} | ||
return newFileLines; | ||
} | ||
} | ||
}, | ||
// No use of Java.io classes | ||
/* NV: TODO: finalise for when there is several occurences of the string in the same line | ||
JavaIoPackageAccess: { | ||
variables: [ | ||
{ | ||
name: "CLASSNAME", | ||
regex: /The use of java.io.(.*) violates the Enterprise Java Bean specification/, | ||
regexPos: 1 | ||
} | ||
], | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem, evaluatedVars) => { | ||
return getLastVariableRange(errLine, evaluatedVars, "CLASSNAME", errItem); | ||
} | ||
} | ||
}, */ | ||
// Too many methods in a class | ||
MethodCount: { | ||
variables: [ | ||
{ | ||
name: "CLASSNAME", | ||
regex: /Class (.*) has 52 methods/, | ||
regexPos: 1 | ||
} | ||
], | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem, evaluatedVars) => { | ||
return getVariableRange(errLine, evaluatedVars, "CLASSNAME", errItem); | ||
} | ||
} | ||
}, | ||
// No use of Java.util.date | ||
NoJavaUtilDate: { | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem) => { | ||
return getStringRange(errLine, "Date", errItem); | ||
} | ||
} | ||
}, | ||
// No tab character | ||
NoTabCharacter: { | ||
scope: "file", | ||
priority: getPriority("NoTabCharacter"), | ||
fix: { | ||
label: "Replace tabs by spaces in all file", | ||
type: "function", | ||
func: allLines => { | ||
const newFileLines = []; | ||
const replaceChars = " ".repeat(indentLength); | ||
for (const line of allLines) { | ||
newFileLines.push(line.replace(/\t/g, replaceChars)); | ||
} | ||
return newFileLines; | ||
} | ||
} | ||
}, | ||
// Space after catch | ||
SpaceAfterCatch: { | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem) => { | ||
return getStringRange(errLine, "){", errItem); | ||
} | ||
}, | ||
priority: getPriority("SpaceAfterCatch"), | ||
fix: { | ||
label: "Add space after catch", | ||
type: "replaceString", | ||
before: "){", | ||
after: ") {" | ||
} | ||
}, | ||
// Space after opening brace | ||
SpaceAfterOpeningBrace: { | ||
priority: getPriority("SpaceAfterOpeningBrace"), | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem) => { | ||
return getStringRange(errLine, "{", errItem); | ||
} | ||
}, | ||
fix: { | ||
label: "Add space after opening brace", | ||
type: "function", | ||
func: line => { | ||
const regexMatch = line.match(new RegExp(/{[^ ]/, "g")); | ||
if (regexMatch && regexMatch[0]) { | ||
line = line.replace(regexMatch[0], "{ " + regexMatch[0][1]); | ||
} | ||
return line; | ||
} | ||
} | ||
}, | ||
// Space around operators | ||
SpaceAroundOperator: { | ||
priority: getPriority("SpaceAroundOperator"), | ||
variables: [ | ||
{ | ||
name: "OPERATOR", | ||
regex: /The operator "(.*)" within class (.*) is not (.*) by a space or whitespace/ | ||
} | ||
], | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem, evaluatedVars) => { | ||
return getVariableRange(errLine, evaluatedVars, "OPERATOR", errItem); | ||
} | ||
}, | ||
fix: { | ||
label: "Add space around operator", | ||
type: "function", | ||
func: (line, evaluatedVars) => { | ||
let operator = getVariable(evaluatedVars, "OPERATOR", { mandatory: true, htmlToString: true, line: line }); | ||
if (!line.includes("+=") && !line.includes("++") && !line.includes("--") && !line.includes("-=")) { | ||
return addSpaceAroundChar(line, operator); | ||
} else { | ||
return line; | ||
} | ||
} | ||
} | ||
}, | ||
// Add space after a comma | ||
SpaceAfterComma: { | ||
priority: getPriority("SpaceAfterComma"), | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem) => { | ||
return getStringRange(errLine, ",", errItem); | ||
} | ||
}, | ||
fix: { | ||
label: "Add space after comma", | ||
type: "function", | ||
func: line => { | ||
return addSpaceAroundChar(line, ","); | ||
} | ||
} | ||
}, | ||
// Space before opening brace | ||
SpaceBeforeOpeningBrace: { | ||
priority: getPriority("SpaceBeforeOpeningBrace"), | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem) => { | ||
return getStringRange(errLine, "{", errItem); | ||
} | ||
}, | ||
fix: { | ||
label: "Add space before opening brace", | ||
type: "function", | ||
func: line => { | ||
const regexMatch = line.match(new RegExp(/[^ ]{/, "g")); | ||
if (regexMatch && regexMatch[0]) { | ||
line = line.replace(regexMatch[0], regexMatch[0][0] + " {"); | ||
} | ||
return line; | ||
} | ||
} | ||
}, | ||
// System.exit forbidden | ||
SystemExit: { | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem, evaluatedVars) => { | ||
return getVariableRange(errLine, evaluatedVars, "System.exit", errItem); | ||
} | ||
} | ||
}, | ||
// Trailing Whitespaces | ||
TrailingWhitespace: { | ||
priority: getPriority("TrailingWhitespace"), | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem) => { | ||
const diff = errLine.length - errLine.trimEnd().length; | ||
return { | ||
start: { line: errItem.line, character: errLine.length - diff }, | ||
end: { line: errItem.line, character: errLine.length } | ||
}; | ||
} | ||
}, | ||
fix: { | ||
label: "Remove trailing whitespace", | ||
type: "function", | ||
func: line => { | ||
return line.trimEnd(); | ||
} | ||
} | ||
}, | ||
// Unnecessary def in field declaration (statif def) | ||
UnnecessaryDefInFieldDeclaration: { | ||
priority: getPriority("UnnecessaryDefInFieldDeclaration"), | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem) => { | ||
return getStringRange(errLine, "def", errItem); | ||
} | ||
}, | ||
fix: { | ||
label: "Remove def", | ||
type: "replaceString", | ||
before: "def ", | ||
after: "" | ||
} | ||
}, | ||
// Unnecessary Groovy String | ||
UnnecessaryGString: { | ||
priority: getPriority("UnnecessaryGString"), | ||
variables: [ | ||
{ | ||
name: "STRING", | ||
regex: /The String '(.*)' can be wrapped in single quotes instead of double quotes/ | ||
} | ||
], | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem, evaluatedVars) => { | ||
return getVariableRange(errLine, evaluatedVars, "STRING", errItem); | ||
} | ||
}, | ||
fix: { | ||
label: "Replace double quotes by single quotes", | ||
type: "replaceString", | ||
before: '"{{STRING}}"', | ||
after: "'{{STRING}}'" | ||
} | ||
}, | ||
// Unnecessary semi colon at the end of a line | ||
UnnecessarySemicolon: { | ||
priority: getPriority("UnnecessarySemicolon"), | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem) => { | ||
return getLastStringRange(errLine, ";", errItem); | ||
} | ||
}, | ||
fix: { | ||
label: "Remove unnecessary semicolon", | ||
type: "function", | ||
func: line => { | ||
if ((line.match(/;/g) || []).length === 1) { | ||
line = line.split(";").join(""); | ||
} | ||
return line; | ||
} | ||
} | ||
}, | ||
// Unnecessary toString() | ||
UnnecessaryToString: { | ||
priority: getPriority("UnnecessaryToString"), | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem) => { | ||
return getStringRange(errLine, ".toString()", errItem); | ||
} | ||
}, | ||
fix: { | ||
label: "Remove unnecessary toString()", | ||
type: "function", | ||
func: line => { | ||
return line.replace(".toString()"); | ||
} | ||
} | ||
}, | ||
// Unused method parameter | ||
UnusedMethodParameter: { | ||
variables: [ | ||
{ | ||
name: "PARAMNAME", | ||
regex: /Violation in class (.*) Method parameter \[(.*)\] is never referenced in the method (.*) of class (.*)/, | ||
regexPos: 2 | ||
} | ||
], | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem, evaluatedVars) => { | ||
return getVariableRange(errLine, evaluatedVars, "PARAMNAME", errItem); | ||
} | ||
} | ||
}, | ||
// Unused variable | ||
UnusedVariable: { | ||
variables: [ | ||
{ | ||
name: "VARNAME", | ||
regex: /The variable \[(.*)\] in (.*) is not used/ | ||
} | ||
], | ||
range: { | ||
type: "function", | ||
func: (errLine, errItem, evaluatedVars) => { | ||
return getVariableRange(errLine, evaluatedVars, "VARNAME", errItem); | ||
} | ||
} | ||
const ruleFiles = fse.readdirSync(RULES_FOLDER); | ||
const npmGroovyLintRules = {}; | ||
for (const file of ruleFiles) { | ||
const ruleName = file.replace(".js", ""); | ||
const { rule } = require(`${RULES_FOLDER}/${file}`); | ||
if (rule.disabled) { | ||
continue; | ||
} | ||
}; | ||
function getPriority(ruleName) { | ||
return rulesFixPriorityOrder.indexOf(ruleName); | ||
rule.priority = rulesFixPriorityOrder.indexOf(ruleName); | ||
npmGroovyLintRules[ruleName] = rule; | ||
} | ||
function getVariable(evaluatedVars, name, optns = { mandatory: true, decodeHtml: false, line: "" }) { | ||
const matchingVars = evaluatedVars.filter(evaluatedVar => evaluatedVar.name === name); | ||
if (matchingVars && matchingVars.length > 0) { | ||
return optns.decodeHtml ? decodeHtml(matchingVars[0].value) : matchingVars[0].value; | ||
} else if (optns.mandatory) { | ||
throw new Error("NGL fix: missing mandatory variable " + name + " in " + JSON.stringify(evaluatedVars)) + "for line :\n" + optns.line; | ||
} else { | ||
return null; | ||
} | ||
} | ||
function getStringRange(errLine, str, errItem) { | ||
const varStartPos = errLine.indexOf(str); | ||
return { | ||
start: { line: errItem.line, character: varStartPos }, | ||
end: { line: errItem.line, character: varStartPos + str.length } | ||
}; | ||
} | ||
function getStringRangeMultiline(allLines, str, errItem) { | ||
let range = getDefaultRange(allLines, errItem); | ||
let pos = errItem.line - 1; | ||
let isFound = false; | ||
while (isFound === false && pos < allLines.length) { | ||
if (!isFound && allLines[pos].indexOf(str) > -1) { | ||
const varStartPos = allLines[pos].indexOf(str); | ||
range = { | ||
start: { line: errItem.line, character: varStartPos }, | ||
end: { line: errItem.line, character: varStartPos + str.length } | ||
}; | ||
isFound = true; | ||
} | ||
pos++; | ||
} | ||
return range; | ||
} | ||
function getLastStringRange(errLine, str, errItem) { | ||
const varStartPos = errLine.lastIndexOf(str); | ||
return { | ||
start: { line: errItem.line, character: varStartPos }, | ||
end: { line: errItem.line, character: varStartPos + str.length } | ||
}; | ||
} | ||
function getVariableRange(errLine, evaluatedVars, variable, errItem) { | ||
const varValue = getVariable(evaluatedVars, variable); | ||
return getStringRange(errLine, varValue, errItem); | ||
} | ||
/* | ||
function getLastVariableRange(errLine, evaluatedVars, variable, errItem) { | ||
const varValue = getVariable(evaluatedVars, variable); | ||
return getLastStringRange(errLine, varValue, errItem); | ||
} | ||
*/ | ||
function findRangeBetweenStrings(allLines, errItem, strStart, strEnd) { | ||
let range = getDefaultRange(allLines, errItem); | ||
let pos = errItem.line - 1; | ||
let isStartFound = false; | ||
let isEndFound = false; | ||
while ((isStartFound === false || isEndFound === false) && pos < allLines.length) { | ||
if (!isStartFound && allLines[pos].indexOf(strStart) > -1) { | ||
range.start = { line: pos + 1, character: allLines[pos].indexOf(strStart) }; | ||
isStartFound = true; | ||
} | ||
if (!isEndFound && allLines[pos].indexOf(strEnd) > -1) { | ||
range.end = { line: pos + 1, character: allLines[pos].indexOf(strEnd) }; | ||
isEndFound = true; | ||
} | ||
pos++; | ||
} | ||
return range; | ||
} | ||
function getDefaultRange(allLines, errItem) { | ||
return { | ||
start: { line: errItem.line, character: 0 }, | ||
end: { line: errItem.line, character: allLines[errItem.line - 1].length } | ||
}; | ||
} | ||
function isValidCodeLine(line) { | ||
return line.trim() !== "" && line.trim().split("//")[0] !== ""; | ||
} | ||
function addSpaceAroundChar(line, char) { | ||
let pos = -1; | ||
const splits = line.split(char); | ||
const newArray = splits.map(str => { | ||
pos++; | ||
if (pos === 0) { | ||
return str.trimEnd(); | ||
} else if (pos === splits.length - 1) { | ||
return str.trimStart(); | ||
} else { | ||
return str.trim(); | ||
} | ||
}); | ||
return newArray.join(" " + char + " ").trimEnd(); | ||
} | ||
module.exports = { npmGroovyLintRules }; |
@@ -70,2 +70,3 @@ #! /usr/bin/env node | ||
async run() { | ||
debug(`<<< NpmGroovyLint.run START >>>`); | ||
const doProcess = await this.preProcess(); | ||
@@ -76,2 +77,3 @@ if (doProcess) { | ||
} | ||
debug(`>>> NpmGroovyLint.run END <<<`); | ||
return this; | ||
@@ -81,10 +83,11 @@ } | ||
// Call an existing NpmGroovyLint instance to request fix of errors | ||
async fixErrors(errorIds) { | ||
async fixErrors(errorIds, optns = {}) { | ||
debug(`Fix errors for ${JSON.stringify(errorIds)} on existing NpmGroovyLint instance`); | ||
this.fixer = new NpmGroovyLintFix(this.lintResult, { | ||
verbose: this.options.verbose, | ||
fixrules: this.options.fixrules, | ||
source: this.options.source, | ||
verbose: optns.verbose || this.options.verbose, | ||
fixrules: optns.fixrules || this.options.fixrules, | ||
source: optns.source || this.options.source, | ||
save: this.tmpGroovyFileName ? false : true | ||
}); | ||
await this.fixer.run(errorIds); | ||
await this.fixer.run({ errorIds: errorIds, propagate: true }); | ||
this.lintResult = this.fixer.updatedLintResult; | ||
@@ -181,2 +184,3 @@ } | ||
await fse.writeFile(this.tmpGroovyFileName, this.options.source); | ||
debug(`Create temp file ${this.tmpGroovyFileName} with input source, as CodeNarc requires physical files`); | ||
} | ||
@@ -290,2 +294,3 @@ | ||
}; | ||
debug(`CALL CodeNarcServer with ${JSON.stringify(rqstOptions, null, 2)}`); | ||
let parsedBody = null; | ||
@@ -323,3 +328,3 @@ try { | ||
// Start progress bar | ||
debug("NGL: running CodeNarc using " + jDeployCommand); | ||
debug(`CALL CodeNarcJava with ${jDeployCommand}`); | ||
this.bar = new cliProgress.SingleBar( | ||
@@ -374,5 +379,8 @@ { | ||
let interval; | ||
debug(`ATTEMPT to start CodeNarcServer with ${jDeployCommand}`); | ||
try { | ||
// Start server using java | ||
exec(jDeployCommand, { timeout: this.execTimeout }); | ||
// Start server using java (we don't care the promise result, as the following promise will poll the server) | ||
exec(jDeployCommand, { timeout: this.execTimeout }) | ||
.then(() => {}) | ||
.catch(() => {}); | ||
// Poll it until it is ready | ||
@@ -387,2 +395,3 @@ const start = performance.now(); | ||
this.serverStatus = "running"; | ||
debug(`SUCCESS: CodeNarcServer is running`); | ||
clearInterval(interval); | ||
@@ -426,4 +435,6 @@ resolve(); | ||
} | ||
console.log("NGL: Unable to start CodeNarc Server. Use --noserver if you do not even want to try"); | ||
const errMsg = "NGL: Unable to start CodeNarc Server. Use --noserver if you do not even want to try"; | ||
debug(errMsg); | ||
debug(e.message); | ||
console.log(errMsg); | ||
} | ||
@@ -445,3 +456,3 @@ | ||
} | ||
// no --ngl* options | ||
// only --codenarcargs arguments | ||
else if (this.onlyCodeNarc) { | ||
@@ -476,2 +487,4 @@ console.log("NGL: Successfully processed CodeNarc: \n" + this.codeNarcStdOut); | ||
await fse.remove(this.tmpGroovyFileName); | ||
debug(`Removed temp file ${this.tmpGroovyFileName} as it is not longer used`); | ||
this.tmpXmlFileName = null; | ||
} | ||
@@ -504,2 +517,3 @@ } | ||
if (!folderInfo.File) { | ||
debug(`Warning: ${folderInfo} does not contain any File item`); | ||
continue; | ||
@@ -562,4 +576,4 @@ } | ||
} | ||
// Complete with files with no error | ||
result.files = files; | ||
await fse.remove(this.tmpXmlFileName); // Remove temporary file | ||
return result; | ||
@@ -572,2 +586,3 @@ } | ||
const lintAgainOptions = JSON.parse(JSON.stringify(this.options)); | ||
debug(`Fix is done, lint again with options ${JSON.stringify(lintAgainOptions)}`); | ||
if (this.options.source) { | ||
@@ -609,3 +624,3 @@ lintAgainOptions.source = this.lintResult.files[0].updatedSource; | ||
const initialResfileErrors = initialResults.files[fileNm].errors; | ||
const afterFixResfileErrors = afterFixResults.files[fileNm].errors; | ||
const afterFixResfileErrors = afterFixResults.files[fileNm] ? afterFixResults.files[fileNm].errors : []; | ||
const fileDtl = { | ||
@@ -626,3 +641,3 @@ errors: afterFixResfileErrors, | ||
updatedResults.summary.fixedErrorsIds = fixedErrorsIds; | ||
debug(`Merged results summary ${JSON.stringify(updatedResults.summary)}`); | ||
return updatedResults; | ||
@@ -629,0 +644,0 @@ } |
@@ -17,2 +17,7 @@ // Shared functions | ||
// Get indent length | ||
function getIndentLength() { | ||
return 4; | ||
} | ||
// Evaluate variables from messages | ||
@@ -57,2 +62,118 @@ function evaluateVariables(variableDefs, msg) { | ||
module.exports = { evaluateVariables, getSourceLines, evaluateRange }; | ||
function getVariable(evaluatedVars, name, optns = { mandatory: true, decodeHtml: false, line: "" }) { | ||
const matchingVars = evaluatedVars.filter(evaluatedVar => evaluatedVar.name === name); | ||
if (matchingVars && matchingVars.length > 0) { | ||
return optns.decodeHtml ? decodeHtml(matchingVars[0].value) : matchingVars[0].value; | ||
} else if (optns.mandatory) { | ||
throw new Error("NGL fix: missing mandatory variable " + name + " in " + JSON.stringify(evaluatedVars)) + "for line :\n" + optns.line; | ||
} else { | ||
return null; | ||
} | ||
} | ||
function getStringRange(errLine, str, errItem) { | ||
const varStartPos = errLine.indexOf(str); | ||
return { | ||
start: { line: errItem.line, character: varStartPos }, | ||
end: { line: errItem.line, character: varStartPos + str.length } | ||
}; | ||
} | ||
function getStringRangeMultiline(allLines, str, errItem) { | ||
let range = getDefaultRange(allLines, errItem); | ||
let pos = errItem.line - 1; | ||
let isFound = false; | ||
while (isFound === false && pos < allLines.length) { | ||
if (!isFound && allLines[pos].indexOf(str) > -1) { | ||
const varStartPos = allLines[pos].indexOf(str); | ||
range = { | ||
start: { line: errItem.line, character: varStartPos }, | ||
end: { line: errItem.line, character: varStartPos + str.length } | ||
}; | ||
isFound = true; | ||
} | ||
pos++; | ||
} | ||
return range; | ||
} | ||
function getLastStringRange(errLine, str, errItem) { | ||
const varStartPos = errLine.lastIndexOf(str); | ||
return { | ||
start: { line: errItem.line, character: varStartPos }, | ||
end: { line: errItem.line, character: varStartPos + str.length } | ||
}; | ||
} | ||
function getVariableRange(errLine, evaluatedVars, variable, errItem) { | ||
const varValue = getVariable(evaluatedVars, variable); | ||
return getStringRange(errLine, varValue, errItem); | ||
} | ||
/* | ||
function getLastVariableRange(errLine, evaluatedVars, variable, errItem) { | ||
const varValue = getVariable(evaluatedVars, variable); | ||
return getLastStringRange(errLine, varValue, errItem); | ||
} | ||
*/ | ||
function findRangeBetweenStrings(allLines, errItem, strStart, strEnd) { | ||
let range = getDefaultRange(allLines, errItem); | ||
let pos = errItem.line - 1; | ||
let isStartFound = false; | ||
let isEndFound = false; | ||
while ((isStartFound === false || isEndFound === false) && pos < allLines.length) { | ||
if (!isStartFound && allLines[pos].indexOf(strStart) > -1) { | ||
range.start = { line: pos + 1, character: allLines[pos].indexOf(strStart) }; | ||
isStartFound = true; | ||
} | ||
if (!isEndFound && allLines[pos].indexOf(strEnd) > -1) { | ||
range.end = { line: pos + 1, character: allLines[pos].indexOf(strEnd) }; | ||
isEndFound = true; | ||
} | ||
pos++; | ||
} | ||
return range; | ||
} | ||
function getDefaultRange(allLines, errItem) { | ||
return { | ||
start: { line: errItem.line, character: 0 }, | ||
end: { line: errItem.line, character: allLines[errItem.line - 1].length } | ||
}; | ||
} | ||
function isValidCodeLine(line) { | ||
return line.trim() !== "" && line.trim().split("//")[0] !== ""; | ||
} | ||
function addSpaceAroundChar(line, char) { | ||
let pos = -1; | ||
const splits = line.split(char); | ||
const newArray = splits.map(str => { | ||
pos++; | ||
if (pos === 0) { | ||
return str.trimEnd(); | ||
} else if (pos === splits.length - 1) { | ||
return str.trimStart(); | ||
} else { | ||
return str.trim(); | ||
} | ||
}); | ||
return newArray.join(" " + char + " ").trimEnd(); | ||
} | ||
module.exports = { | ||
addSpaceAroundChar, | ||
evaluateRange, | ||
evaluateVariables, | ||
findRangeBetweenStrings, | ||
getIndentLength, | ||
getLastStringRange, | ||
getSourceLines, | ||
getStringRange, | ||
getStringRangeMultiline, | ||
getVariable, | ||
getVariableRange, | ||
isValidCodeLine | ||
}; |
{ | ||
"name": "npm-groovy-lint", | ||
"version": "3.0.0-beta.4", | ||
"version": "3.0.0-beta.5", | ||
"description": "NPM CodeNarc wrapper to easily lint Groovy files", | ||
@@ -60,2 +60,3 @@ "main": "index.js", | ||
"babel-eslint": "^10.0.3", | ||
"diff": "^4.0.2", | ||
"eslint": "^6.8.0", | ||
@@ -62,0 +63,0 @@ "eslint-config-standard": "^14.1.0", |
Sorry, the diff of this file is not supported yet
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
16189041
89
2732
13
13