Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 97 additions & 62 deletions src/components/CodeEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const MONACO_EDITOR_OPTIONS = {
glyphMargin: true,
lineNumbersMinChars: 3,
scrollbar: {
vertical: 'hidden',
vertical: 'auto',
horizontal: 'hidden'
}
};
Expand Down Expand Up @@ -66,6 +66,11 @@ watch(() => store.codeEditorCode, (newCode) => {
if (model && model.getValue() !== newCode) {
model.setValue(newCode);
}

editorRef.value.changeViewZones(accessor => {
// Remove existing annotation zones
componentState.exisitingAnnotationZones.forEach(zone => accessor.removeZone(zone));
});
}
}, { deep: true });

Expand Down Expand Up @@ -104,95 +109,125 @@ function determineHighlightLinesFromTestResult(rawTestResults) {
if (!rawTestResults || !Object.entries(rawTestResults.results).length) return;

let newDecorations = [];
store.jsonResult.parsedTestResults = [];

// Check for comments indicating false positives (//ok or // ok)
const model = editorRef.value.getModel();
const linesCount = model.getLineCount();
for (let lineNumber = 1; lineNumber <= linesCount; lineNumber++) {
const lineContent = model.getLineContent(lineNumber);
if (lineContent.includes('ok: ') || lineContent.includes('ruleid: ')) {
store.jsonResult.parsedTestResults.push({
mustMatch: false,
lineNumber: ++lineNumber,
ruleId: null,
status: 'SUCCESS'
});

newDecorations.push({
range: new monaco.Range(lineNumber - 1, 1, lineNumber - 1, 1),
options: {
isWholeLine: true,
className: "full-line-highlight-added",
glyphMarginClassName: "diff-added-gutter"
}
});
}
}

store.jsonResult.parsedTestResults = [];
let expectedSet = null;
let reportedSet = null;

// Extract the matches from the JSON
Object.entries(rawTestResults.results[store.ruleFilePath].checks).forEach(([ruleId, check]) => {
Object.entries(check.matches || {}).forEach(([, matchData]) => {
const { expected_lines = [], reported_lines = [] } = matchData;

// Convert arrays to sets for easier comparison
const expectedSet = new Set(expected_lines);
const reportedSet = new Set(reported_lines);

// Highlight expected lines (Green - should be present)
expectedSet = new Set(expected_lines);
reportedSet = new Set(reported_lines);

/** if expected but not reported then
* ok ==> must not match green
* ruleid ==> must match red
* nothing ==> empty
*/
expected_lines.forEach((line) => {
newDecorations = newDecorations.map(decoration => {
if (decoration.range.startLineNumber === line - 1) {
return {
...decoration,
options: {
...decoration.options,
className: reportedSet.has(line) ? "full-line-highlight-added" : "full-line-highlight-removed",
glyphMarginClassName: reportedSet.has(line) ? "diff-added-gutter" : "diff-removed-gutter"
}
if (!reportedSet.has(line)) {
newDecorations.push({
range: new monaco.Range(line - 1, 1, line - 1, 1),
options: {
isWholeLine: true,
className: "full-line-highlight-removed",
glyphMarginClassName: "diff-removed-gutter"
}
}
return decoration;
});

store.jsonResult.parsedTestResults = store.jsonResult.parsedTestResults.map(testResult => {
if (testResult.lineNumber === line) {
return {
...testResult,
mustMatch: reportedSet.has(line),
ruleId,
status: reportedSet.has(line) ? 'SUCCESS' : 'FAILED'
};
}
});

return testResult;
});
store.jsonResult.parsedTestResults.push({
lineNumber: line,
mustMatch: true,
ruleId,
status: 'FAILED'
});
}
});

// Highlight reported lines that are not in expected lines (Red - detected issues)
reported_lines.forEach(line => {
/** if reported but not expected then
* ok comment ==> must not match red
* ruleid comment ==> must match green
* no comment ==> empty
*/
reported_lines.forEach((line) => {
if (!expectedSet.has(line)) {
newDecorations.push({
range: new monaco.Range(line, 1, line, 1),
range: new monaco.Range(line - 1, 1, line - 1, 1),
options: {
isWholeLine: true,
className: "full-line-highlight-unexpected",
glyphMarginClassName: "diff-unexpected-gutter"
className: "full-line-highlight-removed",
glyphMarginClassName: "diff-removed-gutter"
}
});

store.jsonResult.parsedTestResults.push({
lineNumber: line,
mustMatch: false,
ruleId,
status: 'UNTESTED'
})
status: 'FAILED'
});
}
});

/** if reported and expected then
* ok ==> must not match red
* ruleid ==> must match green
* nothing ==> empty
*/
expected_lines.forEach((line) => {
if (reportedSet.has(line)) {
newDecorations.push({
range: new monaco.Range(line - 1, 1, line - 1, 1),
options: {
isWholeLine: true,
className: "full-line-highlight-added",
glyphMarginClassName: "diff-added-gutter"
}
});

store.jsonResult.parsedTestResults.push({
lineNumber: line,
mustMatch: true,
ruleId,
status: 'SUCCESS',
});
}
});
});

/**
* if no reported an no expected but indicated with the ignore command (ok: ) then
* highlight as green not match
*/
const model = editorRef.value.getModel();
const linesCount = model.getLineCount();
for (let lineNumber = 1; lineNumber <= linesCount; lineNumber++) {
const previousLineContent = model.getLineContent(lineNumber);
if (previousLineContent.includes('ok: ') && !expectedSet.has(lineNumber + 1) && !reportedSet.has(lineNumber + 1)) {
store.jsonResult.parsedTestResults.push({
mustMatch: false,
lineNumber: lineNumber + 1,
ruleId: null,
status: 'SUCCESS'
});

newDecorations.push({
range: new monaco.Range(lineNumber, 1, lineNumber, 1),
options: {
isWholeLine: true,
className: "full-line-highlight-added",
glyphMarginClassName: "diff-added-gutter"
}
});
}
}
});

store.jsonResult.parsedTestResults.sort((a, b) => a.lineNumber - b.lineNumber);

// Apply decorations in Monaco Editor
componentState.existingHighlightLinesFromTestResult = editorRef.value.deltaDecorations(componentState.existingHighlightLinesFromTestResult, newDecorations);
}
Expand Down
4 changes: 4 additions & 0 deletions src/components/RuleEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const MONACO_EDITOR_OPTIONS = {
renderWhitespace: "boundary",
glyphMargin: true,
lineNumbersMinChars: 3,
scrollbar: {
vertical: 'auto',
horizontal: 'hidden'
}
};
const languageMappings = {
js: { ext: "js", monaco: "javascript" },
Expand Down
11 changes: 7 additions & 4 deletions src/components/RuleResults.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
</div>

<!-- TEST RESULTS -->
<h4 @click="toggleSection('testResults')" style="cursor: pointer;">Test Results </h4>
<h4 @click="toggleSection('testResults')" style="cursor: pointer;">
Test Results ({{ store.jsonResult?.parsedTestResults.filter(test => test.status === 'SUCCESS').length }}/{{ store.jsonResult?.parsedTestResults.length }} Unit Tests Passed)
</h4>
<div style="flex: 1" class="scrollable-section">
<div v-if="isTestLoading" class="loading-container">
<div class="loading-circle"></div>
Expand Down Expand Up @@ -116,8 +118,8 @@ async function handleRunBinary() {
try {
await runBinaryForTests(binaryPath);
} catch (error) {
showErrorDialog(`Error running tests: Please consult the error.log file at ${store.safeDir}`, error);
console.error("Error running tests:", error);
showErrorDialog(`Error running tests: Please consult the error.log file at ${store.safeDir}`, error);
} finally {
isTestLoading.value = false;
}
Expand All @@ -127,9 +129,9 @@ async function handleRunBinary() {
try {
await runBinaryForScan(binaryPath, false);
} catch (error) {
console.error("Error running scanning:", error);
if (!retryAttempted) {
showErrorDialog(`Error running scanning: Please consult the error.log file at ${store.safeDir}`, error);
console.error("Error running scanning:", error);
return;
}

Expand All @@ -152,6 +154,7 @@ async function runBinaryForScan(binaryPath, runScanWithoutMatchingExplanations)
if (platform.value === 'win32') {
scanArgs.push('-j 1');
}

if (!runScanWithoutMatchingExplanations) {
scanArgs.push('--matching-explanations');
}
Expand Down Expand Up @@ -180,7 +183,7 @@ function extractScanErrors(jsonOutput) {
}

async function runBinaryForTests(binaryPath) {
const testArgs = ['test', `-f "${store.ruleFilePath}" "${store.codeSampleFilePath}"`, '--json', '--experimental'];
const testArgs = ['scan --test', `-f "${store.ruleFilePath}" "${store.codeSampleFilePath}"`, '--json', '--experimental'];

const testResponse = await runBinary(`"${binaryPath}"`, testArgs)
const testResults = JSON.parse(testResponse.output);
Expand Down