From c92630a8f9f77c4ae70b128c32fcf8ea4328c14a Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Wed, 24 Mar 2021 19:37:51 +0000 Subject: [PATCH] feat(path-lint): add support for linting file names, add lint config schema --- .sasjslint | 3 +- sasjslint-schema.json | 55 ++++++++++++++++++++++++++ src/example file.sas | 17 ++++++++ src/example.ts | 4 +- src/index.ts | 2 +- src/lint.spec.ts | 56 ++++++++++++++++++++++++--- src/lint.ts | 33 ++++++++++++++-- src/rules/noSpacesInFileNames.spec.ts | 27 +++++++++++++ src/rules/noSpacesInFileNames.ts | 34 ++++++++++++++++ src/types/LintConfig.ts | 8 +++- src/types/LintRule.ts | 10 ++++- src/types/LintRuleType.ts | 3 +- tsconfig.json | 3 +- 13 files changed, 237 insertions(+), 18 deletions(-) create mode 100644 sasjslint-schema.json create mode 100644 src/example file.sas create mode 100644 src/rules/noSpacesInFileNames.spec.ts create mode 100644 src/rules/noSpacesInFileNames.ts diff --git a/.sasjslint b/.sasjslint index 5b0d5b8..73e6a81 100644 --- a/.sasjslint +++ b/.sasjslint @@ -1,5 +1,6 @@ { "noTrailingSpaces": true, "noEncodedPasswords": true, - "hasDoxygenHeader": true + "hasDoxygenHeader": true, + "noSpacesInFileNames": true } \ No newline at end of file diff --git a/sasjslint-schema.json b/sasjslint-schema.json new file mode 100644 index 0000000..84b9821 --- /dev/null +++ b/sasjslint-schema.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/sasjs/lint/blob/main/sasjslint-schema.json", + "type": "object", + "title": "SASjs Lint Config File", + "description": "The SASjs Lint Config file provides the settings for customising SAS code style in your project.", + "default": { + "noTrailingSpaces": true, + "noEncodedPasswords": true, + "hasDoxygenHeader": true, + "noSpacesInFileNames": true + }, + "examples": [ + { + "noTrailingSpaces": true, + "noEncodedPasswords": true, + "hasDoxygenHeader": true, + "noSpacesInFileNames": true + } + ], + "properties": { + "noTrailingSpaces": { + "$id": "#/properties/noTrailingSpaces", + "type": "boolean", + "title": "noTrailingSpaces", + "description": "Enforces no trailing spaces in lines of SAS code. Shows a warning when they are present.", + "default": "true", + "examples": ["true", "false"] + }, + "noEncodedPasswords": { + "$id": "#/properties/noEncodedPasswords", + "type": "boolean", + "title": "noEncodedPasswords", + "description": "Enforces no encoded passwords such as {SAS001} or {SASENC} in lines of SAS code. Shows an error when they are present.", + "default": "true", + "examples": ["true", "false"] + }, + "hasDoxygenHeader": { + "$id": "#/properties/hasDoxygenHeader", + "type": "boolean", + "title": "hasDoxygenHeader", + "description": "Enforces the presence of a Doxygen header in the form of a comment block at the start of each SAS file. Shows a warning when one is absent.", + "default": "true", + "examples": ["true", "false"] + }, + "noSpacesInFileNames": { + "$id": "#/properties/noSpacesInFileNames", + "type": "boolean", + "title": "noSpacesInFileNames", + "description": "Enforces no spaces in file names. Shows a warning when they are present.", + "default": "true", + "examples": ["true", "false"] + } + } +} diff --git a/src/example file.sas b/src/example file.sas new file mode 100644 index 0000000..3ff3243 --- /dev/null +++ b/src/example file.sas @@ -0,0 +1,17 @@ + + + %macro mf_getuniquelibref(prefix=mclib,maxtries=1000); + %local x libref; + %let x={SAS002}; + %do x=0 %to &maxtries; + %if %sysfunc(libref(&prefix&x)) ne 0 %then %do; + %let libref=&prefix&x; + %let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work)))); + %if &rc %then %put %sysfunc(sysmsg()); + &prefix&x + %*put &sysmacroname: Libref &libref assigned as WORK and returned; + %return; + %end; + %end; + %put unable to find available libref in range &prefix.0-&maxtries; + %mend; \ No newline at end of file diff --git a/src/example.ts b/src/example.ts index 67ad5fc..22a9a14 100644 --- a/src/example.ts +++ b/src/example.ts @@ -1,4 +1,4 @@ -import { lint } from './lint' +import { lintText } from './lint' /** * Example which tests a piece of text with all known violations. @@ -46,4 +46,4 @@ const text = `/** %mend; ` -lint(text).then((diagnostics) => console.table(diagnostics)) +lintText(text).then((diagnostics) => console.table(diagnostics)) diff --git a/src/index.ts b/src/index.ts index 6af2390..b7c0c5c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ -export { lint } from './lint' +export { lintText, lintFile } from './lint' export * from './types' diff --git a/src/lint.spec.ts b/src/lint.spec.ts index 239184c..9b6f5e2 100644 --- a/src/lint.spec.ts +++ b/src/lint.spec.ts @@ -1,14 +1,15 @@ -import { lint, splitText } from './lint' +import { lintFile, lintText, splitText } from './lint' import { Severity } from './types/Severity' +import path from 'path' -describe('lint', () => { +describe('lintText', () => { it('should identify trailing spaces', async () => { const text = `/** @file **/ %put 'hello'; %put 'world'; ` - const results = await lint(text) + const results = await lintText(text) expect(results.length).toEqual(2) expect(results[0]).toEqual({ @@ -32,7 +33,7 @@ describe('lint', () => { @file **/ %put '{SAS001}';` - const results = await lint(text) + const results = await lintText(text) expect(results.length).toEqual(1) expect(results[0]).toEqual({ @@ -46,7 +47,7 @@ describe('lint', () => { it('should identify missing doxygen header', async () => { const text = `%put 'hello';` - const results = await lint(text) + const results = await lintText(text) expect(results.length).toEqual(1) expect(results[0]).toEqual({ @@ -62,12 +63,55 @@ describe('lint', () => { const text = `/** @file **/` - const results = await lint(text) + 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(5) + 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 missing Doxygen header', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'Line contains encoded password', + lineNumber: 5, + startColumnNumber: 11, + endColumnNumber: 19, + severity: Severity.Error + }) + }) +}) + describe('splitText', () => { it('should return an empty array when text is falsy', () => { const lines = splitText('') diff --git a/src/lint.ts b/src/lint.ts index bd6c9fa..5070def 100644 --- a/src/lint.ts +++ b/src/lint.ts @@ -1,3 +1,4 @@ +import { readFile } from '@sasjs/utils/file' import { Diagnostic } from './types/Diagnostic' import { LintConfig } from './types/LintConfig' import { getLintConfig } from './utils/getLintConfig' @@ -7,11 +8,26 @@ import { getLintConfig } from './utils/getLintConfig' * @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 lint = async (text: string) => { +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. + * @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() + const text = await readFile(filePath) + + const fileDiagnostics = processFile(filePath, config) + const textDiagnostics = processText(text, config) + + return [...fileDiagnostics, ...textDiagnostics] +} + /** * 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. @@ -25,7 +41,7 @@ export const splitText = (text: string): string[] => { const processText = (text: string, config: LintConfig) => { const lines = splitText(text) const diagnostics: Diagnostic[] = [] - diagnostics.push(...processFile(config, text)) + diagnostics.push(...processContent(config, text)) lines.forEach((line, index) => { diagnostics.push(...processLine(config, line, index + 1)) }) @@ -33,10 +49,10 @@ const processText = (text: string, config: LintConfig) => { return diagnostics } -const processFile = (config: LintConfig, fileContent: string): Diagnostic[] => { +const processContent = (config: LintConfig, content: string): Diagnostic[] => { const diagnostics: Diagnostic[] = [] config.fileLintRules.forEach((rule) => { - diagnostics.push(...rule.test(fileContent)) + diagnostics.push(...rule.test(content)) }) return diagnostics @@ -54,3 +70,12 @@ const processLine = ( 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/rules/noSpacesInFileNames.spec.ts b/src/rules/noSpacesInFileNames.spec.ts new file mode 100644 index 0000000..4b7962b --- /dev/null +++ b/src/rules/noSpacesInFileNames.spec.ts @@ -0,0 +1,27 @@ +import { Severity } from '../types/Severity' +import { noSpacesInFileNames } from './noSpacesInFileNames' + +describe('noSpacesInFileNames', () => { + it('should return an empty array when the file name has no spaces', () => { + const filePath = '/code/sas/my_sas_file.sas' + expect(noSpacesInFileNames.test(filePath)).toEqual([]) + }) + + it('should return an empty array when the file name has no spaces, even if the containing folder has spaces', () => { + const filePath = '/code/sas projects/my_sas_file.sas' + expect(noSpacesInFileNames.test(filePath)).toEqual([]) + }) + + it('should return an array with a single diagnostic when the file name has spaces', () => { + const filePath = '/code/sas/my sas file.sas' + expect(noSpacesInFileNames.test(filePath)).toEqual([ + { + message: 'File name contains spaces', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ]) + }) +}) diff --git a/src/rules/noSpacesInFileNames.ts b/src/rules/noSpacesInFileNames.ts new file mode 100644 index 0000000..cf9eb46 --- /dev/null +++ b/src/rules/noSpacesInFileNames.ts @@ -0,0 +1,34 @@ +import { PathLintRule } from '../types/LintRule' +import { LintRuleType } from '../types/LintRuleType' +import path from 'path' +import { Severity } from '../types/Severity' + +const name = 'noSpacesInFileNames' +const description = 'Enforce the absence of spaces within file names.' +const message = 'File name contains spaces' +const test = (value: string) => { + const fileName = path.basename(value) + if (fileName.includes(' ')) { + return [ + { + message, + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ] + } + return [] +} + +/** + * Lint rule that checks for the absence of spaces in a given file name. + */ +export const noSpacesInFileNames: PathLintRule = { + type: LintRuleType.Path, + name, + description, + message, + test +} diff --git a/src/types/LintConfig.ts b/src/types/LintConfig.ts index 99c1470..ab65991 100644 --- a/src/types/LintConfig.ts +++ b/src/types/LintConfig.ts @@ -1,7 +1,8 @@ import { hasDoxygenHeader } from '../rules/hasDoxygenHeader' import { noEncodedPasswords } from '../rules/noEncodedPasswords' +import { noSpacesInFileNames } from '../rules/noSpacesInFileNames' import { noTrailingSpaces } from '../rules/noTrailingSpaces' -import { FileLintRule, LineLintRule } from './LintRule' +import { FileLintRule, LineLintRule, PathLintRule } from './LintRule' /** * LintConfig is the logical representation of the .sasjslint file. @@ -13,6 +14,7 @@ import { FileLintRule, LineLintRule } from './LintRule' export class LintConfig { readonly lineLintRules: LineLintRule[] = [] readonly fileLintRules: FileLintRule[] = [] + readonly pathLintRules: PathLintRule[] = [] constructor(json?: any) { if (json?.noTrailingSpaces) { @@ -26,5 +28,9 @@ export class LintConfig { if (json?.hasDoxygenHeader) { this.fileLintRules.push(hasDoxygenHeader) } + + if (json?.noSpacesInFileNames) { + this.pathLintRules.push(noSpacesInFileNames) + } } } diff --git a/src/types/LintRule.ts b/src/types/LintRule.ts index 488fd0f..bb2bcb0 100644 --- a/src/types/LintRule.ts +++ b/src/types/LintRule.ts @@ -10,7 +10,6 @@ export interface LintRule { name: string description: string message: string - test: (value: string, lineNumber: number) => Diagnostic[] } /** @@ -18,6 +17,7 @@ export interface LintRule { */ export interface LineLintRule extends LintRule { type: LintRuleType.Line + test: (value: string, lineNumber: number) => Diagnostic[] } /** @@ -27,3 +27,11 @@ export interface FileLintRule extends LintRule { type: LintRuleType.File test: (value: string) => Diagnostic[] } + +/** + * A PathLintRule is run once per file. + */ +export interface PathLintRule extends LintRule { + type: LintRuleType.Path + test: (value: string) => Diagnostic[] +} diff --git a/src/types/LintRuleType.ts b/src/types/LintRuleType.ts index 370fc33..0d91075 100644 --- a/src/types/LintRuleType.ts +++ b/src/types/LintRuleType.ts @@ -4,5 +4,6 @@ */ export enum LintRuleType { Line, - File + File, + Path } diff --git a/tsconfig.json b/tsconfig.json index 7e9b97b..50fe783 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ ], "exclude": [ "node_modules", - "**/*.spec.ts" + "**/*.spec.ts", + "**/example.ts" ] } \ No newline at end of file