UNPKG

@syntaxs/compiler

Version:

Compiler used to compile Syntax Script projects.

314 lines (313 loc) 13.7 kB
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 } }; }