Skip to content

Commit

Permalink
Merge pull request #29 from sasjs/line-ending-formatting
Browse files Browse the repository at this point in the history
feat(*): add line endings rule, add automatic formatting for fixable violations
  • Loading branch information
krishna-acondy authored Apr 21, 2021
2 parents 99813f0 + 2687a8f commit 984915f
Show file tree
Hide file tree
Showing 42 changed files with 1,409 additions and 258 deletions.
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ module.exports = {
statements: -10
}
},
collectCoverageFrom: ['src/**/{!(index|example),}.ts']
collectCoverageFrom: ['src/**/{!(index|formatExample|lintExample),}.ts']
}
14 changes: 12 additions & 2 deletions sasjslint-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"noNestedMacros": true,
"noSpacesInFileNames": true,
"noTabIndentation": true,
"noTrailingSpaces": true
"noTrailingSpaces": true,
"lineEndings": "lf"
},
"examples": [
{
Expand All @@ -29,7 +30,8 @@
"indentationMultiple": 4,
"hasMacroNameInMend": true,
"noNestedMacros": true,
"hasMacroParentheses": true
"hasMacroParentheses": true,
"lineEndings": "crlf"
}
],
"properties": {
Expand Down Expand Up @@ -120,6 +122,14 @@
"description": "Enforces no trailing spaces in lines of SAS code. Shows a warning when they are present.",
"default": true,
"examples": [true, false]
},
"lineEndings": {
"$id": "#/properties/lineEndings",
"type": "string",
"title": "lineEndings",
"description": "Enforces the configured terminating character for each line. Shows a warning when incorrect line endings are present.",
"default": "lf",
"examples": ["lf", "crlf"]
}
}
}
1 change: 0 additions & 1 deletion src/format.ts

This file was deleted.

42 changes: 42 additions & 0 deletions src/format/formatFile.spec.ts
Original file line number Diff line number Diff line change
@@ -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 <Your brief here>\n <h4> SAS Macros </h4>\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;`
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 <Your brief here>\r\n <h4> SAS Macros </h4>\r\n**/\r\n%macro somemacro();\r\n%put 'hello';\r\n%mend somemacro;`
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'))
})
})
22 changes: 22 additions & 0 deletions src/format/formatFile.ts
Original file line number Diff line number Diff line change
@@ -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<void>} 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)
}
59 changes: 59 additions & 0 deletions src/format/formatFolder.spec.ts
Original file line number Diff line number Diff line change
@@ -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 <Your brief here>\n <h4> SAS Macros </h4>\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;`
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 <Your brief here>\n <h4> SAS Macros </h4>\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;`
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'))
})
})
42 changes: 42 additions & 0 deletions src/format/formatFolder.ts
Original file line number Diff line number Diff line change
@@ -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<void>} 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)
})
}
51 changes: 51 additions & 0 deletions src/format/formatProject.spec.ts
Original file line number Diff line number Diff line change
@@ -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 <Your brief here>\n <h4> SAS Macros </h4>\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;`
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.'
)
})
})
15 changes: 15 additions & 0 deletions src/format/formatProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getProjectRoot } from '../utils/getProjectRoot'
import { formatFolder } from './formatFolder'

/**
* Automatically formats all SAS files in the current project.
* @returns {Promise<void>} 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)
}
49 changes: 49 additions & 0 deletions src/format/formatText.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { formatText } from './formatText'
import * as getLintConfigModule from '../utils/getLintConfig'
import { LintConfig } from '../types'
jest.mock('../utils/getLintConfig')

describe('formatText', () => {
it('should format the given text based on configured rules', async () => {
jest
.spyOn(getLintConfigModule, 'getLintConfig')
.mockImplementationOnce(() =>
Promise.resolve(
new LintConfig(getLintConfigModule.DefaultLintConfiguration)
)
)
const text = `%macro test
%put 'hello';\r\n%mend; `

const expectedOutput = `/**
@file
@brief <Your brief here>
<h4> SAS Macros </h4>
**/\n%macro test
%put 'hello';\n%mend test;`

const output = await formatText(text)

expect(output).toEqual(expectedOutput)
})

it('should use CRLF line endings when configured', async () => {
jest
.spyOn(getLintConfigModule, 'getLintConfig')
.mockImplementationOnce(() =>
Promise.resolve(
new LintConfig({
...getLintConfigModule.DefaultLintConfiguration,
lineEndings: 'crlf'
})
)
)
const text = `%macro test\n %put 'hello';\r\n%mend; `

const expectedOutput = `/**\r\n @file\r\n @brief <Your brief here>\r\n <h4> SAS Macros </h4>\r\n**/\r\n%macro test\r\n %put 'hello';\r\n%mend test;`

const output = await formatText(text)

expect(output).toEqual(expectedOutput)
})
})
7 changes: 7 additions & 0 deletions src/format/formatText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { getLintConfig } from '../utils'
import { processText } from './shared'

export const formatText = async (text: string) => {
const config = await getLintConfig()
return processText(text, config)
}
4 changes: 4 additions & 0 deletions src/format/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './formatText'
export * from './formatFile'
export * from './formatFolder'
export * from './formatProject'
37 changes: 37 additions & 0 deletions src/format/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { LintConfig } from '../types'
import { LineEndings } from '../types/LineEndings'
import { splitText } from '../utils/splitText'

export const processText = (text: string, config: LintConfig) => {
const processedText = processContent(config, text)
const lines = splitText(processedText, config)
const formattedLines = lines.map((line) => {
return processLine(config, line)
})

const configuredLineEnding =
config.lineEndings === LineEndings.LF ? '\n' : '\r\n'
return formattedLines.join(configuredLineEnding)
}

const processContent = (config: LintConfig, content: string): string => {
let processedContent = content
config.fileLintRules
.filter((r) => !!r.fix)
.forEach((rule) => {
processedContent = rule.fix!(processedContent)
})

return processedContent
}

export const processLine = (config: LintConfig, line: string): string => {
let processedLine = line
config.lineLintRules
.filter((r) => !!r.fix)
.forEach((rule) => {
processedLine = rule.fix!(line)
})

return processedLine
}
Loading

0 comments on commit 984915f

Please sign in to comment.