@syntaxs/compiler
Version:
Compiler used to compile Syntax Script projects.
314 lines (313 loc) • 13.7 kB
JavaScript
import { CodeActionKind, DiagnosticSeverity, DocumentDiagnosticReportKind } from 'lsp-types';
import { NodeType, TokenType, isCompilerError, statementIsA } from './types.js';
import { existsSync, readFileSync, statSync } from 'fs';
import { sysparser, syxparser } from './ast.js';
import { tokenizeSys, tokenizeSyx } from './lexer.js';
import { CompilerFunctions } from './compiler.js';
import { dictionary } from './dictionary/index.js';
import { fileURLToPath } from 'url';
import { join } from 'path';
// Use with addRange to include semicolons
const semiRange = { end: { line: 0, character: 1 }, start: { line: 0, character: 0 } };
/**
* Creates a diagnostic report from the file path given.
* @param {string} filePath Path of the file to create a report.
* @param {string} fileContent Content of the file if it is already fetched.
* @author efekos
* @version 1.0.1
* @since 0.0.2-alpha
* @returns A diagnostic report language servers can use.
*/
export function createSyntaxScriptDiagnosticReport(filePath, fileContent) {
const isSyx = filePath.endsWith('.syx');
const items = [];
try {
const content = fileContent ?? readFileSync(filePath).toString();
const tokens = (isSyx ? tokenizeSyx : tokenizeSys)(content);
const ast = (isSyx ? syxparser : sysparser).parseTokens(tokens, filePath);
items.push(...exportableCheck(ast.body, filePath));
items.push(...ruleConflictCheck(ast, filePath));
items.push(...sameRuleCheck(ast, filePath));
items.push(...importedExistentCheck(ast, filePath));
items.push(...sameRegexCheck(ast, filePath));
items.push(...sameNameCheck(ast.body, filePath));
}
catch (error) {
if (isCompilerError(error)) {
items.push({
message: error.message,
range: subRange(error.range),
severity: DiagnosticSeverity.Error,
data: error.actions
});
}
else {
items.push({ message: `Parser Error: ${error.message}`, range: { end: { line: 0, character: 1 }, start: { line: 0, character: 0 } }, severity: DiagnosticSeverity.Warning });
}
}
finally {
return { items: items.map(r => { return { ...r, source: 'syntax-script' }; }), kind: DocumentDiagnosticReportKind.Full };
}
}
// Checks rule conflicts and adds warnings when there is two defined rules that conflict each other
function ruleConflictCheck(ast, filePath) {
const items = [];
ast.body.forEach(stmt => {
if (statementIsA(stmt, NodeType.Rule)) {
const dictRule = dictionary.Rules.find(r => r.name === stmt.rule.value);
ast.body.filter(r => statementIsA(r, NodeType.Rule)).filter(r => r.range !== stmt.range).map(r => r).forEach(otherRules => {
if (dictRule.conflicts.includes(otherRules.rule.value))
items.push({
message: `Rule '${otherRules.rule.value}' conflicts with '${stmt.rule.value}', Both of them should not be defined.`,
range: subRange(otherRules.rule.range),
severity: DiagnosticSeverity.Warning,
data: [
{
title: `Remove ${stmt.rule.value} definition`,
kind: CodeActionKind.QuickFix,
edit: {
changes: {
[filePath]: [
{
range: subRange(addRange(stmt.range, semiRange)),
newText: ''
}
]
}
}
},
{
title: `Remove ${otherRules.rule.value} definition`,
kind: CodeActionKind.QuickFix,
edit: {
changes: {
[filePath]: [
{
range: subRange(addRange(otherRules.range, semiRange)),
newText: ''
}
]
}
}
}
]
});
});
}
});
return items;
}
// Checks if same rule is defined twice
function sameRuleCheck(ast, filePath) {
const items = [];
ast.body.forEach(stmt => {
if (statementIsA(stmt, NodeType.Rule)) {
ast.body.filter(r => statementIsA(r, NodeType.Rule)).filter(r => r.range !== stmt.range).map(r => r).forEach(otherRules => {
if (otherRules.rule === stmt.rule)
items.push({
message: `Rule '${stmt.rule.value}' is already defined.`,
range: subRange(stmt.rule.range),
severity: DiagnosticSeverity.Error,
data: [
{
title: 'Remove this definition',
kind: CodeActionKind.QuickFix,
edit: {
changes: {
[filePath]: [
{
range: subRange(addRange(stmt.range, semiRange)),
newText: ''
}
]
}
}
}
]
});
});
}
});
return items;
}
// Checks if an import statements refers to an empty file
function importedExistentCheck(ast, filePath) {
const items = [];
ast.body.filter(r => statementIsA(r, NodeType.Import)).map(r => r).forEach(stmt => {
const filePathButPath = fileURLToPath(filePath);
const fullPath = join(filePathButPath, '../', stmt.path.value);
if (!existsSync(fullPath))
items.push({
message: `Can't find file '${fullPath}' imported from '${filePathButPath}'`,
severity: DiagnosticSeverity.Error,
range: subRange(stmt.path.range),
data: [
{
title: 'Remove this import statement',
kind: CodeActionKind.QuickFix,
edit: {
changes: {
[filePath]: [
{ range: subRange(addRange(stmt.range, semiRange)), newText: '' }
]
}
}
}
]
});
if (existsSync(fullPath)) {
const status = statSync(fullPath);
if (!status.isFile())
items.push({
message: `'${fullPath}' imported from '${filePathButPath}' doesn't seem to be a file.`,
severity: DiagnosticSeverity.Error,
range: subRange(stmt.path.range),
data: [
{
title: 'Remove this import statement',
kind: CodeActionKind.QuickFix,
edit: {
changes: {
[filePath]: [
{ range: subRange(addRange(stmt.range, semiRange)), newText: '' }
]
}
}
}
]
});
if (!fullPath.endsWith('.syx'))
items.push({
message: `'${fullPath}' imported from '${filePathButPath}' cannot be imported.`,
severity: DiagnosticSeverity.Error,
range: subRange(stmt.path.range),
data: [
{
title: 'Remove this import statement',
kind: CodeActionKind.QuickFix,
edit: {
changes: {
[filePath]: [
{ range: subRange(addRange(stmt.range, semiRange)), newText: '' }
]
}
}
}
]
});
}
});
return items;
}
// Checks if there are multiple operators with the same regex
function sameRegexCheck(ast, filePath) {
const items = [];
const encounteredRegexes = [];
ast.body.filter(r => statementIsA(r, NodeType.Operator)).map(r => r).forEach(stmt => {
const regex = new RegExp(CompilerFunctions.generateRegexMatcher(stmt));
if (encounteredRegexes.some(r => r.source === regex.source))
items.push({
message: 'Regex of this operator is same with another operator.',
range: subRange(syxparser.combineTwo(stmt.regex[0].range, stmt.regex[stmt.regex.length - 1].range)),
severity: DiagnosticSeverity.Error,
data: [
{
title: 'Remove this operator',
kind: CodeActionKind.QuickFix,
edit: {
changes: {
[filePath]: [
{
newText: '',
range: subRange(stmt.range)
}
]
}
}
}
]
});
else
encounteredRegexes.push(regex);
});
return items;
}
// Checks if every exported statement it actually exportable
function exportableCheck(statements, filePath) {
const items = [];
statements.forEach(stmt => {
if (stmt.modifiers.some(t => t.type === TokenType.ExportKeyword) && !dictionary.ExportableNodeTypes.includes(stmt.type))
items.push({
message: 'This statement cannot be exported.',
range: subRange(stmt.modifiers.find(r => r.type === TokenType.ExportKeyword).range),
severity: DiagnosticSeverity.Error,
data: [
{
title: 'Remove export keyword',
kind: CodeActionKind.QuickFix,
edit: {
changes: {
[filePath]: [
{
newText: '', range: subRange(stmt.modifiers.find(r => r.type === TokenType.ExportKeyword).range)
}
]
}
}
}
]
});
if (dictionary.StatementTypesWithBody.includes(stmt.type))
items.push(...exportableCheck(stmt.body, filePath));
});
return items;
}
// Check if everything has a unique name
function sameNameCheck(statements, filePath) {
const items = [];
function c(s) {
const encounteredNames = [];
s
.filter(r => statementIsA(r, NodeType.Function) || statementIsA(r, NodeType.Global) || statementIsA(r, NodeType.Keyword))
.map(r => {
if (statementIsA(r, NodeType.Function))
return r;
if (statementIsA(r, NodeType.Global))
return r;
if (statementIsA(r, NodeType.Keyword))
return r;
}).forEach(stmt => {
const n = stmt[statementIsA(stmt, NodeType.Keyword) ? 'word' : 'name'];
if (encounteredNames.includes(n.value))
items.push({
message: `Name '${n.value}' is already seen before.`,
range: subRange(n.range),
severity: DiagnosticSeverity.Error
});
else
encounteredNames.push(n.value);
if (statementIsA(stmt, NodeType.Global))
c(stmt.body);
});
}
c(statements);
return items;
}
/**
* Modifies the given range to be zero-based.
* @param {Range} r Any range.
* @returns Same range with every value decreased by 1.
* @author efekos
* @version 1.0.0
* @since 0.0.1-alpha
*/
export function subRange(r) {
const a = r.start.character;
const b = r.start.line;
const c = r.end.character;
const d = r.end.line;
return { start: { character: a === 0 ? 0 : a - 1, line: b === 0 ? 0 : b - 1 }, end: { character: c === 0 ? 0 : c - 1, line: d === 0 ? 0 : d - 1 } };
}
function addRange(r, r2) {
return { end: { line: r.end.line + r2.end.line, character: r.end.character + r.end.character }, start: { character: r.start.character, line: r.start.line } };
}