diff --git a/README.md b/README.md index 1acab8f..2ae4a66 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,16 @@ Profile Scripting Language functionality for Visual Studio Code. ## Dependencies -* Visual Studio Code version 1.23.0 (April 2018) or higher +* Visual Studio Code version 1.23.0 (April 2018) or higher. -## Configuration +## Avaliable Settings + +* `psl.lint` Whether to lint files written in PSL. The default value is `config`, which means linting only activates when the `psl-lint.json` config file is present. [Read more here](#psl-lint). +* `psl.previewFeatures` Set true to enable the latest developing features (requires restart). Default value is false. +* `psl.trailingNewline` Adds a trailing newline after a "Get" or "Refresh". The default behavior is to not change the output. +* `psl.documentationServer` HTTP POST endpoint that responds with PSL documentation in markdown format. + +## Environment Configuration Locate the button at the bottom-right corner titled `Configure Environments`. If the button is not visible, use the Command Palette (F1 or Ctrl+Shift+P) to find the `PSL: Configure Environment` command. A JSON object of the following form will appear: @@ -44,6 +51,7 @@ The extension is able to communicate with Host via MRPC121 to do the following: * Test Compile .PROC and .PSL files * Compile and Link .PROC and .PSL files * Run .PROC and .PSL files +* [Code Quality checks via psl-lint](#psl-lint) These commands can be executed via the Command Pallette (F1 or Ctrl+Shift+P), icons at the top-right corner of the document, right-clicking the document, or right-clicking the file in the Side Bar Explorer. @@ -54,14 +62,35 @@ Basic language features also exist for files written in PSL, data configuration, These features include: * Syntax coloring -* Property and Label outline for PSL files (access by Ctrl+Shift+O or with [this extension](https://marketplace.visualstudio.com/items?itemName=patrys.vscode-code-outline)) -* Auto-complete for Record objects in PSL (activated by the `.` operator or by Ctrl+Space) +* Property and Label outline for PSL files (access by Ctrl+Shift+O or by enabling the built-in outline). +* Code Completion, Hoves, and Go-To Definition by activating `psl.previewFeatures`. * Highlighting and Hover information for editing data configuration files * Code snippets for loops, comments, and table/column definitions +### psl-lint + +This extension includes support for checking PSL against common coding standards. The setting `psl.lint` is by default set to `config`, meaning the linting module will activate upon finding a `psl-lint.json` configuration file. Here is a sample: + +``` +{ + "version": 1, + "include": { + "Z*": ["*"], + "*": ["TodoInfo"] + }, + "exclude": { + "ZRPC*.PROC": ["MemberCamelCase"] + } +} +``` + +Within `include` and `exclude` mappings from filename patterns to rules. These are glob-style patterns ("Z*" will match all files that start with Z). The rules are written in an array, and must be explicitly stated. The only exception is the "*" rule, which matches all rules. + +[For more information about which rules are available, and how the linting can be used outside of vscode, visit the package at npm](https://www.npmjs.com/package/psl-lint). + ## Development -If you would like to join the development of this extension, you will need to install [node.js](https://nodejs.org/en/) (with NPM) in order to install the dependencies. +If you would like to join the development of this extension, you will need to install [node.js](https://nodejs.org/en/) (with npm) in order to install the dependencies. Once you clone the project, from the command line in the root of this project, run `npm install`. diff --git a/src/pslLint/activate.ts b/src/pslLint/activate.ts index 5fc40fa..cab6e8e 100644 --- a/src/pslLint/activate.ts +++ b/src/pslLint/activate.ts @@ -1,14 +1,12 @@ import * as path from 'path'; +import { ParsedDocument } from '../parser/parser'; import { - DeclarationRule, Diagnostic, FileDefinitionRule, MemberRule, MethodRule, ParameterRule, - ProfileComponent, ProfileComponentRule, PropertyRule, PslRule, + DeclarationRule, DeclarationRuleConstructor, Diagnostic, FileDefinitionRule, FileDefinitionRuleConstructor, + MemberRule, MemberRuleConstructor, MethodRule, MethodRuleConstructor, ParameterRule, ParameterRuleConstructor, + ProfileComponent, ProfileComponentRule, ProfileComponentRuleConstructor, PropertyRule, PropertyRuleConstructor, + PslRule, PslRuleConstructor, } from './api'; import { getConfig, matchConfig } from './config'; - -/** - * Import rules here. - */ -import { ParsedDocument } from '../parser/parser'; import { MemberCamelCase, MemberLength, MemberLiteralCase, MemberStartsWithV, PropertyIsDummy, PropertyIsDuplicate, @@ -20,36 +18,33 @@ import { RuntimeStart } from './runtime'; import { TblColDocumentation } from './tblcolDoc'; import { TodoInfo } from './todos'; -/** - * Add new rules here to have them checked at the appropriate time. - */ -const componentRules: ProfileComponentRule[] = []; -const fileDefinitionRules: FileDefinitionRule[] = [ - new TblColDocumentation(), +const componentRuleConstructors: ProfileComponentRuleConstructor[] = []; +const fileDefinitionRuleConstructors: FileDefinitionRuleConstructor[] = [ + TblColDocumentation, ]; -const pslRules: PslRule[] = [ - new TodoInfo(), +const pslRuleConstructors: PslRuleConstructor[] = [ + TodoInfo, ]; -const memberRules: MemberRule[] = [ - new MemberCamelCase(), - new MemberLength(), - new MemberStartsWithV(), - new MemberLiteralCase(), +const memberRuleConstructors: MemberRuleConstructor[] = [ + MemberCamelCase, + MemberLength, + MemberStartsWithV, + MemberLiteralCase, ]; -const methodRules: MethodRule[] = [ - new MethodDocumentation(), - new MethodSeparator(), - new MethodParametersOnNewLine(), - new RuntimeStart(), - new MultiLineDeclare(), - new TwoEmptyLines(), +const methodRuleConstructors: MethodRuleConstructor[] = [ + MethodDocumentation, + MethodSeparator, + MethodParametersOnNewLine, + RuntimeStart, + MultiLineDeclare, + TwoEmptyLines, ]; -const propertyRules: PropertyRule[] = [ - new PropertyIsDummy(), - new PropertyIsDuplicate(), +const propertyRuleConstructors: PropertyRuleConstructor[] = [ + PropertyIsDummy, + PropertyIsDuplicate, ]; -const declarationRules: DeclarationRule[] = []; -const parameterRules: ParameterRule[] = []; +const declarationRuleConstructors: DeclarationRuleConstructor[] = []; +const parameterRuleConstructors: ParameterRuleConstructor[] = []; export function getDiagnostics( profileComponent: ProfileComponent, @@ -61,11 +56,10 @@ export function getDiagnostics( } /** - * Interface for adding and executing rules. + * Manages which rules need to be applied to a given component. */ class RuleSubscription { - private diagnostics: Diagnostic[]; private componentRules: ProfileComponentRule[]; private pslRules: PslRule[]; private fileDefinitionRules: FileDefinitionRule[]; @@ -76,81 +70,80 @@ class RuleSubscription { private parameterRules: ParameterRule[]; constructor(private profileComponent: ProfileComponent, private parsedDocument?: ParsedDocument, useConfig?: boolean) { - this.diagnostics = []; - const config = useConfig ? getConfig(this.profileComponent.fsPath) : undefined; - const initializeRules = (rules: ProfileComponentRule[]) => { - return rules.filter(rule => { - if (!config) return true; - return matchConfig(path.basename(this.profileComponent.fsPath), rule.ruleName, config); - }).map(rule => { - rule.profileComponent = this.profileComponent; - return rule; + const filterRule = (ruleCtor: ProfileComponentRuleConstructor | PslRuleConstructor) => { + if (!useConfig) return true; + if (!config) return false; + return matchConfig(path.basename(this.profileComponent.fsPath), ruleCtor.name, config); + }; + const initializeRules = (ruleCtors: ProfileComponentRuleConstructor[]) => { + return ruleCtors.filter(filterRule).map(ruleCtor => { + return new ruleCtor(this.profileComponent); }); }; - const initializePslRules = (rules: PslRule[]) => { - const componentInitialized = initializeRules(rules) as PslRule[]; - const pslParsedDocument = this.parsedDocument as ParsedDocument; - return componentInitialized.map(rule => { - rule.parsedDocument = pslParsedDocument; - return rule; + const initializePslRules = (ruleCtors: PslRuleConstructor[]) => { + return ruleCtors.filter(filterRule).map(ruleCtor => { + const pslParsedDocument = this.parsedDocument as ParsedDocument; + return new ruleCtor(this.profileComponent, pslParsedDocument); }); }; - this.componentRules = initializeRules(componentRules); - this.fileDefinitionRules = initializeRules(fileDefinitionRules); - this.pslRules = initializePslRules(pslRules); - this.methodRules = initializePslRules(methodRules); - this.memberRules = initializePslRules(memberRules); - this.propertyRules = initializePslRules(propertyRules); - this.declarationRules = initializePslRules(declarationRules); - this.parameterRules = initializePslRules(parameterRules); + this.componentRules = initializeRules(componentRuleConstructors); + this.fileDefinitionRules = initializeRules(fileDefinitionRuleConstructors); + this.pslRules = initializePslRules(pslRuleConstructors); + this.methodRules = initializePslRules(methodRuleConstructors); + this.memberRules = initializePslRules(memberRuleConstructors); + this.propertyRules = initializePslRules(propertyRuleConstructors); + this.declarationRules = initializePslRules(declarationRuleConstructors); + this.parameterRules = initializePslRules(parameterRuleConstructors); } reportRules(): Diagnostic[] { - const addDiagnostics = (rules: ProfileComponentRule[], ...args: any[]) => { - rules.forEach(rule => this.diagnostics.push(...rule.report(...args))); + const diagnostics: Diagnostic[] = []; + + const collectDiagnostics = (rules: ProfileComponentRule[], ...args: any[]) => { + rules.forEach(rule => diagnostics.push(...rule.report(...args))); }; - addDiagnostics(this.componentRules); + collectDiagnostics(this.componentRules); if (ProfileComponent.isFileDefinition(this.profileComponent.fsPath)) { - addDiagnostics(this.fileDefinitionRules); + collectDiagnostics(this.fileDefinitionRules); } if (ProfileComponent.isPsl(this.profileComponent.fsPath)) { - addDiagnostics(this.pslRules); + collectDiagnostics(this.pslRules); const parsedDocument = this.parsedDocument as ParsedDocument; for (const property of parsedDocument.properties) { - addDiagnostics(this.memberRules, property); - addDiagnostics(this.propertyRules, property); + collectDiagnostics(this.memberRules, property); + collectDiagnostics(this.propertyRules, property); } for (const declaration of parsedDocument.declarations) { - addDiagnostics(this.memberRules, declaration); - addDiagnostics(this.declarationRules, declaration); + collectDiagnostics(this.memberRules, declaration); + collectDiagnostics(this.declarationRules, declaration); } for (const method of parsedDocument.methods) { - addDiagnostics(this.memberRules, method); - addDiagnostics(this.methodRules, method); + collectDiagnostics(this.memberRules, method); + collectDiagnostics(this.methodRules, method); for (const parameter of method.parameters) { - addDiagnostics(this.memberRules, parameter); - addDiagnostics(this.parameterRules, parameter, method); + collectDiagnostics(this.memberRules, parameter); + collectDiagnostics(this.parameterRules, parameter, method); } for (const declaration of method.declarations) { - addDiagnostics(this.memberRules, declaration); - addDiagnostics(this.declarationRules, declaration, method); + collectDiagnostics(this.memberRules, declaration); + collectDiagnostics(this.declarationRules, declaration, method); } } } - return this.diagnostics; + return diagnostics; } } diff --git a/src/pslLint/api.ts b/src/pslLint/api.ts index fcbae73..9a7acd3 100644 --- a/src/pslLint/api.ts +++ b/src/pslLint/api.ts @@ -113,10 +113,21 @@ export class DiagnosticRelatedInformation { export abstract class ProfileComponentRule { + /** + * The rule name as it is known in the `psl-lint.json` configuration. + * This is automatically the same as the rule class name. + */ readonly ruleName: string = this.constructor.name; + /** + * The current component being checked. + */ profileComponent: ProfileComponent; + constructor(profileComponent: ProfileComponent) { + this.profileComponent = profileComponent; + } + abstract report(...args: any[]): Diagnostic[]; } @@ -124,31 +135,77 @@ export abstract class FileDefinitionRule extends ProfileComponentRule { } export abstract class PslRule extends ProfileComponentRule { + /** + * A parsed version of a PSL component. + */ parsedDocument: ParsedDocument; - abstract report(...args: any[]): Diagnostic[]; + constructor(profileComponent: ProfileComponent, parsedDocument: ParsedDocument) { + super(profileComponent); + this.parsedDocument = parsedDocument; + } + } +/** + * A rule that apply to a Property, Declaration, Parameter, or Method equally. + */ export abstract class MemberRule extends PslRule { abstract report(member: Member): Diagnostic[]; } +/** + * A rule that only apply to a Property. + */ export abstract class PropertyRule extends PslRule { abstract report(property: Property): Diagnostic[]; } - +/** + * A rule that only apply to a Method. + */ export abstract class MethodRule extends PslRule { abstract report(method: Method): Diagnostic[]; } - +/** + * A rule that only apply to a Parameter. + */ export abstract class ParameterRule extends PslRule { abstract report(parameter: Parameter, method: Method): Diagnostic[]; } - +/** + * A rule that only apply to a Declaration. + */ export abstract class DeclarationRule extends PslRule { abstract report(declaration: Declaration, method?: Method): Diagnostic[]; } +export type FileDefinitionRuleConstructor = new (profileComponent: ProfileComponent) => FileDefinitionRule; +export type ProfileComponentRuleConstructor = new (profileComponent: ProfileComponent) => ProfileComponentRule; +export type PslRuleConstructor = new ( + profileComponent: ProfileComponent, + parsedDocument: ParsedDocument, +) => PslRule; +export type MemberRuleConstructor = new ( + profileComponent: ProfileComponent, + parsedDocument: ParsedDocument, +) => MemberRule; +export type PropertyRuleConstructor = new ( + profileComponent: ProfileComponent, + parsedDocument: ParsedDocument, +) => PropertyRule; +export type MethodRuleConstructor = new ( + profileComponent: ProfileComponent, + parsedDocument: ParsedDocument, +) => MethodRule; +export type ParameterRuleConstructor = new ( + profileComponent: ProfileComponent, + parsedDocument: ParsedDocument, +) => ParameterRule; +export type DeclarationRuleConstructor = new ( + profileComponent: ProfileComponent, + parsedDocument: ParsedDocument, +) => DeclarationRule; + type GetTextMethod = (lineNumber: number) => string; /** @@ -156,25 +213,38 @@ type GetTextMethod = (lineNumber: number) => string; * The file may be PSL or non-PSL (such as a TBL or COL). */ export class ProfileComponent { - + /* + * Utility method to check a path. + */ static isPsl(fsPath: string): boolean { return path.extname(fsPath) === '.PROC' || path.extname(fsPath) === '.BATCH' || path.extname(fsPath) === '.TRIG' || path.extname(fsPath).toUpperCase() === '.PSL'; } - + /* + * Utility method to check a path. + */ static isFileDefinition(fsPath: string): boolean { return path.extname(fsPath) === '.TBL' || path.extname(fsPath) === '.COL'; } - + /* + * Utility method to check a path. + */ static isProfileComponent(fsPath: string): boolean { return ProfileComponent.isPsl(fsPath) || ProfileComponent.isFileDefinition(fsPath); } + /** + * The path on the file system to the component. + */ fsPath: string; + + /** + * The text contents of the component. + */ textDocument: string; private indexedDocument?: Map; diff --git a/src/pslLint/cli/README.md b/src/pslLint/cli/README.md index eae911f..d27b93a 100644 --- a/src/pslLint/cli/README.md +++ b/src/pslLint/cli/README.md @@ -15,12 +15,29 @@ This module works by adding rules that are automatically checked at the appropri * MethodSeparator * MultiLineDeclare * PropertyIsDummy -* PropertyIsDuplicate * RuntimeStart * TblColDocumentation * TodoInfo * TwoEmptyLines +## Configuration + +By creating a file `psl-lint.json` at the root of the project, you can control the behavior of the linter. The format of the file is as follows: + +``` +{ + "version": 1, + "include": { + "Z*": ["*"], + "*": ["TodoInfo"] + }, + "exclude": { + "ZRPC*.PROC": ["MemberCamelCase"] + } +} +``` + +Within `include` and `exclude` are filename-to-rules mappings. The filenames can be glob patterns ("Z*" will match all files that start with Z). The rules are written in an array, and must be explicitly stated. The only exception is the "*" rule, which matches all rules. ## Contributing diff --git a/tslint.json b/tslint.json index 881607c..40efb64 100644 --- a/tslint.json +++ b/tslint.json @@ -43,7 +43,8 @@ ], "no-console": [ false - ] + ], + "array-type": [true, "array"] }, "defaultSeverity": "warn" }