diff --git a/sasjslint-schema.json b/sasjslint-schema.json index cd33536..d533a3b 100644 --- a/sasjslint-schema.json +++ b/sasjslint-schema.json @@ -14,6 +14,7 @@ "lowerCaseFileNames": true, "maxLineLength": 80, "maxHeaderLineLength": 80, + "maxDataLineLength": 80, "noGremlins": true, "noNestedMacros": true, "noSpacesInFileNames": true, @@ -32,6 +33,7 @@ "lowerCaseFileNames": true, "maxLineLength": 80, "maxHeaderLineLength": 80, + "maxDataLineLength": 80, "noGremlins": true, "allowedGremlins": ["0x0080", "0x3000"], "noTabs": true, @@ -137,6 +139,14 @@ "default": 80, "examples": [60, 80, 120] }, + "maxDataLineLength": { + "$id": "#/properties/maxDataLineLength", + "type": "number", + "title": "maxDataLineLength", + "description": "Enforces a configurable maximum line length for data section. Shows a warning for lines exceeding this length.", + "default": 80, + "examples": [60, 80, 120] + }, "noNestedMacros": { "$id": "#/properties/noNestedMacros", "type": "boolean", diff --git a/src/lint/shared.ts b/src/lint/shared.ts index db82940..7aeb40d 100644 --- a/src/lint/shared.ts +++ b/src/lint/shared.ts @@ -1,15 +1,18 @@ -import { LintConfig, Diagnostic } from '../types' +import { LintConfig, Diagnostic, LineLintRuleOptions } from '../types' import { getHeaderLinesCount, splitText } from '../utils' +import { checkIsDataLine, getDataSectionsDetail } from '../utils' export const processText = (text: string, config: LintConfig) => { const lines = splitText(text, config) const headerLinesCount = getHeaderLinesCount(text, config) + const dataSections = getDataSectionsDetail(text, config) const diagnostics: Diagnostic[] = [] diagnostics.push(...processContent(config, text)) lines.forEach((line, index) => { - index += 1 + const isHeaderLine = index + 1 <= headerLinesCount + const isDataLine = checkIsDataLine(dataSections, index) diagnostics.push( - ...processLine(config, line, index, index <= headerLinesCount) + ...processLine(config, line, index + 1, { isHeaderLine, isDataLine }) ) }) @@ -41,11 +44,11 @@ export const processLine = ( config: LintConfig, line: string, lineNumber: number, - isHeaderLine: boolean + options: LineLintRuleOptions ): Diagnostic[] => { const diagnostics: Diagnostic[] = [] config.lineLintRules.forEach((rule) => { - diagnostics.push(...rule.test(line, lineNumber, config, isHeaderLine)) + diagnostics.push(...rule.test(line, lineNumber, config, options)) }) return diagnostics diff --git a/src/rules/line/maxLineLength.spec.ts b/src/rules/line/maxLineLength.spec.ts index ad79d1c..548d0a7 100644 --- a/src/rules/line/maxLineLength.spec.ts +++ b/src/rules/line/maxLineLength.spec.ts @@ -41,4 +41,44 @@ describe('maxLineLength', () => { 'Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yard' expect(maxLineLength.test(line, 1)).toEqual([]) }) + + it('should return an array with a single diagnostic when the line in header section exceeds the specified length', () => { + const line = 'This line is from header section' + const config = new LintConfig({ + maxLineLength: 10, + maxHeaderLineLength: 15 + }) + expect(maxLineLength.test(line, 1, config, { isHeaderLine: true })).toEqual( + [ + { + message: `Line exceeds maximum length by ${ + line.length - config.maxHeaderLineLength + } characters`, + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ] + ) + }) + + it('should return an array with a single diagnostic when the line in data section exceeds the specified length', () => { + const line = 'GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8.' + const config = new LintConfig({ + maxLineLength: 10, + maxDataLineLength: 15 + }) + expect(maxLineLength.test(line, 1, config, { isDataLine: true })).toEqual([ + { + message: `Line exceeds maximum length by ${ + line.length - config.maxDataLineLength + } characters`, + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ]) + }) }) diff --git a/src/rules/line/maxLineLength.ts b/src/rules/line/maxLineLength.ts index 05ce2a9..4e2ad0d 100644 --- a/src/rules/line/maxLineLength.ts +++ b/src/rules/line/maxLineLength.ts @@ -1,5 +1,5 @@ import { LintConfig } from '../../types' -import { LineLintRule } from '../../types/LintRule' +import { LineLintRule, LineLintRuleOptions } from '../../types/LintRule' import { LintRuleType } from '../../types/LintRuleType' import { Severity } from '../../types/Severity' import { DefaultLintConfiguration } from '../../utils' @@ -12,15 +12,19 @@ const test = ( value: string, lineNumber: number, config?: LintConfig, - isHeaderLine?: boolean + options?: LineLintRuleOptions ) => { const severity = config?.severityLevel[name] || Severity.Warning - let maxLineLength = config - ? config.maxLineLength - : DefaultLintConfiguration.maxLineLength + let maxLineLength = DefaultLintConfiguration.maxLineLength - if (isHeaderLine && config) { - maxLineLength = Math.max(config.maxLineLength, config.maxHeaderLineLength) + if (config) { + if (options?.isHeaderLine) { + maxLineLength = Math.max(config.maxLineLength, config.maxHeaderLineLength) + } else if (options?.isDataLine) { + maxLineLength = Math.max(config.maxLineLength, config.maxDataLineLength) + } else { + maxLineLength = config.maxLineLength + } } if (value.length <= maxLineLength) return [] diff --git a/src/types/LintConfig.ts b/src/types/LintConfig.ts index 7a490f3..9a0800e 100644 --- a/src/types/LintConfig.ts +++ b/src/types/LintConfig.ts @@ -35,6 +35,7 @@ export class LintConfig { readonly pathLintRules: PathLintRule[] = [] readonly maxLineLength: number = 80 readonly maxHeaderLineLength: number = 80 + readonly maxDataLineLength: number = 80 readonly indentationMultiple: number = 2 readonly lineEndings: LineEndings = LineEndings.LF readonly defaultHeader: string = getDefaultHeader() @@ -75,6 +76,10 @@ export class LintConfig { if (!isNaN(json?.maxHeaderLineLength)) { this.maxHeaderLineLength = json.maxHeaderLineLength } + + if (!isNaN(json?.maxDataLineLength)) { + this.maxDataLineLength = json.maxDataLineLength + } } this.fileLintRules.push(lineEndings) diff --git a/src/types/LintRule.ts b/src/types/LintRule.ts index 27cd0a7..fe8ea0e 100644 --- a/src/types/LintRule.ts +++ b/src/types/LintRule.ts @@ -13,6 +13,11 @@ export interface LintRule { message: string } +export interface LineLintRuleOptions { + isHeaderLine?: boolean + isDataLine?: boolean +} + /** * A LineLintRule is run once per line of text. */ @@ -22,7 +27,7 @@ export interface LineLintRule extends LintRule { value: string, lineNumber: number, config?: LintConfig, - isHeaderLine?: boolean + options?: LineLintRuleOptions ) => Diagnostic[] fix?: (value: string, config?: LintConfig) => string } diff --git a/src/utils/getDataSectionDetail.spec.ts b/src/utils/getDataSectionDetail.spec.ts new file mode 100644 index 0000000..3a1e07d --- /dev/null +++ b/src/utils/getDataSectionDetail.spec.ts @@ -0,0 +1,113 @@ +import { LintConfig } from '../types' +import { getDataSectionsDetail, checkIsDataLine } from './getDataSectionsDetail' +import { DefaultLintConfiguration } from './getLintConfig' + +const datalines = `GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32. OPERATOR_NM:$10. RAW_VALUE:$4000. +AND,AND,1,LIBREF,CONTAINS,"'DC'" +AND,OR,2,DSN,=,"'MPE_LOCK_ANYTABLE'"` + +const datalinesBeginPattern1 = `datalines;` +const datalinesBeginPattern2 = `datalines4;` +const datalinesBeginPattern3 = `cards;` +const datalinesBeginPattern4 = `cards4;` +const datalinesBeginPattern5 = `parmcards;` +const datalinesBeginPattern6 = `parmcards4;` + +const datalinesEndPattern1 = `;` +const datalinesEndPattern2 = `;;;;` + +describe('getDataSectionsDetail', () => { + const config = new LintConfig(DefaultLintConfiguration) + it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern1}' and '${datalinesEndPattern1}' markers`, () => { + const text = `%put hello\n${datalinesBeginPattern1}\n${datalines}\n${datalinesEndPattern1}\n%put world;` + expect(getDataSectionsDetail(text, config)).toEqual([ + { + start: 1, + end: 5 + } + ]) + }) + + it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern2}' and '${datalinesEndPattern2}' markers`, () => { + const text = `%put hello\n${datalinesBeginPattern2}\n${datalines}\n${datalinesEndPattern2}\n%put world;` + expect(getDataSectionsDetail(text, config)).toEqual([ + { + start: 1, + end: 5 + } + ]) + }) + + it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern3}' and '${datalinesEndPattern1}' markers`, () => { + const text = `%put hello\n${datalinesBeginPattern3}\n${datalines}\n${datalinesEndPattern1}\n%put world;` + expect(getDataSectionsDetail(text, config)).toEqual([ + { + start: 1, + end: 5 + } + ]) + }) + + it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern4}' and '${datalinesEndPattern1}' markers`, () => { + const text = `%put hello\n${datalinesBeginPattern4}\n${datalines}\n${datalinesEndPattern1}\n%put world;` + expect(getDataSectionsDetail(text, config)).toEqual([ + { + start: 1, + end: 5 + } + ]) + }) + + it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern5}' and '${datalinesEndPattern1}' markers`, () => { + const text = `%put hello\n${datalinesBeginPattern5}\n${datalines}\n${datalinesEndPattern1}\n%put world;` + expect(getDataSectionsDetail(text, config)).toEqual([ + { + start: 1, + end: 5 + } + ]) + }) + + it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern6}' and '${datalinesEndPattern2}' markers`, () => { + const text = `%put hello\n${datalinesBeginPattern6}\n${datalines}\n${datalinesEndPattern2}\n%put world;` + expect(getDataSectionsDetail(text, config)).toEqual([ + { + start: 1, + end: 5 + } + ]) + }) +}) + +describe('checkIsDataLine', () => { + const config = new LintConfig(DefaultLintConfiguration) + it(`should return true if a line index is in a range of any data section`, () => { + const text = `%put hello\n${datalinesBeginPattern1}\n${datalines}\n${datalinesEndPattern1}\n%put world;` + expect( + checkIsDataLine( + [ + { + start: 1, + end: 5 + } + ], + 4 + ) + ).toBe(true) + }) + + it(`should return false if a line index is not in a range of any of data sections`, () => { + const text = `%put hello\n${datalinesBeginPattern1}\n${datalines}\n${datalinesEndPattern1}\n%put world;` + expect( + checkIsDataLine( + [ + { + start: 1, + end: 5 + } + ], + 8 + ) + ).toBe(false) + }) +}) diff --git a/src/utils/getDataSectionsDetail.ts b/src/utils/getDataSectionsDetail.ts new file mode 100644 index 0000000..f637a0b --- /dev/null +++ b/src/utils/getDataSectionsDetail.ts @@ -0,0 +1,56 @@ +import { LintConfig } from '../types' +import { splitText } from './splitText' + +interface DataSectionsDetail { + start: number + end: number +} + +export const getDataSectionsDetail = (text: string, config: LintConfig) => { + const dataSections: DataSectionsDetail[] = [] + const lines = splitText(text, config) + + const dataSectionStartRegex1 = new RegExp( + '^(datalines;)|(cards;)|(cards4;)|(parmcards;)' + ) + const dataSectionEndRegex1 = new RegExp(';') + const dataSectionStartRegex2 = new RegExp('^(datalines4)|(parmcards4);') + const dataSectionEndRegex2 = new RegExp(';;;;') + + let dataSectionStarted = false + let dataSectionStartIndex = -1 + let dataSectionEndRegex = dataSectionEndRegex1 + + lines.forEach((line, index) => { + if (dataSectionStarted) { + if (dataSectionEndRegex.test(line)) { + dataSections.push({ start: dataSectionStartIndex, end: index }) + dataSectionStarted = false + } + } else { + if (dataSectionStartRegex1.test(line)) { + dataSectionStarted = true + dataSectionStartIndex = index + dataSectionEndRegex = dataSectionEndRegex1 + } else if (dataSectionStartRegex2.test(line)) { + dataSectionStarted = true + dataSectionStartIndex = index + dataSectionEndRegex = dataSectionEndRegex2 + } + } + }) + + return dataSections +} + +export const checkIsDataLine = ( + dataSections: DataSectionsDetail[], + lineIndex: number +) => { + for (const dataSection of dataSections) { + if (lineIndex >= dataSection.start && lineIndex <= dataSection.end) + return true + } + + return false +} diff --git a/src/utils/getLintConfig.ts b/src/utils/getLintConfig.ts index f937d0b..ed000aa 100644 --- a/src/utils/getLintConfig.ts +++ b/src/utils/getLintConfig.ts @@ -17,6 +17,7 @@ export const DefaultLintConfiguration = { lowerCaseFileNames: true, maxLineLength: 80, maxHeaderLineLength: 80, + maxDataLineLength: 80, noTabIndentation: true, indentationMultiple: 2, hasMacroNameInMend: true, diff --git a/src/utils/index.ts b/src/utils/index.ts index 3fcf5af..05de2c7 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,3 +7,4 @@ export * from './listSasFiles' export * from './splitText' export * from './getIndicesOf' export * from './getHeaderLinesCount' +export * from './getDataSectionsDetail'