Skip to content

Commit

Permalink
Merge pull request #1357 from sasjs/snippet
Browse files Browse the repository at this point in the history
Snippets generation feature
  • Loading branch information
YuryShkoda authored Aug 7, 2023
2 parents adcdffe + b405852 commit 643fe8e
Show file tree
Hide file tree
Showing 19 changed files with 609 additions and 13 deletions.
4 changes: 4 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ What code changes have been made to achieve the intent.
- [ ] Development comments have been added or updated.
- [ ] Development documentation coverage has been increased and a new threshold is set.
- [ ] Reviewer is assigned.

### Reviewer checks

- [ ] Any new code is documented.
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
statements: 73.16,
branches: 60.2,
functions: 73.23,
lines: 73.23
statements: 73.44,
branches: 60.45,
functions: 73.56,
lines: 74.09
}
},

Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"@sasjs/adapter": "4.8.0",
"@sasjs/core": "4.46.3",
"@sasjs/lint": "2.3.1",
"@sasjs/utils": "3.3.0",
"@sasjs/utils": "3.4.0",
"adm-zip": "0.5.9",
"chalk": "4.1.2",
"dotenv": "16.0.3",
Expand Down
1 change: 0 additions & 1 deletion src/commands/docs/spec/docsCommand.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,6 @@ const setupMocks = () => {
jest
.spyOn(configUtils, 'findTargetInConfiguration')
.mockImplementation(() => Promise.resolve({ target, isLocal: true }))
jest.spyOn(process.logger, 'error')
jest
.spyOn(configUtils, 'getLocalConfig')
.mockImplementation(() => Promise.resolve(config))
Expand Down
11 changes: 11 additions & 0 deletions src/commands/help/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,17 @@ export async function printHelpText() {
`[2spaces]NOTE: Providing outDirectory flag is optional. If not present, CLI will use save outputs into sasjsresults folder.`,
`[2spaces]NOTE: Providing force (--force or -f) flag is optional. If present, CLI will force command to finish running all tests and will not return an error code even when some are failing. Useful when the requirement is not to make CI Pipeline fail.`
]
},
{
name: 'snippets',
title: 'snippets',
description: [
`Generates VS Code snippets from the Doxygen headers in the SAS Macros.`,
`[2spaces]command example: sasjs snippets --outDirectory <folderPath> --target <targetName>`,
``,
`[2spaces]NOTE: Providing <folderPath> is optional. If not present, generated snippets will be saved to 'sasjsresults/sasjs-macro-snippets.json' file.`,
`[2spaces]NOTE: more information can be found here https://cli.sasjs.io/snippets/.`
]
}
]

Expand Down
118 changes: 118 additions & 0 deletions src/commands/snippets/snippets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {
listSasFilesInFolder,
asyncForEach,
readFile,
createFile,
isTestFile,
getLineEnding
} from '@sasjs/utils'
import path from 'path'

interface Snippet {
prefix: string
body: string
description: string[]
}

const sasRegExp = /\.sas$/ // to check if file has .sas extension
const briefRegExp = /^\s\s@brief\s/ // to get lines that has @brief keyword
const paramRegExp = /^\s\s@param\s/ // to get lines that has @param keyword

