Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/syntax_checker #248

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"type": "npm",
"script": "compile:tests"
},
"args": ["issue_239"],
"env": {
"INCLUDE_DIR": "${workspaceFolder}"
}
Expand Down
2 changes: 1 addition & 1 deletion extension/server/src/providers/linter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export async function refreshLinterDiagnostics(document: TextDocument, docs: Cac

const diagnostic = Diagnostic.create(
range,
Linter.getErrorText(error.type),
error.message || Linter.getErrorText(error.type!),
DiagnosticSeverity.Warning
);

Expand Down
11 changes: 9 additions & 2 deletions language/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Range, Position } from "./models/DataPoints";
import opcodes from "./models/opcodes";
import Document from "./document";
import { IssueRange, Offset, Rules, SelectBlock } from "./parserTypes";
import { validateTokens } from "./validator";

const errorText = {
'BlankStructNamesCheck': `Struct names cannot be blank (\`*N\`).`,
Expand Down Expand Up @@ -39,7 +40,8 @@ const errorText = {
'NoExtProgramVariable': `Not allowed to use variable in EXTPGM or EXTPROC.`,
'IncludeMustBeRelative': `Path not valid. It must be relative to the project.`,
'SQLHostVarCheck': `Also defined in scope. Should likely be host variable.`,
'RequireOtherBlock': `OTHER block missing from SELECT block.`
'RequireOtherBlock': `OTHER block missing from SELECT block.`,
'Validator': `Token not expected.`
};

const skipRules = {
Expand All @@ -50,7 +52,7 @@ const skipRules = {
};

export default class Linter {
static getErrorText(error) {
static getErrorText(error: keyof Rules) {
return errorText[error];
}

Expand Down Expand Up @@ -118,6 +120,11 @@ export default class Linter {
lineNumber = docStatement.range.line;
currentIndent = docStatement.indent;

if (rules.Validator) {
const possibleError = validateTokens(statement);
if (possibleError) errors.push(possibleError);
}

if (currentIndent >= 0) {
skipIndentCheck = false;

Expand Down
16 changes: 8 additions & 8 deletions language/parserTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,15 @@ export interface Rules {
IncludeMustBeRelative?: boolean;
SQLHostVarCheck?: boolean;
RequireOtherBlock?: boolean;
Validator?: boolean;

/** Not exposed rules below */

/** When true, will update Cache will references found in linter */
CollectReferences?: boolean;

InvalidDeclareNumber?: boolean;
UnexpectedEnd?: boolean;
}

export interface DefinitionPosition {
Expand All @@ -73,15 +79,9 @@ export interface Offset {

export interface IssueRange {
offset: Offset;
type?: "BlankStructNamesCheck"|"QualifiedCheck"|"PrototypeCheck"|"ForceOptionalParens"|
"NoOCCURS"|"NoSELECTAll"|"UselessOperationCheck"|"UppercaseConstants"|"SpecificCasing"|
"InvalidDeclareNumber"|"IncorrectVariableCase"|"RequiresParameter"|
"RequiresProcedureDescription"|"StringLiteralDupe"|"RequireBlankSpecial"|
"CopybookDirective"|"UppercaseDirectives"|"NoSQLJoins"|"NoGlobalsInProcedures"|
"NoCTDATA"|"PrettyComments"|"NoGlobalSubroutines"|"NoLocalSubroutines"|"UnexpectedEnd"|
"NoUnreferenced"|"NoExternalTo"|"NoExecuteImmediate"|"NoExtProgramVariable"|"IncludeMustBeRelative"|
"SQLHostVarCheck"|"RequireOtherBlock";
type?: keyof Rules;
newValue?: string;
message?: string;
}

export interface SelectBlock {
Expand Down
40 changes: 37 additions & 3 deletions language/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ enum ReadState {
IN_COMMENT = "Comment"
}

const numReg = new RegExp(/^\d+$/)

const commonMatchers: Matcher[] = [
{
name: `FORMAT_STATEMEMT`,
Expand Down Expand Up @@ -44,6 +46,18 @@ const commonMatchers: Matcher[] = [
},
{
name: `IS_NUMBER`,
match: [
{
type: `word`,
match: (word) => numReg.test(word)
},
],
becomes: {
type: `number`
}
},
{
name: `DEC_NUMBER`,
match: [
{ type: `number` },
{ type: `dot` },
Expand All @@ -59,7 +73,7 @@ const commonMatchers: Matcher[] = [
{ type: `asterisk` },
{
type: `word`, match: (word) =>
[`CTDATA`, `BLANK`, `BLANKS`, `ZERO`, `ZEROS`, `ON`, `OFF`, `NULL`, `ISO`, `MDY`, `DMY`, `EUR`, `YMD`, `USA`, `SECONDS`, `S`, `MINUTES`, `MN`, `HOURS`, `H`, `DAYS`, `D`, `MONTHS`, `M`, `YEARS`, `Y`, `HIVAL`, `END`, `LOVAL`, `START`, `N`, `OMIT`, `STRING`, `CWIDEN`, `CONVERT`].includes(word.toUpperCase()) || word.toUpperCase().startsWith(`IN`)
[`CTDATA`, `BLANK`, `BLANKS`, `ZERO`, `ZEROS`, `ON`, `OFF`, `NULL`, `ISO`, `MDY`, `DMY`, `EUR`, `YMD`, `USA`, `SECONDS`, `S`, `MINUTES`, `MN`, `HOURS`, `H`, `DAYS`, `D`, `MONTHS`, `M`, `YEARS`, `Y`, `HIVAL`, `END`, `LOVAL`, `START`, `N`, `OMIT`, `STRING`, `CWIDEN`, `CONVERT`, `KEY`, `SRCSTMT`, `NOPASS`, `YES`, `NO`, `NODEBUGIO`, `TRIM`, `VARSIZE`, `CALLER`].includes(word.toUpperCase()) || word.toUpperCase().startsWith(`IN`)
}
],
becomes: {
Expand Down Expand Up @@ -169,6 +183,16 @@ const commonMatchers: Matcher[] = [
type: `word`
}
},
{
name: `QUOTE-STRING`,
match: [
{ type: `string`, match: (word) => word === `''` },
{ type: `string`, match: (word) => word === `''` },
],
becomes: {
type: `string`
}
},
{
name: `CTL-OPT`,
match: [
Expand Down Expand Up @@ -279,6 +303,14 @@ export function tokenise(statement) {
switch (statement[i]) {
// When it's the string character..
case stringChar:
// Silly way RPG does escape strings...
const escapeStringStart = (state === ReadState.IN_STRING && statement[i+1] === stringChar);

if (escapeStringStart) {
currentText += (stringChar + stringChar);
i++;

} else
if (state === ReadState.IN_STRING) {
currentText += statement[i];
result.push({ value: currentText, type: `string`, range: { start: startsAt, end: startsAt + currentText.length, line: lineNumber } });
Expand All @@ -288,8 +320,10 @@ export function tokenise(statement) {
currentText += statement[i];
}

// @ts-ignore
state = state === ReadState.IN_STRING ? ReadState.NORMAL : ReadState.IN_STRING;
if (escapeStringStart !== true) {
// @ts-ignore
state = state === ReadState.IN_STRING ? ReadState.NORMAL : ReadState.IN_STRING;
}
break;

// When it's any other character...
Expand Down
139 changes: 139 additions & 0 deletions language/validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { IssueRange } from "./parserTypes";
import { Token } from "./types";

interface GenericTokenFormat {
not?: boolean;
type: string[];
value?: string;
};

interface GenericWhenRule {
when: GenericTokenFormat;
start?: boolean;
then?: GenericTokenFormat[];
end?: boolean;
}

const Operators = [`plus`, `minus`, `divide`, `asterisk`, `comma`, `dot`];
const ValueTypes = [`string`, `number`, `special`, `hex`, `builtin`];
const ExprParts = [...Operators, ...ValueTypes];

const GenericRules: GenericWhenRule[] = [
{
when: {
type: [`format`]
},
start: true,
then: [],
end: true,
},
{
when: {
type: [`directive`, `declare`],
},
start: true
},
{
when: {
type: [`seperator`],
},
then: [{not: true, type: Operators}]
},
{
when: {
type: [`openbracket`],
},
then: [{not: true, type: Operators}]
},
{
when: {
type: [`closebracket`],
},
then: [{not: true, type: ValueTypes}]
},
{
when: {
type: Operators
},
then: [{not: true, type: Operators}]
},
{
when: {
type: ValueTypes
},
then: [{not: true, type: ValueTypes}]
},
{
when: {
type: Operators
},
then: [{type: ValueTypes}]
},
];
Object.freeze(GenericRules);

export function validateTokens(tokens: Token[]): IssueRange|undefined {
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
let cToken = tokens[i];

const currentRule = GenericRules.find(rule => rule.when.type.includes(cToken.type) && rule.when.value === undefined || rule.when.value === cToken.value);

if (currentRule) {
let cI = i;
if (cI !== 0 && currentRule.start) {
// Throw error. This can only be at the start
return {
offset: {position: cToken.range.start, end: cToken.range.end},
type: `Validator`,
message: `Token is expected at the start of statements`
}
}

if (currentRule.then && currentRule.then.length > 0) {
for (const thenItem of currentRule.then) {
cI++;

cToken = tokens[cI];

if (cToken) {
const typeMatch = thenItem.type.includes(cToken.type) && (thenItem.value ? thenItem.value === cToken.value : true);
const isError = (thenItem.not ? typeMatch : !typeMatch);

if (isError) {
// Token unexpected
return {
offset: {position: cToken.range.start, end: cToken.range.end},
type: `Validator`,
message: `Token not expected`
}
}
} else if (thenItem.not !== true) {
return {
offset: {position: token.range.start, end: token.range.end},
type: `Validator`,
message: `'${thenItem.type.join()}' is ${thenItem.not ? `not` : ``} expected.`
}
}
}

cI++;
}

if (cI !== (tokens.length-1) && currentRule.end) {
cToken = tokens[cI];

if (cToken) {
// Throw error. This can only be at the end
return {
offset: {position: cToken.range.start, end: cToken.range.end},
type: `Validator`,
message: `Token should be at the end of the statement only.`
}
}
}
}
}

return;
}
5 changes: 5 additions & 0 deletions schemas/rpglint.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@
"$id": "#/properties/RequireOtherBlock",
"type": "boolean",
"description": "Require SELECT blocks to have an OTHER block."
},
"Validator": {
"$id": "#/properties/Validator",
"type": "boolean",
"description": "Will add additional validation against RPGLE syntax. Useful for finding common code issue."
}
},
"additionalProperties": true
Expand Down
1 change: 1 addition & 0 deletions tests/suite/basics.js
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ exports.indicators1 = async () => {
const cache = await parser.getDocs(uri, lines);

Linter.getErrors({ uri, content: lines }, {
Validator: true,
CollectReferences: true,
}, cache);

Expand Down
Loading