1- import { Diagnostic , DiagnosticSeverity , DocumentDiagnosticReportKind , FullDocumentDiagnosticReport , Range } from 'lsp-types' ;
1+ import { CodeAction , CodeActionKind , Diagnostic , DiagnosticSeverity , DocumentDiagnosticReportKind , FullDocumentDiagnosticReport , Range } from 'lsp-types' ;
2+ import { ImportStatement , NodeType , ProgramStatement , RuleStatement , Statement , TokenType , isCompilerError , statementIsA } from './types.js' ;
3+ import { existsSync , readFileSync , statSync } from 'fs' ;
24import { sysparser , syxparser } from './ast.js' ;
35import { tokenizeSys , tokenizeSyx } from './lexer.js' ;
4- import { isCompilerError } from './types.js' ;
5- import { readFileSync } from 'fs' ;
6+ import { dictionary } from './dictionary/index.js' ;
7+ import { fileURLToPath } from 'url' ;
8+ import { join } from 'path' ;
9+
10+
11+ // Use with addRange to include semicolons
12+ const semiRange : Range = { end : { line : 0 , character : 1 } , start : { line : 0 , character : 0 } } ;
613
714/**
815 * Creates a diagnostic report from the file path given.
@@ -19,8 +26,12 @@ export function createSyntaxScriptDiagnosticReport(filePath: string, fileContent
1926
2027 const content = fileContent ?? readFileSync ( filePath ) . toString ( ) ;
2128 const tokens = ( isSyx ? tokenizeSyx : tokenizeSys ) ( content ) ;
22- ( isSyx ? syxparser : sysparser ) . parseTokens ( tokens , filePath ) ;
29+ const ast = ( isSyx ? syxparser : sysparser ) . parseTokens ( tokens , filePath ) ;
2330
31+ items . push ( ...exportableCheck ( ast , filePath ) ) ;
32+ items . push ( ...ruleConflictCheck ( ast , filePath ) ) ;
33+ items . push ( ...sameRuleCheck ( ast , filePath ) ) ;
34+ items . push ( ...importedExistentCheck ( ast , filePath ) ) ;
2435 } catch ( error ) {
2536 if ( isCompilerError ( error ) ) {
2637 items . push ( {
@@ -37,6 +48,215 @@ export function createSyntaxScriptDiagnosticReport(filePath: string, fileContent
3748
3849}
3950
51+ // Checks rule conflicts and adds warnings when there is two defined rules that conflict each other
52+ function ruleConflictCheck ( ast : ProgramStatement , filePath : string ) : Diagnostic [ ] {
53+ const items : Diagnostic [ ] = [ ] ;
54+
55+ ast . body . forEach ( stmt => {
56+ if ( statementIsA ( stmt , NodeType . Rule ) ) {
57+ const dictRule = dictionary . Rules . find ( r => r . name === stmt . rule ) ;
58+
59+ ast . body . filter ( r => statementIsA ( r , NodeType . Rule ) ) . filter ( r => r . range !== stmt . range ) . map ( r => r as RuleStatement ) . forEach ( otherRules => {
60+ if ( dictRule . conflicts . includes ( otherRules . rule ) ) items . push ( {
61+ message : `Rule '${ otherRules . rule } ' conflicts with '${ stmt . rule } ', Both of them should not be defined.` ,
62+ range : subRange ( otherRules . range ) ,
63+ source : 'syntax-script' ,
64+ severity : DiagnosticSeverity . Warning ,
65+ data : [
66+ {
67+ title : `Remove ${ stmt . rule } definition` ,
68+ kind : CodeActionKind . QuickFix ,
69+ edit : {
70+ changes : {
71+ [ filePath ] : [
72+ {
73+ range : subRange ( addRange ( stmt . range , semiRange ) ) ,
74+ newText : ''
75+ }
76+ ]
77+ }
78+ }
79+ } ,
80+ {
81+ title : `Remove ${ otherRules . rule } definition` ,
82+ kind : CodeActionKind . QuickFix ,
83+ edit : {
84+ changes : {
85+ [ filePath ] : [
86+ {
87+ range : subRange ( addRange ( otherRules . range , semiRange ) ) ,
88+ newText : ''
89+ }
90+ ]
91+ }
92+ }
93+ }
94+ ] as CodeAction [ ]
95+ } ) ;
96+ } ) ;
97+ }
98+ } ) ;
99+
100+ return items ;
101+ }
102+
103+ // Checks if same rule is defined twice
104+ function sameRuleCheck ( ast : ProgramStatement , filePath : string ) : Diagnostic [ ] {
105+ const items : Diagnostic [ ] = [ ] ;
106+
107+ ast . body . forEach ( stmt => {
108+ if ( statementIsA ( stmt , NodeType . Rule ) ) {
109+ ast . body . filter ( r => statementIsA ( r , NodeType . Rule ) ) . filter ( r => r . range !== stmt . range ) . map ( r => r as RuleStatement ) . forEach ( otherRules => {
110+ if ( otherRules . rule === stmt . rule ) items . push ( {
111+ message : `Rule '${ stmt . rule } ' is already defined.` ,
112+ range : subRange ( stmt . range ) ,
113+ source : 'syntax-script' ,
114+ severity : DiagnosticSeverity . Error ,
115+ data : [
116+ {
117+ title : 'Remove this definition' ,
118+ kind : CodeActionKind . QuickFix ,
119+ edit : {
120+ changes : {
121+ [ filePath ] : [
122+ {
123+ range : subRange ( addRange ( stmt . range , semiRange ) ) ,
124+ newText : ''
125+ }
126+ ]
127+ }
128+ }
129+ }
130+ ] as CodeAction [ ]
131+ } ) ;
132+ } ) ;
133+ }
134+ } ) ;
135+
136+ return items ;
137+ }
138+
139+ // Checks if an import statements refers to an empty file
140+ function importedExistentCheck ( ast : ProgramStatement , filePath : string ) : Diagnostic [ ] {
141+ const items : Diagnostic [ ] = [ ] ;
142+
143+ ast . body . filter ( r => statementIsA ( r , NodeType . Import ) ) . map ( r => r as ImportStatement ) . forEach ( stmt => {
144+
145+ const filePathButPath = fileURLToPath ( filePath ) ;
146+ const fullPath = join ( filePathButPath , '../' , stmt . path ) ;
147+ if ( ! existsSync ( fullPath ) ) items . push ( {
148+ message : `Can't find file '${ fullPath } ' imported from '${ filePathButPath } '` ,
149+ severity : DiagnosticSeverity . Error ,
150+ range : subRange ( stmt . range ) ,
151+ source : 'syntax-script' ,
152+ data : [
153+ {
154+ title : 'Remove this import statement' ,
155+ kind : CodeActionKind . QuickFix ,
156+ edit : {
157+ changes : {
158+ [ filePath ] : [
159+ { range : subRange ( addRange ( stmt . range , semiRange ) ) , newText : '' }
160+ ]
161+ }
162+ }
163+ }
164+ ] as CodeAction [ ]
165+ } ) ;
166+
167+ if ( existsSync ( fullPath ) ) {
168+ const status = statSync ( fullPath ) ;
169+
170+ if ( ! status . isFile ( ) ) items . push ( {
171+ message : `'${ fullPath } ' imported from '${ filePathButPath } ' doesn't seem to be a file.` ,
172+ severity : DiagnosticSeverity . Error ,
173+ range : subRange ( stmt . range ) ,
174+ source : 'syntax-script' ,
175+ data : [
176+ {
177+ title : 'Remove this import statement' ,
178+ kind : CodeActionKind . QuickFix ,
179+ edit : {
180+ changes : {
181+ [ filePath ] : [
182+ { range : subRange ( addRange ( stmt . range , semiRange ) ) , newText : '' }
183+ ]
184+ }
185+ }
186+ }
187+ ] as CodeAction [ ]
188+ } ) ;
189+
190+ if ( ! fullPath . endsWith ( '.syx' ) ) items . push ( {
191+ message : `'${ fullPath } ' imported from '${ filePathButPath } ' cannot be imported.` ,
192+ severity : DiagnosticSeverity . Error ,
193+ range : subRange ( stmt . range ) ,
194+ source : 'syntax-script' ,
195+ data : [
196+ {
197+ title : 'Remove this import statement' ,
198+ kind : CodeActionKind . QuickFix ,
199+ edit : {
200+ changes : {
201+ [ filePath ] : [
202+ { range : subRange ( addRange ( stmt . range , semiRange ) ) , newText : '' }
203+ ]
204+ }
205+ }
206+ }
207+ ] as CodeAction [ ]
208+ } ) ;
209+ }
210+
211+ } ) ;
212+
213+ return items ;
214+ }
215+
216+ // Checks if every exported statement it actually exportable
217+ // TODO this doesnt work for some reason
218+ function exportableCheck ( ast : ProgramStatement , filePath : string ) : Diagnostic [ ] {
219+
220+ const items : Diagnostic [ ] = [ ] ;
221+
222+ ast . body . forEach ( stmt => {
223+
224+ items . push ( {
225+ message : `${ stmt . modifiers . map ( r => r . type ) . join ( ',' ) } l` ,
226+ range : subRange ( stmt . range ) ,
227+ severity : DiagnosticSeverity . Error ,
228+ source : 'syntax-script' ,
229+ data : [ ]
230+ } ) ;
231+
232+ if ( stmt . modifiers . some ( t => t . type === TokenType . ExportKeyword ) && ! dictionary . ExportableNodeTypes . includes ( stmt . type ) ) items . push ( {
233+ message : 'This statement cannot be exported.' ,
234+ range : subRange ( stmt . range ) ,
235+ severity : DiagnosticSeverity . Error ,
236+ source : 'syntax-script' ,
237+ data : [
238+ {
239+ title : 'Remove export keyword' ,
240+ kind : CodeActionKind . QuickFix ,
241+ edit : {
242+ changes : {
243+ [ filePath ] : [
244+ {
245+ newText : '' , range : subRange ( stmt . modifiers . find ( r => r . type === TokenType . ExportKeyword ) . range )
246+ }
247+ ]
248+ }
249+ }
250+ }
251+ ] as CodeAction [ ]
252+ } ) ;
253+
254+ // if (dictionary.ExportableNodeTypes.includes(stmt.type)) c((stmt as GlobalStatement).body);
255+ } ) ;
256+
257+ return items ;
258+ }
259+
40260/**
41261 * Modifies the given range to be zero-based.
42262 * @param {Range } r Any range.
@@ -52,4 +272,8 @@ export function subRange(r: Range): Range {
52272 const d = r . end . line ;
53273
54274 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 } } ;
275+ }
276+
277+ function addRange ( r : Range , r2 : Range ) : Range {
278+ 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 } } ;
55279}
0 commit comments