/**
* Generates VS Code snippets from the Doxygen headers in the SAS Macros.
* @param macroFolders an array of file paths of SAS Macros.
* @param outDirectory a folder path where generated snippets should be put into. If ends with '<file name>.json' then provided file name will be used, otherwise default file name ('sasjs-macro-snippets.json') will be used.
* @returns a promise that resolves into a file path with generated VS Code snippets.
*/
export async function generateSnippets(
macroFolders: string[],
outDirectory?: string
) {
// an object that represents generated snippets
const snippets: { [key: string]: Snippet } = {}

// return an error if no macro folders has been provided
if (!macroFolders.length) {
return Promise.reject(
`"macroFolders" array was not found in sasjs/sasjsconfig.json.`
)
}

// generate snippets from all .sas file in macro folders
await asyncForEach(macroFolders, async (folder) => {
// get .sas files excluding test('*.test.sas') files
const sasFiles = (await listSasFilesInFolder(folder, true))
.map((file) => path.join(folder, file))
.filter((file) => !isTestFile(file))

await asyncForEach(sasFiles, async (file) => {
// get macro name
const macro: string = file.split(path.sep).pop().replace(sasRegExp, '')

// put generated snippet into snippets object
snippets[macro] = await createSnippetFromMacro(file)
})
})

// return an error if no snippets has been generated
if (!Object.keys(snippets).length) {
return Promise.reject('No VS Code snippets has been found.')
}

const defaultOutputFileName = 'sasjs-macro-snippets.json'
const { buildDestinationResultsFolder } = process.sasjsConstants

// if outDirectory is provided, use it depending if it has file name or a folder only. If outDirectory is not provided, default file name and build result folder should be used.
const snippetsFilePath = path.join(
outDirectory ? process.projectDir : buildDestinationResultsFolder,
outDirectory
? /\.json$/.test(outDirectory)
? outDirectory
: path.join(outDirectory, defaultOutputFileName)
: defaultOutputFileName
)

// create file with generated VS Code snippets
await createFile(snippetsFilePath, JSON.stringify(snippets, null, 2))

// return file path with generated VS Code snippets
return Promise.resolve(snippetsFilePath)
}

/**
* Creates a VS Code snippet from SAS macro.
* @param file file path of SAS macro.
* @returns promise that resolves with VS Code snippet.
*/
const createSnippetFromMacro = async (file: string): Promise<Snippet> => {
const fileContent = await readFile(file)
const lineEnding = getLineEnding(fileContent) // LF or CRLF
const lines = fileContent.split(lineEnding)

let brief = lines.filter((line) => briefRegExp.test(line))
let params = lines.filter((line) => paramRegExp.test(line))
const macro: string = file.split(path.sep).pop()!.replace(sasRegExp, '') // macro name

// if brief present, remove @brief keyword
if (brief.length) brief = [brief[0].replace(briefRegExp, '')]

// if params present, add a line break before list of params and a prefix to each param
if (params.length) {
brief.push('\r')

params = params.map((param) => param.replace(paramRegExp, '-'))
}

// construct snippet description removing empty lines
const description = [
...brief,
params.length ? `Params:` : '',
...params
].filter((line) => line)

return {
prefix: `%${macro}`,
body: `%${macro}($1)`,
description: description
}
}
62 changes: 62 additions & 0 deletions src/commands/snippets/snippetsCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { generateSnippets } from './snippets'
import { CommandExample, ReturnCode } from '../../types/command'
import { TargetCommand } from '../../types/command/targetCommand'
import { getMacroFolders } from '../../utils'

const syntax = 'snippets'
const usage = 'sasjs snippets'
const description = `Generates VS Code snippets from the Doxygen headers in the SAS Macros.`
const examples: CommandExample[] = [
{
command: 'sasjs snippets',
description: description
}
]

const parseOptions = {
outDirectory: {
type: 'string',
alias: 'o',
description:
'Path to the directory where the VS Code snippets output will be generated.'
}
}

/**
* 'snippets' command class.
*/
export class SnippetsCommand extends TargetCommand {
constructor(args: string[]) {
super(args, { syntax, usage, description, examples, parseOptions })
}

/**
* Command execution method.
* @returns promise that results into return code.
*/
public async execute() {
const { target } = await this.getTargetInfo()
const macroFolders = await getMacroFolders(target)
const { outDirectory } = this.parsed

// Generate snippets
return await generateSnippets(macroFolders, outDirectory as string)
.then((filePath) => {
// handle command execution success
process.logger?.success(
`VS Code snippets generated! File location: ${filePath}`
)
process.logger?.info(
`Follow these instructions https://cli.sasjs.io/snippets/#import-snippets-to-vs-code to import generated VS Code snippets into your VS Code.`
)

return ReturnCode.Success
})
.catch((err) => {
// handle command execution failure
process.logger?.error('Error generating VS Code snippets: ', err)

return ReturnCode.InternalError
})
}
}
Empty file.
Loading

0 comments on commit 643fe8e

Please sign in to comment.