diff --git a/src/format/formatFile.spec.ts b/src/format/formatFile.spec.ts new file mode 100644 index 0000000..d4ba1b5 --- /dev/null +++ b/src/format/formatFile.spec.ts @@ -0,0 +1,42 @@ +import { formatFile } from './formatFile' +import path from 'path' +import { createFile, deleteFile, readFile } from '@sasjs/utils/file' +import { LintConfig } from '../types' + +describe('formatFile', () => { + it('should fix linting issues in a given file', 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;\n` + await createFile(path.join(__dirname, 'format-file-test.sas'), content) + + await formatFile(path.join(__dirname, 'format-file-test.sas')) + const result = await readFile(path.join(__dirname, 'format-file-test.sas')) + + expect(result).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;\r\n` + await createFile(path.join(__dirname, 'format-file-config.sas'), content) + + await formatFile( + path.join(__dirname, 'format-file-config.sas'), + new LintConfig({ + lineEndings: 'crlf', + hasMacroNameInMend: true, + hasDoxygenHeader: true, + noTrailingSpaces: true + }) + ) + const result = await readFile( + path.join(__dirname, 'format-file-config.sas') + ) + + expect(result).toEqual(expectedContent) + + await deleteFile(path.join(__dirname, 'format-file-config.sas')) + }) +}) diff --git a/src/format/formatFile.ts b/src/format/formatFile.ts new file mode 100644 index 0000000..fa6950f --- /dev/null +++ b/src/format/formatFile.ts @@ -0,0 +1,22 @@ +import { createFile, readFile } from '@sasjs/utils/file' +import { LintConfig } from '../types/LintConfig' +import { getLintConfig } from '../utils/getLintConfig' +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. + */ +export const formatFile = async ( + filePath: string, + configuration?: LintConfig +) => { + const config = configuration || (await getLintConfig()) + const text = await readFile(filePath) + + const formattedText = processText(text, config) + + await createFile(filePath, formattedText) +} diff --git a/src/format/formatFolder.spec.ts b/src/format/formatFolder.spec.ts new file mode 100644 index 0000000..5bda372 --- /dev/null +++ b/src/format/formatFolder.spec.ts @@ -0,0 +1,59 @@ +import { formatFolder } from './formatFolder' +import path from 'path' +import { + createFile, + createFolder, + deleteFolder, + readFile +} from '@sasjs/utils/file' + +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;\n` + 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( + path.join(__dirname, 'format-folder-test', 'format-folder-test.sas') + ) + + expect(result).toEqual(expectedContent) + + 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;\n` + 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 + ) + + await formatFolder(path.join(__dirname, 'format-folder-test')) + const result = await readFile( + path.join( + __dirname, + 'format-folder-test', + 'subfolder', + 'format-folder-test.sas' + ) + ) + + expect(result).toEqual(expectedContent) + + await deleteFolder(path.join(__dirname, 'format-folder-test')) + }) +}) diff --git a/src/format/formatFolder.ts b/src/format/formatFolder.ts new file mode 100644 index 0000000..b7ec86f --- /dev/null +++ b/src/format/formatFolder.ts @@ -0,0 +1,42 @@ +import { listSubFoldersInFolder } from '@sasjs/utils/file' +import path from 'path' +import { LintConfig } from '../types/LintConfig' +import { asyncForEach } from '../utils/asyncForEach' +import { getLintConfig } from '../utils/getLintConfig' +import { listSasFiles } from '../utils/listSasFiles' +import { formatFile } from './formatFile' + +const excludeFolders = [ + '.git', + '.github', + '.vscode', + 'node_modules', + 'sasjsbuild', + 'sasjsresults' +] + +/** + * 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. + */ +export const formatFolder = async ( + folderPath: string, + configuration?: LintConfig +) => { + const config = configuration || (await getLintConfig()) + const fileNames = await listSasFiles(folderPath) + await asyncForEach(fileNames, async (fileName) => { + const filePath = path.join(folderPath, fileName) + await formatFile(filePath) + }) + + const subFolders = (await listSubFoldersInFolder(folderPath)).filter( + (f: string) => !excludeFolders.includes(f) + ) + + await asyncForEach(subFolders, async (subFolder) => { + await formatFolder(path.join(folderPath, subFolder), config) + }) +} diff --git a/src/format/formatProject.spec.ts b/src/format/formatProject.spec.ts new file mode 100644 index 0000000..dc275eb --- /dev/null +++ b/src/format/formatProject.spec.ts @@ -0,0 +1,51 @@ +import { formatProject } from './formatProject' +import path from 'path' +import { + createFile, + createFolder, + deleteFolder, + readFile +} from '@sasjs/utils/file' +import { DefaultLintConfiguration } from '../utils' +import * as getProjectRootModule from '../utils/getProjectRoot' +jest.mock('../utils/getProjectRoot') + +describe('formatProject', () => { + it('should format files in the current project', 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;\n` + await createFolder(path.join(__dirname, 'format-project-test')) + await createFile( + path.join(__dirname, 'format-project-test', 'format-project-test.sas'), + content + ) + await createFile( + path.join(__dirname, 'format-project-test', '.sasjslint'), + JSON.stringify(DefaultLintConfiguration) + ) + jest + .spyOn(getProjectRootModule, 'getProjectRoot') + .mockImplementation(() => + Promise.resolve(path.join(__dirname, 'format-project-test')) + ) + + await formatProject() + const result = await readFile( + path.join(__dirname, 'format-project-test', 'format-project-test.sas') + ) + + expect(result).toEqual(expectedContent) + + await deleteFolder(path.join(__dirname, 'format-project-test')) + }) + + it('should throw an error when a project root is not found', async () => { + jest + .spyOn(getProjectRootModule, 'getProjectRoot') + .mockImplementationOnce(() => Promise.resolve('')) + + await expect(formatProject()).rejects.toThrowError( + 'SASjs Project Root was not found.' + ) + }) +}) diff --git a/src/format/formatProject.ts b/src/format/formatProject.ts new file mode 100644 index 0000000..c0eb2fd --- /dev/null +++ b/src/format/formatProject.ts @@ -0,0 +1,15 @@ +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. + */ +export const formatProject = async () => { + 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/format/index.ts b/src/format/index.ts new file mode 100644 index 0000000..5bff5f1 --- /dev/null +++ b/src/format/index.ts @@ -0,0 +1,4 @@ +export * from './formatText' +export * from './formatFile' +export * from './formatFolder' +export * from './formatProject' diff --git a/src/formatExample.ts b/src/formatExample.ts index 10a428c..816c608 100644 --- a/src/formatExample.ts +++ b/src/formatExample.ts @@ -1,3 +1,5 @@ +import { formatFile } from './format/formatFile' +import path from 'path' import { formatText } from './format/formatText' import { lintText } from './lint' @@ -7,15 +9,17 @@ const content = `%put 'Hello'; %put 'test'; %mend;\r\n` -console.log(content) -lintText(content).then((diagnostics) => { - console.log('Before Formatting:') - console.table(diagnostics) - formatText(content).then((formattedText) => { - lintText(formattedText).then((newDiagnostics) => { - console.log('After Formatting:') - console.log(formattedText) - console.table(newDiagnostics) - }) - }) -}) +// console.log(content) +// lintText(content).then((diagnostics) => { +// console.log('Before Formatting:') +// console.table(diagnostics) +// formatText(content).then((formattedText) => { +// lintText(formattedText).then((newDiagnostics) => { +// console.log('After Formatting:') +// console.log(formattedText) +// console.table(newDiagnostics) +// }) +// }) +// }) + +formatFile(path.join(__dirname, 'Example File.sas')) diff --git a/src/index.ts b/src/index.ts index 7ed1b17..160eb2c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +export * from './format' export * from './lint' export * from './types' export * from './utils' diff --git a/src/lint/lintFolder.spec.ts b/src/lint/lintFolder.spec.ts index 69938fd..9c2c2c9 100644 --- a/src/lint/lintFolder.spec.ts +++ b/src/lint/lintFolder.spec.ts @@ -1,6 +1,12 @@ import { lintFolder } from './lintFolder' import { Severity } from '../types/Severity' import path from 'path' +import { + createFile, + createFolder, + deleteFolder, + readFile +} from '@sasjs/utils/file' const expectedFilesCount = 1 const expectedDiagnostics = [ @@ -71,11 +77,18 @@ const expectedDiagnostics = [ describe('lintFolder', () => { it('should identify lint issues in a given folder', async () => { - const results = await lintFolder(path.join(__dirname, '..')) - + await createFolder(path.join(__dirname, 'lint-folder-test')) + const content = await readFile( + path.join(__dirname, '..', 'Example File.sas') + ) + await createFile( + path.join(__dirname, 'lint-folder-test', 'Example File.sas'), + content + ) + const results = await lintFolder(path.join(__dirname, 'lint-folder-test')) expect(results.size).toEqual(expectedFilesCount) const diagnostics = results.get( - path.join(__dirname, '..', 'Example File.sas') + path.join(__dirname, 'lint-folder-test', 'Example File.sas') )! expect(diagnostics.length).toEqual(expectedDiagnostics.length) expect(diagnostics).toContainEqual(expectedDiagnostics[0]) @@ -87,5 +100,36 @@ describe('lintFolder', () => { expect(diagnostics).toContainEqual(expectedDiagnostics[6]) expect(diagnostics).toContainEqual(expectedDiagnostics[7]) expect(diagnostics).toContainEqual(expectedDiagnostics[8]) + + await deleteFolder(path.join(__dirname, 'lint-folder-test')) + }) + + it('should identify lint issues in subfolders of a given folder', async () => { + await createFolder(path.join(__dirname, 'lint-folder-test')) + await createFolder(path.join(__dirname, 'lint-folder-test', 'subfolder')) + const content = await readFile( + path.join(__dirname, '..', 'Example File.sas') + ) + await createFile( + path.join(__dirname, 'lint-folder-test', 'subfolder', 'Example File.sas'), + content + ) + const results = await lintFolder(path.join(__dirname, 'lint-folder-test')) + expect(results.size).toEqual(expectedFilesCount) + const diagnostics = results.get( + path.join(__dirname, 'lint-folder-test', 'subfolder', 'Example File.sas') + )! + expect(diagnostics.length).toEqual(expectedDiagnostics.length) + expect(diagnostics).toContainEqual(expectedDiagnostics[0]) + expect(diagnostics).toContainEqual(expectedDiagnostics[1]) + expect(diagnostics).toContainEqual(expectedDiagnostics[2]) + expect(diagnostics).toContainEqual(expectedDiagnostics[3]) + expect(diagnostics).toContainEqual(expectedDiagnostics[4]) + expect(diagnostics).toContainEqual(expectedDiagnostics[5]) + expect(diagnostics).toContainEqual(expectedDiagnostics[6]) + expect(diagnostics).toContainEqual(expectedDiagnostics[7]) + expect(diagnostics).toContainEqual(expectedDiagnostics[8]) + + await deleteFolder(path.join(__dirname, 'lint-folder-test')) }) }) diff --git a/src/lint/lintProject.spec.ts b/src/lint/lintProject.spec.ts index ec7245f..31a3151 100644 --- a/src/lint/lintProject.spec.ts +++ b/src/lint/lintProject.spec.ts @@ -2,6 +2,8 @@ import { lintProject } from './lintProject' import { Severity } from '../types/Severity' import * as getProjectRootModule from '../utils/getProjectRoot' import path from 'path' +import { createFolder, createFile, readFile, deleteFolder } from '@sasjs/utils' +import { DefaultLintConfiguration } from '../utils' jest.mock('../utils/getProjectRoot') const expectedFilesCount = 1 @@ -73,14 +75,29 @@ const expectedDiagnostics = [ describe('lintProject', () => { it('should identify lint issues in a given project', async () => { + await createFolder(path.join(__dirname, 'lint-project-test')) + const content = await readFile( + path.join(__dirname, '..', 'Example File.sas') + ) + await createFile( + path.join(__dirname, 'lint-project-test', 'Example File.sas'), + content + ) + await createFile( + path.join(__dirname, 'lint-project-test', '.sasjslint'), + JSON.stringify(DefaultLintConfiguration) + ) + jest .spyOn(getProjectRootModule, 'getProjectRoot') - .mockImplementation(() => Promise.resolve(path.join(__dirname, '..'))) + .mockImplementation(() => + Promise.resolve(path.join(__dirname, 'lint-project-test')) + ) const results = await lintProject() expect(results.size).toEqual(expectedFilesCount) const diagnostics = results.get( - path.join(__dirname, '..', 'Example File.sas') + path.join(__dirname, 'lint-project-test', 'Example File.sas') )! expect(diagnostics.length).toEqual(expectedDiagnostics.length) expect(diagnostics).toContainEqual(expectedDiagnostics[0]) @@ -92,6 +109,8 @@ describe('lintProject', () => { expect(diagnostics).toContainEqual(expectedDiagnostics[6]) expect(diagnostics).toContainEqual(expectedDiagnostics[7]) expect(diagnostics).toContainEqual(expectedDiagnostics[8]) + + await deleteFolder(path.join(__dirname, 'lint-project-test')) }) it('should throw an error when a project root is not found', async () => {