mirror of
https://github.com/sasjs/lint.git
synced 2026-01-06 20:20:06 +00:00
feat(*): add line endings rule, add automatic formatting for fixable violations
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export * from './getLintConfig'
|
||||
export * from './getProjectRoot'
|
||||
export * from './listSasFiles'
|
||||
export * from './splitText'
|
||||
|
||||
95
src/utils/parseMacros.spec.ts
Normal file
95
src/utils/parseMacros.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { LintConfig } from '../types'
|
||||
import { parseMacros } from './parseMacros'
|
||||
|
||||
describe('parseMacros', () => {
|
||||
it('should return an array with a single macro', () => {
|
||||
const text = `%macro test;
|
||||
%put 'hello';
|
||||
%mend`
|
||||
|
||||
const macros = parseMacros(text, new LintConfig())
|
||||
|
||||
expect(macros.length).toEqual(1)
|
||||
expect(macros).toContainEqual({
|
||||
name: 'test',
|
||||
declaration: '%macro test;',
|
||||
termination: '%mend',
|
||||
startLineNumber: 1,
|
||||
endLineNumber: 3,
|
||||
parentMacro: '',
|
||||
hasMacroNameInMend: false,
|
||||
hasParentheses: false,
|
||||
mismatchedMendMacroName: ''
|
||||
})
|
||||
})
|
||||
|
||||
it('should return an array with multiple macros', () => {
|
||||
const text = `%macro foo;
|
||||
%put 'foo';
|
||||
%mend;
|
||||
%macro bar();
|
||||
%put 'bar';
|
||||
%mend bar;`
|
||||
|
||||
const macros = parseMacros(text, new LintConfig())
|
||||
|
||||
expect(macros.length).toEqual(2)
|
||||
expect(macros).toContainEqual({
|
||||
name: 'foo',
|
||||
declaration: '%macro foo;',
|
||||
termination: '%mend;',
|
||||
startLineNumber: 1,
|
||||
endLineNumber: 3,
|
||||
parentMacro: '',
|
||||
hasMacroNameInMend: false,
|
||||
hasParentheses: false,
|
||||
mismatchedMendMacroName: ''
|
||||
})
|
||||
expect(macros).toContainEqual({
|
||||
name: 'bar',
|
||||
declaration: '%macro bar();',
|
||||
termination: '%mend bar;',
|
||||
startLineNumber: 4,
|
||||
endLineNumber: 6,
|
||||
parentMacro: '',
|
||||
hasMacroNameInMend: true,
|
||||
hasParentheses: true,
|
||||
mismatchedMendMacroName: ''
|
||||
})
|
||||
})
|
||||
|
||||
it('should detect nested macro definitions', () => {
|
||||
const text = `%macro test()
|
||||
%put 'hello';
|
||||
%macro test2
|
||||
%put 'world;
|
||||
%mend
|
||||
%mend test`
|
||||
|
||||
const macros = parseMacros(text, new LintConfig())
|
||||
|
||||
expect(macros.length).toEqual(2)
|
||||
expect(macros).toContainEqual({
|
||||
name: 'test',
|
||||
declaration: '%macro test()',
|
||||
termination: '%mend test',
|
||||
startLineNumber: 1,
|
||||
endLineNumber: 6,
|
||||
parentMacro: '',
|
||||
hasMacroNameInMend: true,
|
||||
hasParentheses: true,
|
||||
mismatchedMendMacroName: ''
|
||||
})
|
||||
expect(macros).toContainEqual({
|
||||
name: 'test2',
|
||||
declaration: ' %macro test2',
|
||||
termination: ' %mend',
|
||||
startLineNumber: 3,
|
||||
endLineNumber: 5,
|
||||
parentMacro: 'test',
|
||||
hasMacroNameInMend: false,
|
||||
hasParentheses: false,
|
||||
mismatchedMendMacroName: ''
|
||||
})
|
||||
})
|
||||
})
|
||||
90
src/utils/parseMacros.ts
Normal file
90
src/utils/parseMacros.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { LintConfig } from '../types/LintConfig'
|
||||
import { LineEndings } from '../types/LineEndings'
|
||||
import { trimComments } from './trimComments'
|
||||
|
||||
interface Macro {
|
||||
name: string
|
||||
startLineNumber: number | null
|
||||
endLineNumber: number | null
|
||||
declaration: string
|
||||
termination: string
|
||||
parentMacro: string
|
||||
hasMacroNameInMend: boolean
|
||||
hasParentheses: boolean
|
||||
mismatchedMendMacroName: string
|
||||
}
|
||||
|
||||
export const parseMacros = (text: string, config?: LintConfig): Macro[] => {
|
||||
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
|
||||
const lines: string[] = text ? text.split(lineEnding) : []
|
||||
const macros: Macro[] = []
|
||||
|
||||
let isCommentStarted = false
|
||||
let macroStack: Macro[] = []
|
||||
lines.forEach((line, index) => {
|
||||
const { statement: trimmedLine, commentStarted } = trimComments(
|
||||
line,
|
||||
isCommentStarted
|
||||
)
|
||||
isCommentStarted = commentStarted
|
||||
const statements: string[] = trimmedLine ? trimmedLine.split(';') : []
|
||||
|
||||
statements.forEach((statement) => {
|
||||
const { statement: trimmedStatement, commentStarted } = trimComments(
|
||||
statement,
|
||||
isCommentStarted
|
||||
)
|
||||
isCommentStarted = commentStarted
|
||||
|
||||
if (trimmedStatement.startsWith('%macro')) {
|
||||
const startLineNumber = index + 1
|
||||
const name = trimmedStatement
|
||||
.slice(7, trimmedStatement.length)
|
||||
.trim()
|
||||
.split('(')[0]
|
||||
macroStack.push({
|
||||
name,
|
||||
startLineNumber,
|
||||
endLineNumber: null,
|
||||
parentMacro: macroStack.length
|
||||
? macroStack[macroStack.length - 1].name
|
||||
: '',
|
||||
hasParentheses: trimmedStatement.endsWith('()'),
|
||||
hasMacroNameInMend: false,
|
||||
mismatchedMendMacroName: '',
|
||||
declaration: line,
|
||||
termination: ''
|
||||
})
|
||||
} else if (trimmedStatement.startsWith('%mend')) {
|
||||
if (macroStack.length) {
|
||||
const macro = macroStack.pop() as Macro
|
||||
const mendMacroName =
|
||||
trimmedStatement.split(' ').filter((s: string) => !!s)[1] || ''
|
||||
macro.endLineNumber = index + 1
|
||||
macro.hasMacroNameInMend = trimmedStatement.includes(macro.name)
|
||||
macro.mismatchedMendMacroName = macro.hasMacroNameInMend
|
||||
? ''
|
||||
: mendMacroName
|
||||
macro.termination = line
|
||||
macros.push(macro)
|
||||
} else {
|
||||
macros.push({
|
||||
name: '',
|
||||
startLineNumber: null,
|
||||
endLineNumber: index + 1,
|
||||
parentMacro: '',
|
||||
hasParentheses: false,
|
||||
hasMacroNameInMend: false,
|
||||
mismatchedMendMacroName: '',
|
||||
declaration: '',
|
||||
termination: line
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
macros.push(...macroStack)
|
||||
|
||||
return macros
|
||||
}
|
||||
41
src/utils/splitText.spec.ts
Normal file
41
src/utils/splitText.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { LintConfig } from '../types'
|
||||
import { splitText } from './splitText'
|
||||
|
||||
describe('splitText', () => {
|
||||
const config = new LintConfig({
|
||||
noTrailingSpaces: true,
|
||||
noEncodedPasswords: true,
|
||||
hasDoxygenHeader: true,
|
||||
noSpacesInFileNames: true,
|
||||
maxLineLength: 80,
|
||||
lowerCaseFileNames: true,
|
||||
noTabIndentation: true,
|
||||
indentationMultiple: 2,
|
||||
hasMacroNameInMend: true,
|
||||
noNestedMacros: true,
|
||||
hasMacroParentheses: true,
|
||||
lineEndings: 'lf'
|
||||
})
|
||||
|
||||
it('should return an empty array when text is falsy', () => {
|
||||
const lines = splitText('', config)
|
||||
|
||||
expect(lines.length).toEqual(0)
|
||||
})
|
||||
|
||||
it('should return an array of lines from text', () => {
|
||||
const lines = splitText(`line 1\nline 2`, config)
|
||||
|
||||
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`, config)
|
||||
|
||||
expect(lines.length).toEqual(2)
|
||||
expect(lines[0]).toEqual('line 1')
|
||||
expect(lines[1]).toEqual('line 2')
|
||||
})
|
||||
})
|
||||
17
src/utils/splitText.ts
Normal file
17
src/utils/splitText.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { LintConfig } from '../types/LintConfig'
|
||||
import { LineEndings } from '../types/LineEndings'
|
||||
|
||||
/**
|
||||
* 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, config: LintConfig): string[] => {
|
||||
if (!text) return []
|
||||
const expectedLineEndings =
|
||||
config.lineEndings === LineEndings.LF ? '\n' : '\r\n'
|
||||
const incorrectLineEndings = expectedLineEndings === '\n' ? '\r\n' : '\n'
|
||||
return text
|
||||
.replace(new RegExp(incorrectLineEndings, 'g'), expectedLineEndings)
|
||||
.split(expectedLineEndings)
|
||||
}
|
||||
Reference in New Issue
Block a user