1
0
mirror of https://github.com/sasjs/lint.git synced 2025-12-10 17:34:36 +00:00

Merge pull request #9 from sasjs/path-linting

feat(path-lint): add support for linting file names, add lint config schema
This commit is contained in:
Krishna Acondy
2021-03-24 19:58:47 +00:00
committed by GitHub
13 changed files with 237 additions and 18 deletions

View File

@@ -1,5 +1,6 @@
{
"noTrailingSpaces": true,
"noEncodedPasswords": true,
"hasDoxygenHeader": true
"hasDoxygenHeader": true,
"noSpacesInFileNames": true
}

55
sasjslint-schema.json Normal file
View File

@@ -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"]
}
}
}

17
src/example file.sas Normal file
View File

@@ -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;

View File

@@ -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))

View File

@@ -1,2 +1,2 @@
export { lint } from './lint'
export { lintText, lintFile } from './lint'
export * from './types'

View File

@@ -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('')

View File

@@ -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
}

View File

@@ -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
}
])
})
})

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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[]
}

View File

@@ -4,5 +4,6 @@
*/
export enum LintRuleType {
Line,
File
File,
Path
}

View File

@@ -19,6 +19,7 @@
],
"exclude": [
"node_modules",
"**/*.spec.ts"
"**/*.spec.ts",
"**/example.ts"
]
}