From a8ca534b0b10c4895bc9aa5b675fb774bebe760d Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Tue, 30 Mar 2021 08:59:38 +0100 Subject: [PATCH 1/4] feat(lint): add lintFolder and lintProject APIs --- package-lock.json | 12 +++---- package.json | 2 +- src/lint.ts | 64 ++++++++++++++++++++++++++++++++++++-- src/utils/asyncForEach.ts | 8 +++++ src/utils/getLintConfig.ts | 1 + src/utils/index.ts | 1 + src/utils/listSasFiles.ts | 6 ++++ 7 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 src/utils/asyncForEach.ts create mode 100644 src/utils/listSasFiles.ts diff --git a/package-lock.json b/package-lock.json index d184b1d..6743da7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -648,9 +648,9 @@ } }, "@sasjs/utils": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.9.0.tgz", - "integrity": "sha512-j7ssEmb8OSZHUUL0PGVgoby0j0ClCcsLsydDCk/C4OAoWPAUPFI5HgGFPSEipz9+P8OlL/EBnglj4LGtlFHCpw==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.10.1.tgz", + "integrity": "sha512-T54jx6NEMLu2+R/ux4qcb3dDJ7nFrKkPCkmPXEfZxPQBkbq4C0kmaZv6dC63RDH68wYhoXR2S5fION5fFh91iw==", "requires": { "@types/prompts": "^2.0.9", "consola": "^2.15.0", @@ -778,9 +778,9 @@ "dev": true }, "@types/prompts": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.9.tgz", - "integrity": "sha512-TORZP+FSjTYMWwKadftmqEn6bziN5RnfygehByGsjxoK5ydnClddtv6GikGWPvCm24oI+YBwck5WDxIIyNxUrA==", + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.10.tgz", + "integrity": "sha512-W3PEl3l4vmxdgfY6LUG7ysh+mLJOTOFYmSpiLe6MCo1OdEm8b5s6ZJfuTQgEpYNwcMiiaRzJespPS5Py2tqLlQ==", "requires": { "@types/node": "*" } diff --git a/package.json b/package.json index 7b621a5..8a03b3b 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,6 @@ "typescript": "^4.2.3" }, "dependencies": { - "@sasjs/utils": "^2.9.0" + "@sasjs/utils": "^2.10.1" } } diff --git a/src/lint.ts b/src/lint.ts index 8aba8df..c7bb5c9 100644 --- a/src/lint.ts +++ b/src/lint.ts @@ -1,7 +1,20 @@ -import { readFile } from '@sasjs/utils/file' +import { readFile, listSubFoldersInFolder } from '@sasjs/utils/file' import { Diagnostic } from './types/Diagnostic' import { LintConfig } from './types/LintConfig' +import { asyncForEach } from './utils/asyncForEach' import { getLintConfig } from './utils/getLintConfig' +import { listSasFiles } from './utils/listSasFiles' +import path from 'path' +import { getProjectRoot } from './utils' + +const excludeFolders = [ + '.git', + '.github', + '.vscode', + 'node_modules', + 'sasjsbuild', + 'sasjsresults' +] /** * Analyses and produces a set of diagnostics for the given text content. @@ -16,10 +29,14 @@ export const lintText = async (text: string) => { /** * Analyses and produces a set of diagnostics for the file at the given path. * @param {string} filePath - the path to the file to be linted. + * @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file. * @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number. */ -export const lintFile = async (filePath: string) => { - const config = await getLintConfig() +export const lintFile = async ( + filePath: string, + configuration?: LintConfig +) => { + const config = configuration || (await getLintConfig()) const text = await readFile(filePath) const fileDiagnostics = processFile(filePath, config) @@ -28,6 +45,47 @@ export const lintFile = async (filePath: string) => { return [...fileDiagnostics, ...textDiagnostics] } +/** + * Analyses and produces a set of diagnostics for the folder at the given path. + * @param {string} folderPath - the path to the folder to be linted. + * @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file. + * @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number. + */ +export const lintFolder = async ( + folderPath: string, + configuration?: LintConfig +) => { + const config = configuration || (await getLintConfig()) + const diagnostics: Diagnostic[] = [] + const fileNames = await listSasFiles(folderPath) + await asyncForEach(fileNames, async (fileName) => { + diagnostics.push( + ...(await lintFile(path.join(folderPath, fileName), config)) + ) + }) + + const subFolders = (await listSubFoldersInFolder(folderPath)).filter( + (f: string) => !excludeFolders.includes(f) + ) + + await asyncForEach(subFolders, async (subFolder) => { + diagnostics.push( + ...(await lintFolder(path.join(folderPath, subFolder), config)) + ) + }) + + return diagnostics +} + +/** + * Analyses and produces a set of diagnostics for the current project. + * @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number. + */ +export const lintProject = async () => { + const projectRoot = await getProjectRoot() + return await lintFolder(projectRoot) +} + /** * Splits the given content into a list of lines, regardless of CRLF or LF line endings. * @param {string} text - the text content to be split into lines. diff --git a/src/utils/asyncForEach.ts b/src/utils/asyncForEach.ts new file mode 100644 index 0000000..744052b --- /dev/null +++ b/src/utils/asyncForEach.ts @@ -0,0 +1,8 @@ +export async function asyncForEach( + array: any[], + callback: (item: any, index: number, originalArray: any[]) => any +) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index, array) + } +} diff --git a/src/utils/getLintConfig.ts b/src/utils/getLintConfig.ts index 032fbf0..c90b15d 100644 --- a/src/utils/getLintConfig.ts +++ b/src/utils/getLintConfig.ts @@ -27,6 +27,7 @@ export async function getLintConfig(): Promise { const configuration = await readFile( path.join(projectRoot, '.sasjslint') ).catch((_) => { + console.warn('Unable to load .sasjslint file. Using default configuration.') return JSON.stringify(DefaultLintConfiguration) }) return new LintConfig(JSON.parse(configuration)) diff --git a/src/utils/index.ts b/src/utils/index.ts index 4bb8061..f48b820 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './getLintConfig' export * from './getProjectRoot' +export * from './listSasFiles' diff --git a/src/utils/listSasFiles.ts b/src/utils/listSasFiles.ts new file mode 100644 index 0000000..47899d6 --- /dev/null +++ b/src/utils/listSasFiles.ts @@ -0,0 +1,6 @@ +import { listFilesInFolder } from '@sasjs/utils/file' + +export const listSasFiles = async (folderPath: string): Promise => { + const files = await listFilesInFolder(folderPath) + return files.filter((f) => f.endsWith('.sas')) +} From c0d27fa25479d1e4d6bc726ca89163da32c7c3a4 Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Wed, 31 Mar 2021 08:32:42 +0100 Subject: [PATCH 2/4] chore(*): split lint module into smaller submodules, added tests --- src/lint.spec.ts | 158 ------------------------------- src/lint.ts | 139 --------------------------- src/lint/index.ts | 1 + src/lint/lintFile.spec.ts | 69 ++++++++++++++ src/lint/lintFile.ts | 23 +++++ src/lint/lintFolder.spec.ts | 67 +++++++++++++ src/lint/lintFolder.ts | 49 ++++++++++ src/lint/lintProject.spec.ts | 67 +++++++++++++ src/lint/lintProject.ts | 12 +++ src/lint/lintText.spec.ts | 69 ++++++++++++++ src/lint/lintText.ts | 12 +++ src/lint/shared.spec.ts | 25 +++++ src/lint/shared.ts | 56 +++++++++++ src/rules/indentationMultiple.ts | 2 +- src/types/Process.d.ts | 6 ++ src/utils/asyncForEach.spec.ts | 15 +++ 16 files changed, 472 insertions(+), 298 deletions(-) delete mode 100644 src/lint.spec.ts delete mode 100644 src/lint.ts create mode 100644 src/lint/index.ts create mode 100644 src/lint/lintFile.spec.ts create mode 100644 src/lint/lintFile.ts create mode 100644 src/lint/lintFolder.spec.ts create mode 100644 src/lint/lintFolder.ts create mode 100644 src/lint/lintProject.spec.ts create mode 100644 src/lint/lintProject.ts create mode 100644 src/lint/lintText.spec.ts create mode 100644 src/lint/lintText.ts create mode 100644 src/lint/shared.spec.ts create mode 100644 src/lint/shared.ts create mode 100644 src/types/Process.d.ts create mode 100644 src/utils/asyncForEach.spec.ts diff --git a/src/lint.spec.ts b/src/lint.spec.ts deleted file mode 100644 index d6d5e21..0000000 --- a/src/lint.spec.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { lintFile, lintText, splitText } from './lint' -import { Severity } from './types/Severity' -import path from 'path' - -describe('lintText', () => { - it('should identify trailing spaces', async () => { - const text = `/** - @file - **/ - %put 'hello'; - %put 'world'; ` - const results = await lintText(text) - - expect(results.length).toEqual(2) - expect(results[0]).toEqual({ - message: 'Line contains trailing spaces', - lineNumber: 4, - startColumnNumber: 18, - endColumnNumber: 18, - severity: Severity.Warning - }) - expect(results[1]).toEqual({ - message: 'Line contains trailing spaces', - lineNumber: 5, - startColumnNumber: 22, - endColumnNumber: 23, - severity: Severity.Warning - }) - }) - - it('should identify encoded passwords', async () => { - const text = `/** - @file - **/ - %put '{SAS001}';` - const results = await lintText(text) - - expect(results.length).toEqual(1) - expect(results[0]).toEqual({ - message: 'Line contains encoded password', - lineNumber: 4, - startColumnNumber: 11, - endColumnNumber: 19, - severity: Severity.Error - }) - }) - - it('should identify missing doxygen header', async () => { - const text = `%put 'hello';` - const results = await lintText(text) - - expect(results.length).toEqual(1) - expect(results[0]).toEqual({ - message: 'File missing Doxygen header', - lineNumber: 1, - startColumnNumber: 1, - endColumnNumber: 1, - severity: Severity.Warning - }) - }) - - it('should return an empty list with an empty file', async () => { - const text = `/** - @file - **/` - const results = await lintText(text) - - expect(results.length).toEqual(0) - }) -}) - -describe('lintFile', () => { - it('should identify lint issues in a given file', async () => { - const results = await lintFile(path.join(__dirname, 'Example File.sas')) - - expect(results.length).toEqual(8) - expect(results).toContainEqual({ - message: 'Line contains trailing spaces', - lineNumber: 1, - startColumnNumber: 1, - endColumnNumber: 2, - severity: Severity.Warning - }) - expect(results).toContainEqual({ - message: 'Line contains trailing spaces', - lineNumber: 2, - startColumnNumber: 1, - endColumnNumber: 2, - severity: Severity.Warning - }) - expect(results).toContainEqual({ - message: 'File name contains spaces', - lineNumber: 1, - startColumnNumber: 1, - endColumnNumber: 1, - severity: Severity.Warning - }) - expect(results).toContainEqual({ - message: 'File name contains uppercase characters', - lineNumber: 1, - startColumnNumber: 1, - endColumnNumber: 1, - severity: Severity.Warning - }) - expect(results).toContainEqual({ - message: 'File missing Doxygen header', - lineNumber: 1, - startColumnNumber: 1, - endColumnNumber: 1, - severity: Severity.Warning - }) - expect(results).toContainEqual({ - message: 'Line contains encoded password', - lineNumber: 5, - startColumnNumber: 10, - endColumnNumber: 18, - severity: Severity.Error - }) - expect(results).toContainEqual({ - message: 'Line is indented with a tab', - lineNumber: 7, - startColumnNumber: 1, - endColumnNumber: 1, - severity: Severity.Warning - }) - expect(results).toContainEqual({ - message: 'Line has incorrect indentation - 3 spaces', - lineNumber: 6, - startColumnNumber: 1, - endColumnNumber: 1, - severity: Severity.Warning - }) - }) -}) - -describe('splitText', () => { - it('should return an empty array when text is falsy', () => { - const lines = splitText('') - - expect(lines.length).toEqual(0) - }) - - it('should return an array of lines from text', () => { - const lines = splitText(`line 1\nline 2`) - - expect(lines.length).toEqual(2) - expect(lines[0]).toEqual('line 1') - expect(lines[1]).toEqual('line 2') - }) - - it('should work with CRLF line endings', () => { - const lines = splitText(`line 1\r\nline 2`) - - expect(lines.length).toEqual(2) - expect(lines[0]).toEqual('line 1') - expect(lines[1]).toEqual('line 2') - }) -}) diff --git a/src/lint.ts b/src/lint.ts deleted file mode 100644 index c7bb5c9..0000000 --- a/src/lint.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { readFile, listSubFoldersInFolder } from '@sasjs/utils/file' -import { Diagnostic } from './types/Diagnostic' -import { LintConfig } from './types/LintConfig' -import { asyncForEach } from './utils/asyncForEach' -import { getLintConfig } from './utils/getLintConfig' -import { listSasFiles } from './utils/listSasFiles' -import path from 'path' -import { getProjectRoot } from './utils' - -const excludeFolders = [ - '.git', - '.github', - '.vscode', - 'node_modules', - 'sasjsbuild', - 'sasjsresults' -] - -/** - * Analyses and produces a set of diagnostics for the given text content. - * @param {string} text - the text content to be linted. - * @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number. - */ -export const lintText = async (text: string) => { - const config = await getLintConfig() - return processText(text, config) -} - -/** - * Analyses and produces a set of diagnostics for the file at the given path. - * @param {string} filePath - the path to the file to be linted. - * @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file. - * @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number. - */ -export const lintFile = async ( - filePath: string, - configuration?: LintConfig -) => { - const config = configuration || (await getLintConfig()) - const text = await readFile(filePath) - - const fileDiagnostics = processFile(filePath, config) - const textDiagnostics = processText(text, config) - - return [...fileDiagnostics, ...textDiagnostics] -} - -/** - * Analyses and produces a set of diagnostics for the folder at the given path. - * @param {string} folderPath - the path to the folder to be linted. - * @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file. - * @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number. - */ -export const lintFolder = async ( - folderPath: string, - configuration?: LintConfig -) => { - const config = configuration || (await getLintConfig()) - const diagnostics: Diagnostic[] = [] - const fileNames = await listSasFiles(folderPath) - await asyncForEach(fileNames, async (fileName) => { - diagnostics.push( - ...(await lintFile(path.join(folderPath, fileName), config)) - ) - }) - - const subFolders = (await listSubFoldersInFolder(folderPath)).filter( - (f: string) => !excludeFolders.includes(f) - ) - - await asyncForEach(subFolders, async (subFolder) => { - diagnostics.push( - ...(await lintFolder(path.join(folderPath, subFolder), config)) - ) - }) - - return diagnostics -} - -/** - * Analyses and produces a set of diagnostics for the current project. - * @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number. - */ -export const lintProject = async () => { - const projectRoot = await getProjectRoot() - return await lintFolder(projectRoot) -} - -/** - * Splits the given content into a list of lines, regardless of CRLF or LF line endings. - * @param {string} text - the text content to be split into lines. - * @returns {string[]} an array of lines from the given text - */ -export const splitText = (text: string): string[] => { - if (!text) return [] - return text.replace(/\r\n/g, '\n').split('\n') -} - -const processText = (text: string, config: LintConfig) => { - const lines = splitText(text) - const diagnostics: Diagnostic[] = [] - diagnostics.push(...processContent(config, text)) - lines.forEach((line, index) => { - diagnostics.push(...processLine(config, line, index + 1)) - }) - - return diagnostics -} - -const processContent = (config: LintConfig, content: string): Diagnostic[] => { - const diagnostics: Diagnostic[] = [] - config.fileLintRules.forEach((rule) => { - diagnostics.push(...rule.test(content)) - }) - - return diagnostics -} - -const processLine = ( - config: LintConfig, - line: string, - lineNumber: number -): Diagnostic[] => { - const diagnostics: Diagnostic[] = [] - config.lineLintRules.forEach((rule) => { - diagnostics.push(...rule.test(line, lineNumber, config)) - }) - - return diagnostics -} - -const processFile = (filePath: string, config: LintConfig): Diagnostic[] => { - const diagnostics: Diagnostic[] = [] - config.pathLintRules.forEach((rule) => { - diagnostics.push(...rule.test(filePath)) - }) - - return diagnostics -} diff --git a/src/lint/index.ts b/src/lint/index.ts new file mode 100644 index 0000000..fd74629 --- /dev/null +++ b/src/lint/index.ts @@ -0,0 +1 @@ +export { lintText, lintFile, lintFolder, lintProject } from './lint' diff --git a/src/lint/lintFile.spec.ts b/src/lint/lintFile.spec.ts new file mode 100644 index 0000000..a7de9ef --- /dev/null +++ b/src/lint/lintFile.spec.ts @@ -0,0 +1,69 @@ +import { lintFile } from './lintFile' +import { Severity } from '../types/Severity' +import path from 'path' + +describe('lintFile', () => { + it('should identify lint issues in a given file', async () => { + const results = await lintFile( + path.join(__dirname, '..', 'Example File.sas') + ) + + expect(results.length).toEqual(8) + expect(results).toContainEqual({ + message: 'Line contains trailing spaces', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 2, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'Line contains trailing spaces', + lineNumber: 2, + startColumnNumber: 1, + endColumnNumber: 2, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'File name contains spaces', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'File name contains uppercase characters', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'File missing Doxygen header', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'Line contains encoded password', + lineNumber: 5, + startColumnNumber: 10, + endColumnNumber: 18, + severity: Severity.Error + }) + expect(results).toContainEqual({ + message: 'Line is indented with a tab', + lineNumber: 7, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'Line has incorrect indentation - 3 spaces', + lineNumber: 6, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + }) +}) diff --git a/src/lint/lintFile.ts b/src/lint/lintFile.ts new file mode 100644 index 0000000..8d1bc6e --- /dev/null +++ b/src/lint/lintFile.ts @@ -0,0 +1,23 @@ +import { readFile } from '@sasjs/utils/file' +import { LintConfig } from '../types/LintConfig' +import { getLintConfig } from '../utils/getLintConfig' +import { processFile, processText } from './shared' + +/** + * Analyses and produces a set of diagnostics for the file at the given path. + * @param {string} filePath - the path to the file to be linted. + * @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file. + * @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number. + */ +export const lintFile = async ( + filePath: string, + configuration?: LintConfig +) => { + const config = configuration || (await getLintConfig()) + const text = await readFile(filePath) + + const fileDiagnostics = processFile(filePath, config) + const textDiagnostics = processText(text, config) + + return [...fileDiagnostics, ...textDiagnostics] +} diff --git a/src/lint/lintFolder.spec.ts b/src/lint/lintFolder.spec.ts new file mode 100644 index 0000000..f33ee7d --- /dev/null +++ b/src/lint/lintFolder.spec.ts @@ -0,0 +1,67 @@ +import { lintFolder } from './lintFolder' +import { Severity } from '../types/Severity' +import path from 'path' + +describe('lintFolder', () => { + it('should identify lint issues in a given folder', async () => { + const results = await lintFolder(path.join(__dirname, '..')) + + expect(results.length).toEqual(8) + expect(results).toContainEqual({ + message: 'Line contains trailing spaces', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 2, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'Line contains trailing spaces', + lineNumber: 2, + startColumnNumber: 1, + endColumnNumber: 2, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'File name contains spaces', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'File name contains uppercase characters', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'File missing Doxygen header', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'Line contains encoded password', + lineNumber: 5, + startColumnNumber: 10, + endColumnNumber: 18, + severity: Severity.Error + }) + expect(results).toContainEqual({ + message: 'Line is indented with a tab', + lineNumber: 7, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'Line has incorrect indentation - 3 spaces', + lineNumber: 6, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + }) +}) diff --git a/src/lint/lintFolder.ts b/src/lint/lintFolder.ts new file mode 100644 index 0000000..7a2c164 --- /dev/null +++ b/src/lint/lintFolder.ts @@ -0,0 +1,49 @@ +import { listSubFoldersInFolder } from '@sasjs/utils/file' +import path from 'path' +import { Diagnostic } from '../types/Diagnostic' +import { LintConfig } from '../types/LintConfig' +import { asyncForEach } from '../utils/asyncForEach' +import { getLintConfig } from '../utils/getLintConfig' +import { listSasFiles } from '../utils/listSasFiles' +import { lintFile } from './lintFile' + +const excludeFolders = [ + '.git', + '.github', + '.vscode', + 'node_modules', + 'sasjsbuild', + 'sasjsresults' +] + +/** + * Analyses and produces a set of diagnostics for the folder at the given path. + * @param {string} folderPath - the path to the folder to be linted. + * @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file. + * @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number. + */ +export const lintFolder = async ( + folderPath: string, + configuration?: LintConfig +) => { + const config = configuration || (await getLintConfig()) + const diagnostics: Diagnostic[] = [] + const fileNames = await listSasFiles(folderPath) + await asyncForEach(fileNames, async (fileName) => { + diagnostics.push( + ...(await lintFile(path.join(folderPath, fileName), config)) + ) + }) + + const subFolders = (await listSubFoldersInFolder(folderPath)).filter( + (f: string) => !excludeFolders.includes(f) + ) + + await asyncForEach(subFolders, async (subFolder) => { + diagnostics.push( + ...(await lintFolder(path.join(folderPath, subFolder), config)) + ) + }) + + return diagnostics +} diff --git a/src/lint/lintProject.spec.ts b/src/lint/lintProject.spec.ts new file mode 100644 index 0000000..635bf17 --- /dev/null +++ b/src/lint/lintProject.spec.ts @@ -0,0 +1,67 @@ +import { lintProject } from './lintProject' +import { Severity } from '../types/Severity' +import path from 'path' + +describe('lintProject', () => { + it('should identify lint issues in a given project', async () => { + const results = await lintProject() + + expect(results.length).toEqual(8) + expect(results).toContainEqual({ + message: 'Line contains trailing spaces', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 2, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'Line contains trailing spaces', + lineNumber: 2, + startColumnNumber: 1, + endColumnNumber: 2, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'File name contains spaces', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'File name contains uppercase characters', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'File missing Doxygen header', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'Line contains encoded password', + lineNumber: 5, + startColumnNumber: 10, + endColumnNumber: 18, + severity: Severity.Error + }) + expect(results).toContainEqual({ + message: 'Line is indented with a tab', + lineNumber: 7, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'Line has incorrect indentation - 3 spaces', + lineNumber: 6, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + }) +}) diff --git a/src/lint/lintProject.ts b/src/lint/lintProject.ts new file mode 100644 index 0000000..d858a6b --- /dev/null +++ b/src/lint/lintProject.ts @@ -0,0 +1,12 @@ +import { getProjectRoot } from '../utils' +import { lintFolder } from './lintFolder' + +/** + * Analyses and produces a set of diagnostics for the current project. + * @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number. + */ +export const lintProject = async () => { + const projectRoot = + (await getProjectRoot()) || process.projectDir || process.currentDir + return await lintFolder(projectRoot) +} diff --git a/src/lint/lintText.spec.ts b/src/lint/lintText.spec.ts new file mode 100644 index 0000000..05de991 --- /dev/null +++ b/src/lint/lintText.spec.ts @@ -0,0 +1,69 @@ +import { lintText } from './lintText' +import { Severity } from '../types/Severity' + +describe('lintText', () => { + it('should identify trailing spaces', async () => { + const text = `/** + @file + **/ + %put 'hello'; + %put 'world'; ` + const results = await lintText(text) + + expect(results.length).toEqual(2) + expect(results[0]).toEqual({ + message: 'Line contains trailing spaces', + lineNumber: 4, + startColumnNumber: 18, + endColumnNumber: 18, + severity: Severity.Warning + }) + expect(results[1]).toEqual({ + message: 'Line contains trailing spaces', + lineNumber: 5, + startColumnNumber: 22, + endColumnNumber: 23, + severity: Severity.Warning + }) + }) + + it('should identify encoded passwords', async () => { + const text = `/** + @file + **/ + %put '{SAS001}';` + const results = await lintText(text) + + expect(results.length).toEqual(1) + expect(results[0]).toEqual({ + message: 'Line contains encoded password', + lineNumber: 4, + startColumnNumber: 11, + endColumnNumber: 19, + severity: Severity.Error + }) + }) + + it('should identify missing doxygen header', async () => { + const text = `%put 'hello';` + const results = await lintText(text) + + expect(results.length).toEqual(1) + expect(results[0]).toEqual({ + message: 'File missing Doxygen header', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + }) + + it('should return an empty list with an empty file', async () => { + const text = `/** + @file + **/` + const results = await lintText(text) + + expect(results.length).toEqual(0) + }) +}) diff --git a/src/lint/lintText.ts b/src/lint/lintText.ts new file mode 100644 index 0000000..f3b1b78 --- /dev/null +++ b/src/lint/lintText.ts @@ -0,0 +1,12 @@ +import { getLintConfig } from '../utils/getLintConfig' +import { processText } from './shared' + +/** + * Analyses and produces a set of diagnostics for the given text content. + * @param {string} text - the text content to be linted. + * @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number. + */ +export const lintText = async (text: string) => { + const config = await getLintConfig() + return processText(text, config) +} diff --git a/src/lint/shared.spec.ts b/src/lint/shared.spec.ts new file mode 100644 index 0000000..668610b --- /dev/null +++ b/src/lint/shared.spec.ts @@ -0,0 +1,25 @@ +import { splitText } from './shared' + +describe('splitText', () => { + it('should return an empty array when text is falsy', () => { + const lines = splitText('') + + expect(lines.length).toEqual(0) + }) + + it('should return an array of lines from text', () => { + const lines = splitText(`line 1\nline 2`) + + expect(lines.length).toEqual(2) + expect(lines[0]).toEqual('line 1') + expect(lines[1]).toEqual('line 2') + }) + + it('should work with CRLF line endings', () => { + const lines = splitText(`line 1\r\nline 2`) + + expect(lines.length).toEqual(2) + expect(lines[0]).toEqual('line 1') + expect(lines[1]).toEqual('line 2') + }) +}) diff --git a/src/lint/shared.ts b/src/lint/shared.ts new file mode 100644 index 0000000..bbbadc6 --- /dev/null +++ b/src/lint/shared.ts @@ -0,0 +1,56 @@ +import { LintConfig, Diagnostic } from '../types' + +/** + * Splits the given content into a list of lines, regardless of CRLF or LF line endings. + * @param {string} text - the text content to be split into lines. + * @returns {string[]} an array of lines from the given text + */ +export const splitText = (text: string): string[] => { + if (!text) return [] + return text.replace(/\r\n/g, '\n').split('\n') +} + +export const processText = (text: string, config: LintConfig) => { + const lines = splitText(text) + const diagnostics: Diagnostic[] = [] + diagnostics.push(...processContent(config, text)) + lines.forEach((line, index) => { + diagnostics.push(...processLine(config, line, index + 1)) + }) + + return diagnostics +} + +export const processFile = ( + filePath: string, + config: LintConfig +): Diagnostic[] => { + const diagnostics: Diagnostic[] = [] + config.pathLintRules.forEach((rule) => { + diagnostics.push(...rule.test(filePath)) + }) + + return diagnostics +} + +const processContent = (config: LintConfig, content: string): Diagnostic[] => { + const diagnostics: Diagnostic[] = [] + config.fileLintRules.forEach((rule) => { + diagnostics.push(...rule.test(content)) + }) + + return diagnostics +} + +export const processLine = ( + config: LintConfig, + line: string, + lineNumber: number +): Diagnostic[] => { + const diagnostics: Diagnostic[] = [] + config.lineLintRules.forEach((rule) => { + diagnostics.push(...rule.test(line, lineNumber, config)) + }) + + return diagnostics +} diff --git a/src/rules/indentationMultiple.ts b/src/rules/indentationMultiple.ts index b0a3874..0517ddf 100644 --- a/src/rules/indentationMultiple.ts +++ b/src/rules/indentationMultiple.ts @@ -11,7 +11,7 @@ const test = (value: string, lineNumber: number, config?: LintConfig) => { const indentationMultiple = isNaN(config?.indentationMultiple as number) ? 2 - : config?.indentationMultiple + : config!.indentationMultiple if (indentationMultiple === 0) return [] const numberOfSpaces = value.search(/\S|$/) diff --git a/src/types/Process.d.ts b/src/types/Process.d.ts new file mode 100644 index 0000000..67bd790 --- /dev/null +++ b/src/types/Process.d.ts @@ -0,0 +1,6 @@ +declare namespace NodeJS { + export interface Process { + projectDir: string + currentDir: string + } +} diff --git a/src/utils/asyncForEach.spec.ts b/src/utils/asyncForEach.spec.ts new file mode 100644 index 0000000..fe27cdd --- /dev/null +++ b/src/utils/asyncForEach.spec.ts @@ -0,0 +1,15 @@ +import { asyncForEach } from './asyncForEach' + +describe('asyncForEach', () => { + it('should execute the async callback for each item in the given array', async () => { + const callback = jest.fn().mockImplementation(() => Promise.resolve()) + const array = [1, 2, 3] + + await asyncForEach(array, callback) + + expect(callback.mock.calls.length).toEqual(3) + expect(callback.mock.calls[0]).toEqual([1, 0, array]) + expect(callback.mock.calls[1]).toEqual([2, 1, array]) + expect(callback.mock.calls[2]).toEqual([3, 2, array]) + }) +}) From 28d5e7121a2a5d98ad89fdfce1d991a5c032a87d Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Wed, 31 Mar 2021 08:34:02 +0100 Subject: [PATCH 3/4] chore(*): throw error when project root is not found --- src/lint/lintProject.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lint/lintProject.ts b/src/lint/lintProject.ts index d858a6b..9712ee9 100644 --- a/src/lint/lintProject.ts +++ b/src/lint/lintProject.ts @@ -8,5 +8,9 @@ import { lintFolder } from './lintFolder' export const lintProject = async () => { const projectRoot = (await getProjectRoot()) || process.projectDir || process.currentDir + + if (!projectRoot) { + throw new Error('SASjs Project Root was not found.') + } return await lintFolder(projectRoot) } From 86a6d36693bb1b42ed0079f82acd3d7a2f5b5b39 Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Wed, 31 Mar 2021 08:36:04 +0100 Subject: [PATCH 4/4] chore(*): fix exports --- src/index.ts | 2 +- src/lint/index.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index a45d40e..7ed1b17 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ -export { lintText, lintFile } from './lint' +export * from './lint' export * from './types' export * from './utils' diff --git a/src/lint/index.ts b/src/lint/index.ts index fd74629..bf4ca25 100644 --- a/src/lint/index.ts +++ b/src/lint/index.ts @@ -1 +1,4 @@ -export { lintText, lintFile, lintFolder, lintProject } from './lint' +export * from './lintText' +export * from './lintFile' +export * from './lintFolder' +export * from './lintProject'