From dce94536802a60d89eb9feb633a1388d8ef310a0 Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Tue, 4 May 2021 08:27:06 +0100 Subject: [PATCH 1/3] feat(format-diagnostics): add diagnostic information to format result payload --- src/format/formatFile.ts | 27 +++++++++++++++++++++++++-- src/format/formatFolder.ts | 36 ++++++++++++++++++++++++++++++++++-- src/format/formatProject.ts | 7 +++++-- src/types/FormatResult.ts | 7 +++++++ src/types/index.ts | 1 + 5 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 src/types/FormatResult.ts diff --git a/src/format/formatFile.ts b/src/format/formatFile.ts index fa6950f..87bd36f 100644 --- a/src/format/formatFile.ts +++ b/src/format/formatFile.ts @@ -1,4 +1,6 @@ import { createFile, readFile } from '@sasjs/utils/file' +import { lintFile } from '../lint' +import { FormatResult } from '../types' import { LintConfig } from '../types/LintConfig' import { getLintConfig } from '../utils/getLintConfig' import { processText } from './shared' @@ -7,16 +9,37 @@ import { processText } from './shared' * Applies automatic formatting to the file at the given path. * @param {string} filePath - the path to the file to be formatted. * @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file. - * @returns {Promise} Resolves successfully when the file has been formatted. + * @returns {Promise} Resolves successfully when the file has been formatted. */ export const formatFile = async ( filePath: string, configuration?: LintConfig -) => { +): Promise => { const config = configuration || (await getLintConfig()) + const diagnosticsBeforeFormat = await lintFile(filePath) + const diagnosticsCountBeforeFormat = diagnosticsBeforeFormat.length + const text = await readFile(filePath) const formattedText = processText(text, config) await createFile(filePath, formattedText) + + const diagnosticsAfterFormat = await lintFile(filePath) + const diagnosticsCountAfterFormat = diagnosticsAfterFormat.length + + const fixedDiagnosticsCount = + diagnosticsCountBeforeFormat - diagnosticsCountAfterFormat + + const updatedFilePaths: string[] = [] + + if (fixedDiagnosticsCount) { + updatedFilePaths.push(filePath) + } + + return { + updatedFilePaths, + fixedDiagnosticsCount, + unfixedDiagnostics: diagnosticsAfterFormat + } } diff --git a/src/format/formatFolder.ts b/src/format/formatFolder.ts index b7ec86f..0685822 100644 --- a/src/format/formatFolder.ts +++ b/src/format/formatFolder.ts @@ -1,5 +1,7 @@ import { listSubFoldersInFolder } from '@sasjs/utils/file' import path from 'path' +import { lintFolder } from '../lint' +import { FormatResult } from '../types' import { LintConfig } from '../types/LintConfig' import { asyncForEach } from '../utils/asyncForEach' import { getLintConfig } from '../utils/getLintConfig' @@ -19,13 +21,18 @@ const excludeFolders = [ * Automatically formats all SAS files in the folder at the given path. * @param {string} folderPath - the path to the folder to be formatted. * @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file. - * @returns {Promise} Resolves successfully when all SAS files in the given folder have been formatted. + * @returns {Promise} Resolves successfully when all SAS files in the given folder have been formatted. */ export const formatFolder = async ( folderPath: string, configuration?: LintConfig -) => { +): Promise => { const config = configuration || (await getLintConfig()) + const diagnosticsBeforeFormat = await lintFolder(folderPath) + const diagnosticsCountBeforeFormat = Array.from( + diagnosticsBeforeFormat.values() + ).reduce((a, b) => a + b.length, 0) + const fileNames = await listSasFiles(folderPath) await asyncForEach(fileNames, async (fileName) => { const filePath = path.join(folderPath, fileName) @@ -39,4 +46,29 @@ export const formatFolder = async ( await asyncForEach(subFolders, async (subFolder) => { await formatFolder(path.join(folderPath, subFolder), config) }) + + const diagnosticsAfterFormat = await lintFolder(folderPath) + const diagnosticsCountAfterFormat = Array.from( + diagnosticsAfterFormat.values() + ).reduce((a, b) => a + b.length, 0) + + const fixedDiagnosticsCount = + diagnosticsCountBeforeFormat - diagnosticsCountAfterFormat + + const updatedFilePaths: string[] = [] + + Array.from(diagnosticsBeforeFormat.keys()).forEach((filePath) => { + const diagnosticsBefore = diagnosticsBeforeFormat.get(filePath) || [] + const diagnosticsAfter = diagnosticsAfterFormat.get(filePath) || [] + + if (diagnosticsBefore.length !== diagnosticsAfter.length) { + updatedFilePaths.push(filePath) + } + }) + + return { + updatedFilePaths, + fixedDiagnosticsCount, + unfixedDiagnostics: diagnosticsAfterFormat + } } diff --git a/src/format/formatProject.ts b/src/format/formatProject.ts index c0eb2fd..7c8033d 100644 --- a/src/format/formatProject.ts +++ b/src/format/formatProject.ts @@ -1,15 +1,18 @@ +import { lintFolder } from '../lint/lintFolder' +import { FormatResult } from '../types/FormatResult' import { getProjectRoot } from '../utils/getProjectRoot' import { formatFolder } from './formatFolder' /** * Automatically formats all SAS files in the current project. - * @returns {Promise} Resolves successfully when all SAS files in the current project have been formatted. + * @returns {Promise} Resolves successfully when all SAS files in the current project have been formatted. */ -export const formatProject = async () => { +export const formatProject = async (): Promise => { const projectRoot = (await getProjectRoot()) || process.projectDir || process.currentDir if (!projectRoot) { throw new Error('SASjs Project Root was not found.') } + return await formatFolder(projectRoot) } diff --git a/src/types/FormatResult.ts b/src/types/FormatResult.ts new file mode 100644 index 0000000..39aee10 --- /dev/null +++ b/src/types/FormatResult.ts @@ -0,0 +1,7 @@ +import { Diagnostic } from './Diagnostic' + +export interface FormatResult { + updatedFilePaths: string[] + fixedDiagnosticsCount: number + unfixedDiagnostics: Map | Diagnostic[] +} diff --git a/src/types/index.ts b/src/types/index.ts index 148327e..aba5333 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,5 @@ export * from './Diagnostic' +export * from './FormatResult' export * from './LintConfig' export * from './LintRule' export * from './LintRuleType' From bc011c4b47453771e5c674f34b69a4c12e5e8360 Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Thu, 6 May 2021 07:22:29 +0100 Subject: [PATCH 2/3] chore(*): add tests for new functionality --- src/format/formatFile.spec.ts | 73 +++++++++++-- src/format/formatFolder.spec.ts | 181 ++++++++++++++++++++++++++++++-- 2 files changed, 239 insertions(+), 15 deletions(-) diff --git a/src/format/formatFile.spec.ts b/src/format/formatFile.spec.ts index 4581596..02cf58c 100644 --- a/src/format/formatFile.spec.ts +++ b/src/format/formatFile.spec.ts @@ -8,22 +8,76 @@ describe('formatFile', () => { const content = `%macro somemacro(); \n%put 'hello';\n%mend;` const expectedContent = `/**\n @file\n @brief \n

SAS Macros

\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;` await createFile(path.join(__dirname, 'format-file-test.sas'), content) + const expectedResult = { + updatedFilePaths: [path.join(__dirname, 'format-file-test.sas')], + fixedDiagnosticsCount: 3, + unfixedDiagnostics: [] + } - await formatFile(path.join(__dirname, 'format-file-test.sas')) - const result = await readFile(path.join(__dirname, 'format-file-test.sas')) + const result = await formatFile( + path.join(__dirname, 'format-file-test.sas') + ) + const formattedContent = await readFile( + path.join(__dirname, 'format-file-test.sas') + ) - expect(result).toEqual(expectedContent) + expect(result).toEqual(expectedResult) + expect(formattedContent).toEqual(expectedContent) await deleteFile(path.join(__dirname, 'format-file-test.sas')) }) it('should use the provided config if available', async () => { const content = `%macro somemacro(); \n%put 'hello';\n%mend;` - const expectedContent = `/**\r\n @file\r\n @brief \r\n

SAS Macros

\r\n**/\r\n%macro somemacro();\r\n%put 'hello';\r\n%mend somemacro;` + const expectedContent = `/**\r\n @file\r\n @brief \r\n

SAS Macros

\r\n**/\r\n%macro somemacro();\r\n%put 'hello';\r\n%mend;` + const expectedResult = { + updatedFilePaths: [path.join(__dirname, 'format-file-config.sas')], + fixedDiagnosticsCount: 2, + unfixedDiagnostics: [ + { + endColumnNumber: 7, + lineNumber: 8, + message: '%mend statement is missing macro name - somemacro', + severity: 1, + startColumnNumber: 1 + } + ] + } await createFile(path.join(__dirname, 'format-file-config.sas'), content) - await formatFile( + const result = await formatFile( path.join(__dirname, 'format-file-config.sas'), + new LintConfig({ + lineEndings: 'crlf', + hasMacroNameInMend: false, + hasDoxygenHeader: true, + noTrailingSpaces: true + }) + ) + const formattedContent = await readFile( + path.join(__dirname, 'format-file-config.sas') + ) + + expect(result).toEqual(expectedResult) + expect(formattedContent).toEqual(expectedContent) + + await deleteFile(path.join(__dirname, 'format-file-config.sas')) + }) + + it('should not update any files if there are no formatting violations', async () => { + const content = `/**\r\n @file\r\n @brief \r\n

SAS Macros

\r\n**/\r\n%macro somemacro();\r\n%put 'hello';\r\n%mend somemacro;` + const expectedResult = { + updatedFilePaths: [], + fixedDiagnosticsCount: 0, + unfixedDiagnostics: [] + } + await createFile( + path.join(__dirname, 'format-file-no-violations.sas'), + content + ) + + const result = await formatFile( + path.join(__dirname, 'format-file-no-violations.sas'), new LintConfig({ lineEndings: 'crlf', hasMacroNameInMend: true, @@ -31,12 +85,13 @@ describe('formatFile', () => { noTrailingSpaces: true }) ) - const result = await readFile( - path.join(__dirname, 'format-file-config.sas') + const formattedContent = await readFile( + path.join(__dirname, 'format-file-no-violations.sas') ) - expect(result).toEqual(expectedContent) + expect(result).toEqual(expectedResult) + expect(formattedContent).toEqual(content) - await deleteFile(path.join(__dirname, 'format-file-config.sas')) + await deleteFile(path.join(__dirname, 'format-file-no-violations.sas')) }) }) diff --git a/src/format/formatFolder.spec.ts b/src/format/formatFolder.spec.ts index 389813b..b9aa1ec 100644 --- a/src/format/formatFolder.spec.ts +++ b/src/format/formatFolder.spec.ts @@ -6,23 +6,39 @@ import { deleteFolder, readFile } from '@sasjs/utils/file' +import { Diagnostic, LintConfig } from '../types' describe('formatFolder', () => { it('should fix linting issues in a given folder', async () => { const content = `%macro somemacro(); \n%put 'hello';\n%mend;` const expectedContent = `/**\n @file\n @brief \n

SAS Macros

\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;` + const expectedResult = { + updatedFilePaths: [ + path.join(__dirname, 'format-folder-test', 'format-folder-test.sas') + ], + fixedDiagnosticsCount: 3, + unfixedDiagnostics: new Map([ + [ + path.join(__dirname, 'format-folder-test', 'format-folder-test.sas'), + [] + ] + ]) + } await createFolder(path.join(__dirname, 'format-folder-test')) await createFile( path.join(__dirname, 'format-folder-test', 'format-folder-test.sas'), content ) - await formatFolder(path.join(__dirname, 'format-folder-test')) - const result = await readFile( + const result = await formatFolder( + path.join(__dirname, 'format-folder-test') + ) + const formattedContent = await readFile( path.join(__dirname, 'format-folder-test', 'format-folder-test.sas') ) - expect(result).toEqual(expectedContent) + expect(formattedContent).toEqual(expectedContent) + expect(result).toEqual(expectedResult) await deleteFolder(path.join(__dirname, 'format-folder-test')) }) @@ -30,6 +46,29 @@ describe('formatFolder', () => { it('should fix linting issues in subfolders of a given folder', async () => { const content = `%macro somemacro(); \n%put 'hello';\n%mend;` const expectedContent = `/**\n @file\n @brief \n

SAS Macros

\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;` + const expectedResult = { + updatedFilePaths: [ + path.join( + __dirname, + 'format-folder-test', + 'subfolder', + 'format-folder-test.sas' + ) + ], + fixedDiagnosticsCount: 3, + unfixedDiagnostics: new Map([ + [ + path.join( + __dirname, + 'format-folder-test', + 'subfolder', + 'format-folder-test.sas' + ), + [] + ] + ]) + } + await createFolder(path.join(__dirname, 'format-folder-test')) await createFolder(path.join(__dirname, 'subfolder')) await createFile( @@ -42,8 +81,10 @@ describe('formatFolder', () => { content ) - await formatFolder(path.join(__dirname, 'format-folder-test')) - const result = await readFile( + const result = await formatFolder( + path.join(__dirname, 'format-folder-test') + ) + const formattedContent = await readFile( path.join( __dirname, 'format-folder-test', @@ -52,7 +93,135 @@ describe('formatFolder', () => { ) ) - expect(result).toEqual(expectedContent) + expect(result).toEqual(expectedResult) + expect(formattedContent).toEqual(expectedContent) + + await deleteFolder(path.join(__dirname, 'format-folder-test')) + }) + + it('should use a custom configuration when provided', async () => { + const content = `%macro somemacro(); \n%put 'hello';\n%mend;` + const expectedContent = `/**\n @file\n @brief \n

SAS Macros

\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;` + const expectedResult = { + updatedFilePaths: [ + path.join(__dirname, 'format-folder-test', 'format-folder-test.sas') + ], + fixedDiagnosticsCount: 3, + unfixedDiagnostics: new Map([ + [ + path.join(__dirname, 'format-folder-test', 'format-folder-test.sas'), + [] + ] + ]) + } + await createFolder(path.join(__dirname, 'format-folder-test')) + await createFile( + path.join(__dirname, 'format-folder-test', 'format-folder-test.sas'), + content + ) + + const result = await formatFolder( + path.join(__dirname, 'format-folder-test'), + new LintConfig({ + lineEndings: 'crlf', + hasMacroNameInMend: false, + hasDoxygenHeader: true, + noTrailingSpaces: true + }) + ) + const formattedContent = await readFile( + path.join(__dirname, 'format-folder-test', 'format-folder-test.sas') + ) + + expect(formattedContent).toEqual(expectedContent) + expect(result).toEqual(expectedResult) + + await deleteFolder(path.join(__dirname, 'format-folder-test')) + }) + + it('should fix linting issues in subfolders of a given folder', async () => { + const content = `%macro somemacro(); \n%put 'hello';\n%mend;` + const expectedContent = `/**\n @file\n @brief \n

SAS Macros

\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;` + const expectedResult = { + updatedFilePaths: [ + path.join( + __dirname, + 'format-folder-test', + 'subfolder', + 'format-folder-test.sas' + ) + ], + fixedDiagnosticsCount: 3, + unfixedDiagnostics: new Map([ + [ + path.join( + __dirname, + 'format-folder-test', + 'subfolder', + 'format-folder-test.sas' + ), + [] + ] + ]) + } + + await createFolder(path.join(__dirname, 'format-folder-test')) + await createFolder(path.join(__dirname, 'subfolder')) + await createFile( + path.join( + __dirname, + 'format-folder-test', + 'subfolder', + 'format-folder-test.sas' + ), + content + ) + + const result = await formatFolder( + path.join(__dirname, 'format-folder-test') + ) + const formattedContent = await readFile( + path.join( + __dirname, + 'format-folder-test', + 'subfolder', + 'format-folder-test.sas' + ) + ) + + expect(result).toEqual(expectedResult) + expect(formattedContent).toEqual(expectedContent) + + await deleteFolder(path.join(__dirname, 'format-folder-test')) + }) + + it('should not update any files when there are no violations', async () => { + const content = `/**\n @file\n @brief \n

SAS Macros

\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;` + const expectedResult = { + updatedFilePaths: [], + fixedDiagnosticsCount: 0, + unfixedDiagnostics: new Map([ + [ + path.join(__dirname, 'format-folder-test', 'format-folder-test.sas'), + [] + ] + ]) + } + await createFolder(path.join(__dirname, 'format-folder-test')) + await createFile( + path.join(__dirname, 'format-folder-test', 'format-folder-test.sas'), + content + ) + + const result = await formatFolder( + path.join(__dirname, 'format-folder-test') + ) + const formattedContent = await readFile( + path.join(__dirname, 'format-folder-test', 'format-folder-test.sas') + ) + + expect(formattedContent).toEqual(content) + expect(result).toEqual(expectedResult) await deleteFolder(path.join(__dirname, 'format-folder-test')) }) From e3295294842fd50bb463dc5eac9930c645854f31 Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Thu, 6 May 2021 07:24:07 +0100 Subject: [PATCH 3/3] chore(*): add comment --- src/types/FormatResult.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/FormatResult.ts b/src/types/FormatResult.ts index 39aee10..3bf7e0c 100644 --- a/src/types/FormatResult.ts +++ b/src/types/FormatResult.ts @@ -1,5 +1,8 @@ import { Diagnostic } from './Diagnostic' +/** + * Represents the result of a format operation on a file, folder or project. + */ export interface FormatResult { updatedFilePaths: string[] fixedDiagnosticsCount: number