collie 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +333 -0
- data/Rakefile +9 -0
- data/collie.gemspec +37 -0
- data/docs/TUTORIAL.md +588 -0
- data/docs/index.html +56 -0
- data/docs/playground/README.md +134 -0
- data/docs/playground/build-collie-bundle.rb +85 -0
- data/docs/playground/css/styles.css +402 -0
- data/docs/playground/index.html +146 -0
- data/docs/playground/js/app.js +231 -0
- data/docs/playground/js/collie-bridge.js +186 -0
- data/docs/playground/js/editor.js +129 -0
- data/docs/playground/js/examples.js +80 -0
- data/docs/playground/js/ruby-runner.js +75 -0
- data/docs/playground/test-server.sh +18 -0
- data/exe/collie +15 -0
- data/lib/collie/analyzer/conflict.rb +114 -0
- data/lib/collie/analyzer/reachability.rb +83 -0
- data/lib/collie/analyzer/recursion.rb +96 -0
- data/lib/collie/analyzer/symbol_table.rb +67 -0
- data/lib/collie/ast.rb +183 -0
- data/lib/collie/cli.rb +249 -0
- data/lib/collie/config.rb +91 -0
- data/lib/collie/formatter/formatter.rb +196 -0
- data/lib/collie/formatter/options.rb +23 -0
- data/lib/collie/linter/base.rb +62 -0
- data/lib/collie/linter/registry.rb +34 -0
- data/lib/collie/linter/rules/ambiguous_precedence.rb +87 -0
- data/lib/collie/linter/rules/circular_reference.rb +89 -0
- data/lib/collie/linter/rules/consistent_tag_naming.rb +69 -0
- data/lib/collie/linter/rules/duplicate_token.rb +38 -0
- data/lib/collie/linter/rules/empty_action.rb +52 -0
- data/lib/collie/linter/rules/factorizable_rules.rb +67 -0
- data/lib/collie/linter/rules/left_recursion.rb +34 -0
- data/lib/collie/linter/rules/long_rule.rb +37 -0
- data/lib/collie/linter/rules/missing_start_symbol.rb +38 -0
- data/lib/collie/linter/rules/nonterminal_naming.rb +34 -0
- data/lib/collie/linter/rules/prec_improvement.rb +54 -0
- data/lib/collie/linter/rules/redundant_epsilon.rb +44 -0
- data/lib/collie/linter/rules/right_recursion.rb +35 -0
- data/lib/collie/linter/rules/token_naming.rb +39 -0
- data/lib/collie/linter/rules/trailing_whitespace.rb +46 -0
- data/lib/collie/linter/rules/undefined_symbol.rb +55 -0
- data/lib/collie/linter/rules/unreachable_rule.rb +49 -0
- data/lib/collie/linter/rules/unused_nonterminal.rb +93 -0
- data/lib/collie/linter/rules/unused_token.rb +82 -0
- data/lib/collie/parser/lexer.rb +349 -0
- data/lib/collie/parser/parser.rb +416 -0
- data/lib/collie/reporter/github.rb +35 -0
- data/lib/collie/reporter/json.rb +52 -0
- data/lib/collie/reporter/text.rb +97 -0
- data/lib/collie/version.rb +5 -0
- data/lib/collie.rb +52 -0
- metadata +145 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Collie Playground - Interactive Grammar Linter</title>
|
|
7
|
+
<link rel="stylesheet" href="css/styles.css">
|
|
8
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs/editor/editor.main.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="app">
|
|
12
|
+
<!-- Header -->
|
|
13
|
+
<header class="header">
|
|
14
|
+
<div class="container">
|
|
15
|
+
<h1>Collie Playground</h1>
|
|
16
|
+
<p>Interactive linter and formatter for Lrama Style BNF grammar files</p>
|
|
17
|
+
<div class="header-actions">
|
|
18
|
+
<a href="https://github.com/ydah/collie" target="_blank" class="btn btn-secondary">
|
|
19
|
+
GitHub
|
|
20
|
+
</a>
|
|
21
|
+
<a href="../TUTORIAL.html" class="btn btn-secondary">
|
|
22
|
+
Tutorial
|
|
23
|
+
</a>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</header>
|
|
27
|
+
|
|
28
|
+
<!-- Loading Screen -->
|
|
29
|
+
<div id="loading" class="loading">
|
|
30
|
+
<div class="loading-content">
|
|
31
|
+
<div class="spinner"></div>
|
|
32
|
+
<p>Loading Ruby.wasm and Collie...</p>
|
|
33
|
+
<p class="loading-detail">First load may take a moment</p>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- Main Content -->
|
|
38
|
+
<div id="main-content" class="main-content" style="display: none;">
|
|
39
|
+
<div class="container">
|
|
40
|
+
<!-- Toolbar -->
|
|
41
|
+
<div class="toolbar">
|
|
42
|
+
<div class="toolbar-left">
|
|
43
|
+
<select id="example-select" class="select">
|
|
44
|
+
<option value="">Load Example...</option>
|
|
45
|
+
<option value="simple">Simple Calculator</option>
|
|
46
|
+
<option value="lrama">Lrama Features</option>
|
|
47
|
+
<option value="invalid">Invalid Grammar (Demo)</option>
|
|
48
|
+
</select>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="toolbar-right">
|
|
51
|
+
<button id="lint-btn" class="btn btn-primary">
|
|
52
|
+
Lint
|
|
53
|
+
</button>
|
|
54
|
+
<button id="format-btn" class="btn btn-primary">
|
|
55
|
+
Format
|
|
56
|
+
</button>
|
|
57
|
+
<button id="fix-btn" class="btn btn-success">
|
|
58
|
+
Fix All
|
|
59
|
+
</button>
|
|
60
|
+
<button id="clear-btn" class="btn btn-secondary">
|
|
61
|
+
Clear
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Editor and Output -->
|
|
67
|
+
<div class="workspace">
|
|
68
|
+
<!-- Editor Panel -->
|
|
69
|
+
<div class="panel panel-editor">
|
|
70
|
+
<div class="panel-header">
|
|
71
|
+
<h2>Grammar File</h2>
|
|
72
|
+
<span class="panel-info">calc.y</span>
|
|
73
|
+
</div>
|
|
74
|
+
<div id="editor" class="editor-container"></div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Output Panel -->
|
|
78
|
+
<div class="panel panel-output">
|
|
79
|
+
<div class="panel-header">
|
|
80
|
+
<h2>Output</h2>
|
|
81
|
+
<div class="output-tabs">
|
|
82
|
+
<button class="tab-btn active" data-tab="diagnostics">
|
|
83
|
+
Diagnostics
|
|
84
|
+
</button>
|
|
85
|
+
<button class="tab-btn" data-tab="formatted">
|
|
86
|
+
Formatted
|
|
87
|
+
</button>
|
|
88
|
+
<button class="tab-btn" data-tab="rules">
|
|
89
|
+
Rules
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<!-- Diagnostics Tab -->
|
|
95
|
+
<div id="tab-diagnostics" class="tab-content active">
|
|
96
|
+
<div id="diagnostics-output" class="output-content">
|
|
97
|
+
<div class="placeholder">
|
|
98
|
+
Click "Lint" to check for issues
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Formatted Tab -->
|
|
104
|
+
<div id="tab-formatted" class="tab-content">
|
|
105
|
+
<pre id="formatted-output" class="output-content"><div class="placeholder">Click "Format" to see formatted output</div></pre>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<!-- Rules Tab -->
|
|
109
|
+
<div id="tab-rules" class="tab-content">
|
|
110
|
+
<div id="rules-output" class="output-content">
|
|
111
|
+
<div class="placeholder">
|
|
112
|
+
Loading rules...
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<!-- Footer Info -->
|
|
120
|
+
<div class="info-panel">
|
|
121
|
+
<h3>About Collie</h3>
|
|
122
|
+
<p>
|
|
123
|
+
Collie is a linter and formatter for Lrama Style BNF grammar files.
|
|
124
|
+
It provides 18+ built-in lint rules to catch errors, enforce conventions,
|
|
125
|
+
and suggest optimizations.
|
|
126
|
+
</p>
|
|
127
|
+
<p>
|
|
128
|
+
This playground runs completely in your browser using
|
|
129
|
+
<a href="https://github.com/ruby/ruby.wasm" target="_blank">Ruby.wasm</a>.
|
|
130
|
+
Your code never leaves your browser.
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<!-- Scripts -->
|
|
138
|
+
<script src="https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.6.2/dist/browser.script.iife.js"></script>
|
|
139
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs/loader.min.js"></script>
|
|
140
|
+
<script src="js/examples.js?v=3"></script>
|
|
141
|
+
<script src="js/ruby-runner.js?v=3"></script>
|
|
142
|
+
<script src="js/collie-bridge.js?v=3"></script>
|
|
143
|
+
<script src="js/editor.js?v=3"></script>
|
|
144
|
+
<script src="js/app.js?v=3"></script>
|
|
145
|
+
</body>
|
|
146
|
+
</html>
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Main playground application
|
|
2
|
+
|
|
3
|
+
class PlaygroundApp {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.rubyRunner = new RubyRunner();
|
|
6
|
+
this.collieBridge = null;
|
|
7
|
+
this.editorManager = new EditorManager();
|
|
8
|
+
this.currentTab = 'diagnostics';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async initialize() {
|
|
12
|
+
try {
|
|
13
|
+
document.getElementById('loading').style.display = 'flex';
|
|
14
|
+
|
|
15
|
+
await this.rubyRunner.initialize();
|
|
16
|
+
this.collieBridge = new CollieBridge(this.rubyRunner);
|
|
17
|
+
await this.editorManager.initialize('editor');
|
|
18
|
+
await this.loadRulesList();
|
|
19
|
+
this.setupEventListeners();
|
|
20
|
+
|
|
21
|
+
document.getElementById('loading').style.display = 'none';
|
|
22
|
+
document.getElementById('main-content').style.display = 'block';
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Initialization error:', error);
|
|
25
|
+
this.showInitError('Failed to initialize playground: ' + error.message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
showInitError(message) {
|
|
30
|
+
const loadingContent = document.querySelector('.loading-content');
|
|
31
|
+
loadingContent.innerHTML = `
|
|
32
|
+
<div style="color: #f5222d; max-width: 500px;">
|
|
33
|
+
<h2>Initialization Failed</h2>
|
|
34
|
+
<p>${this.escapeHtml(message)}</p>
|
|
35
|
+
<p style="font-size: 0.9rem; margin-top: 1rem;">
|
|
36
|
+
Please check the browser console for more details.
|
|
37
|
+
</p>
|
|
38
|
+
<button onclick="location.reload()" style="margin-top: 1rem; padding: 0.5rem 1rem; cursor: pointer;">
|
|
39
|
+
Retry
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setupEventListeners() {
|
|
46
|
+
// Example selector
|
|
47
|
+
document.getElementById('example-select').addEventListener('change', (e) => {
|
|
48
|
+
const exampleKey = e.target.value;
|
|
49
|
+
if (exampleKey && EXAMPLES[exampleKey]) {
|
|
50
|
+
this.editorManager.setValue(EXAMPLES[exampleKey].code);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Lint button
|
|
55
|
+
document.getElementById('lint-btn').addEventListener('click', () => {
|
|
56
|
+
this.handleLint();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Format button
|
|
60
|
+
document.getElementById('format-btn').addEventListener('click', () => {
|
|
61
|
+
this.handleFormat();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Fix button
|
|
65
|
+
document.getElementById('fix-btn').addEventListener('click', () => {
|
|
66
|
+
this.handleFixAll();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Clear button
|
|
70
|
+
document.getElementById('clear-btn').addEventListener('click', () => {
|
|
71
|
+
this.editorManager.setValue('');
|
|
72
|
+
this.editorManager.clearMarkers();
|
|
73
|
+
this.clearOutput();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Tab switching
|
|
77
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
78
|
+
btn.addEventListener('click', (e) => {
|
|
79
|
+
this.switchTab(e.target.dataset.tab);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async handleLint() {
|
|
85
|
+
const source = this.editorManager.getValue();
|
|
86
|
+
if (!source.trim()) {
|
|
87
|
+
this.showMessage('diagnostics-output', 'Please enter some code first');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.showLoading('diagnostics-output');
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const diagnostics = await this.collieBridge.lint(source);
|
|
95
|
+
this.displayDiagnostics(diagnostics);
|
|
96
|
+
this.editorManager.setMarkers(diagnostics);
|
|
97
|
+
this.switchTab('diagnostics');
|
|
98
|
+
} catch (error) {
|
|
99
|
+
this.showError('Linting failed: ' + error.message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async handleFormat() {
|
|
104
|
+
const source = this.editorManager.getValue();
|
|
105
|
+
if (!source.trim()) {
|
|
106
|
+
this.showMessage('formatted-output', 'Please enter some code first');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.showLoading('formatted-output');
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const formatted = await this.collieBridge.format(source);
|
|
114
|
+
document.getElementById('formatted-output').textContent = formatted;
|
|
115
|
+
this.switchTab('formatted');
|
|
116
|
+
} catch (error) {
|
|
117
|
+
this.showError('Formatting failed: ' + error.message);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async handleFixAll() {
|
|
122
|
+
const source = this.editorManager.getValue();
|
|
123
|
+
if (!source.trim()) {
|
|
124
|
+
this.showMessage('diagnostics-output', 'Please enter some code first');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const corrected = await this.collieBridge.autocorrect(source);
|
|
130
|
+
this.editorManager.setValue(corrected);
|
|
131
|
+
await this.handleLint();
|
|
132
|
+
} catch (error) {
|
|
133
|
+
this.showError('Auto-correction failed: ' + error.message);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
displayDiagnostics(diagnostics) {
|
|
138
|
+
const container = document.getElementById('diagnostics-output');
|
|
139
|
+
|
|
140
|
+
if (diagnostics.length === 0) {
|
|
141
|
+
container.innerHTML = '<div class="no-offenses">✓ No offenses detected</div>';
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const html = diagnostics.map(diag => `
|
|
146
|
+
<div class="diagnostic-item ${diag.severity}">
|
|
147
|
+
<div class="diagnostic-location">
|
|
148
|
+
${diag.location.file}:${diag.location.line}:${diag.location.column}
|
|
149
|
+
</div>
|
|
150
|
+
<div class="diagnostic-message">
|
|
151
|
+
${this.escapeHtml(diag.message)}
|
|
152
|
+
</div>
|
|
153
|
+
<span class="diagnostic-rule">${diag.rule_name}</span>
|
|
154
|
+
${diag.autocorrectable ? ' <span class="diagnostic-rule">autocorrectable</span>' : ''}
|
|
155
|
+
</div>
|
|
156
|
+
`).join('');
|
|
157
|
+
|
|
158
|
+
container.innerHTML = html;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async loadRulesList() {
|
|
162
|
+
try {
|
|
163
|
+
const rules = await this.collieBridge.getRules();
|
|
164
|
+
this.displayRules(rules);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('Failed to load rules:', error);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
displayRules(rules) {
|
|
171
|
+
const container = document.getElementById('rules-output');
|
|
172
|
+
|
|
173
|
+
const html = rules.map(rule => `
|
|
174
|
+
<div class="rule-item">
|
|
175
|
+
<div class="rule-header">
|
|
176
|
+
<span class="rule-name">${rule.name}</span>
|
|
177
|
+
<span class="rule-severity ${rule.severity}">${rule.severity}</span>
|
|
178
|
+
</div>
|
|
179
|
+
<div class="rule-description">
|
|
180
|
+
${this.escapeHtml(rule.description)}
|
|
181
|
+
${rule.autocorrectable ? ' (autocorrectable)' : ''}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
`).join('');
|
|
185
|
+
|
|
186
|
+
container.innerHTML = html;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
switchTab(tabName) {
|
|
190
|
+
// Update buttons
|
|
191
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
192
|
+
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Update content
|
|
196
|
+
document.querySelectorAll('.tab-content').forEach(content => {
|
|
197
|
+
content.classList.toggle('active', content.id === `tab-${tabName}`);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
this.currentTab = tabName;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
showLoading(containerId) {
|
|
204
|
+
document.getElementById(containerId).innerHTML = '<div class="placeholder">Processing...</div>';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
showMessage(containerId, message) {
|
|
208
|
+
document.getElementById(containerId).innerHTML = `<div class="placeholder">${message}</div>`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
showError(message) {
|
|
212
|
+
alert(message);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
clearOutput() {
|
|
216
|
+
document.getElementById('diagnostics-output').innerHTML = '<div class="placeholder">Click "Lint" to check for issues</div>';
|
|
217
|
+
document.getElementById('formatted-output').textContent = '';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
escapeHtml(text) {
|
|
221
|
+
const div = document.createElement('div');
|
|
222
|
+
div.textContent = text;
|
|
223
|
+
return div.innerHTML;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Initialize app when page loads
|
|
228
|
+
window.addEventListener('DOMContentLoaded', async () => {
|
|
229
|
+
const app = new PlaygroundApp();
|
|
230
|
+
await app.initialize();
|
|
231
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Bridge between JavaScript and Ruby Collie code
|
|
2
|
+
|
|
3
|
+
class CollieBridge {
|
|
4
|
+
constructor(rubyRunner) {
|
|
5
|
+
this.ruby = rubyRunner;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async lint(source) {
|
|
9
|
+
const escapedSource = source.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
10
|
+
|
|
11
|
+
const rubyCode = `
|
|
12
|
+
require 'json'
|
|
13
|
+
|
|
14
|
+
source = '${escapedSource}'
|
|
15
|
+
|
|
16
|
+
begin
|
|
17
|
+
lexer = Collie::Parser::Lexer.new(source, filename: "playground.y")
|
|
18
|
+
tokens = lexer.tokenize
|
|
19
|
+
parser = Collie::Parser::Parser.new(tokens)
|
|
20
|
+
ast = parser.parse
|
|
21
|
+
|
|
22
|
+
symbol_table = Collie::Analyzer::SymbolTable.new
|
|
23
|
+
ast.declarations.each do |decl|
|
|
24
|
+
case decl
|
|
25
|
+
when Collie::AST::TokenDeclaration
|
|
26
|
+
decl.names.each do |name|
|
|
27
|
+
symbol_table.add_token(name, type_tag: decl.type_tag, location: decl.location)
|
|
28
|
+
rescue Collie::Error
|
|
29
|
+
# Ignore duplicate declarations
|
|
30
|
+
end
|
|
31
|
+
when Collie::AST::ParameterizedRule
|
|
32
|
+
symbol_table.add_nonterminal(decl.name, location: decl.location)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
ast.rules.each do |rule|
|
|
37
|
+
symbol_table.add_nonterminal(rule.name, location: rule.location)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
config = Collie::Config.new
|
|
41
|
+
Collie::Linter::Registry.load_rules
|
|
42
|
+
enabled_rules = Collie::Linter::Registry.enabled_rules(config)
|
|
43
|
+
|
|
44
|
+
context = { symbol_table: symbol_table, source: source, file: "playground.y" }
|
|
45
|
+
|
|
46
|
+
offenses = []
|
|
47
|
+
enabled_rules.each do |rule_class|
|
|
48
|
+
rule = rule_class.new(config.rule_config(rule_class.rule_name))
|
|
49
|
+
offenses.concat(rule.check(ast, context))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
result = offenses.map do |offense|
|
|
53
|
+
location = offense.location || Collie::AST::Location.new(file: "playground.y", line: 1, column: 1)
|
|
54
|
+
{
|
|
55
|
+
severity: offense.severity.to_s,
|
|
56
|
+
rule_name: offense.rule.rule_name,
|
|
57
|
+
message: offense.message,
|
|
58
|
+
location: {
|
|
59
|
+
file: location.file,
|
|
60
|
+
line: location.line,
|
|
61
|
+
column: location.column
|
|
62
|
+
},
|
|
63
|
+
autocorrectable: offense.autocorrectable?
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
JSON.generate(result)
|
|
68
|
+
rescue => e
|
|
69
|
+
JSON.generate([{
|
|
70
|
+
severity: "error",
|
|
71
|
+
rule_name: "ParseError",
|
|
72
|
+
message: e.message,
|
|
73
|
+
location: { file: "playground.y", line: 1, column: 1 },
|
|
74
|
+
autocorrectable: false
|
|
75
|
+
}])
|
|
76
|
+
end
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const result = await this.ruby.eval(rubyCode);
|
|
80
|
+
return JSON.parse(result.toString());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async format(source) {
|
|
84
|
+
const escapedSource = source.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
85
|
+
|
|
86
|
+
const rubyCode = `
|
|
87
|
+
source = '${escapedSource}'
|
|
88
|
+
|
|
89
|
+
begin
|
|
90
|
+
lexer = Collie::Parser::Lexer.new(source, filename: "playground.y")
|
|
91
|
+
tokens = lexer.tokenize
|
|
92
|
+
parser = Collie::Parser::Parser.new(tokens)
|
|
93
|
+
ast = parser.parse
|
|
94
|
+
|
|
95
|
+
config = Collie::Config.new
|
|
96
|
+
formatter = Collie::Formatter::Formatter.new(
|
|
97
|
+
Collie::Formatter::Options.new(config.formatter_options)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
formatter.format(ast)
|
|
101
|
+
rescue => e
|
|
102
|
+
"Error: #{e.message}"
|
|
103
|
+
end
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
const result = await this.ruby.eval(rubyCode);
|
|
107
|
+
return result.toString();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async autocorrect(source) {
|
|
111
|
+
const escapedSource = source.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
112
|
+
|
|
113
|
+
const rubyCode = `
|
|
114
|
+
source = '${escapedSource}'
|
|
115
|
+
|
|
116
|
+
begin
|
|
117
|
+
lexer = Collie::Parser::Lexer.new(source, filename: "playground.y")
|
|
118
|
+
tokens = lexer.tokenize
|
|
119
|
+
parser = Collie::Parser::Parser.new(tokens)
|
|
120
|
+
ast = parser.parse
|
|
121
|
+
|
|
122
|
+
symbol_table = Collie::Analyzer::SymbolTable.new
|
|
123
|
+
ast.declarations.each do |decl|
|
|
124
|
+
case decl
|
|
125
|
+
when Collie::AST::TokenDeclaration
|
|
126
|
+
decl.names.each do |name|
|
|
127
|
+
symbol_table.add_token(name, type_tag: decl.type_tag, location: decl.location)
|
|
128
|
+
rescue Collie::Error
|
|
129
|
+
# Ignore
|
|
130
|
+
end
|
|
131
|
+
when Collie::AST::ParameterizedRule
|
|
132
|
+
symbol_table.add_nonterminal(decl.name, location: decl.location)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
ast.rules.each do |rule|
|
|
137
|
+
symbol_table.add_nonterminal(rule.name, location: rule.location)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
context = { symbol_table: symbol_table, source: source, file: "playground.y" }
|
|
141
|
+
|
|
142
|
+
config = Collie::Config.new
|
|
143
|
+
Collie::Linter::Registry.load_rules
|
|
144
|
+
enabled_rules = Collie::Linter::Registry.enabled_rules(config)
|
|
145
|
+
|
|
146
|
+
offenses = []
|
|
147
|
+
enabled_rules.each do |rule_class|
|
|
148
|
+
rule = rule_class.new(config.rule_config(rule_class.rule_name))
|
|
149
|
+
offenses.concat(rule.check(ast, context))
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
autocorrectable = offenses.select(&:autocorrectable?)
|
|
153
|
+
autocorrectable.each { |offense| offense.autocorrect&.call }
|
|
154
|
+
|
|
155
|
+
context[:source]
|
|
156
|
+
rescue => e
|
|
157
|
+
source
|
|
158
|
+
end
|
|
159
|
+
`;
|
|
160
|
+
|
|
161
|
+
const result = await this.ruby.eval(rubyCode);
|
|
162
|
+
return result.toString();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async getRules() {
|
|
166
|
+
const rubyCode = `
|
|
167
|
+
require 'json'
|
|
168
|
+
|
|
169
|
+
Collie::Linter::Registry.load_rules
|
|
170
|
+
|
|
171
|
+
rules = Collie::Linter::Registry.all.map do |rule_class|
|
|
172
|
+
{
|
|
173
|
+
name: rule_class.rule_name,
|
|
174
|
+
description: rule_class.description,
|
|
175
|
+
severity: rule_class.severity.to_s,
|
|
176
|
+
autocorrectable: rule_class.autocorrectable
|
|
177
|
+
}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
JSON.generate(rules)
|
|
181
|
+
`;
|
|
182
|
+
|
|
183
|
+
const result = await this.ruby.eval(rubyCode);
|
|
184
|
+
return JSON.parse(result.toString());
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Monaco Editor integration
|
|
2
|
+
|
|
3
|
+
class EditorManager {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.editor = null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async initialize(containerId) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
require.config({
|
|
11
|
+
paths: {
|
|
12
|
+
vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs'
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
require(['vs/editor/editor.main'], () => {
|
|
17
|
+
// Register Yacc language
|
|
18
|
+
monaco.languages.register({ id: 'yacc' });
|
|
19
|
+
|
|
20
|
+
// Define syntax highlighting
|
|
21
|
+
monaco.languages.setMonarchTokensProvider('yacc', {
|
|
22
|
+
keywords: [
|
|
23
|
+
'token', 'type', 'left', 'right', 'nonassoc', 'prec', 'start',
|
|
24
|
+
'union', 'rule', 'inline'
|
|
25
|
+
],
|
|
26
|
+
|
|
27
|
+
tokenizer: {
|
|
28
|
+
root: [
|
|
29
|
+
// Comments
|
|
30
|
+
[/\/\*/, 'comment', '@comment'],
|
|
31
|
+
[/\/\/.*$/, 'comment'],
|
|
32
|
+
|
|
33
|
+
// Directives
|
|
34
|
+
[/%[a-z]+/, {
|
|
35
|
+
cases: {
|
|
36
|
+
'@keywords': 'keyword',
|
|
37
|
+
'@default': 'keyword'
|
|
38
|
+
}
|
|
39
|
+
}],
|
|
40
|
+
|
|
41
|
+
// Section separator
|
|
42
|
+
[/^%%/, 'keyword.control'],
|
|
43
|
+
|
|
44
|
+
// Tokens (uppercase)
|
|
45
|
+
[/\b[A-Z_][A-Z0-9_]*\b/, 'constant'],
|
|
46
|
+
|
|
47
|
+
// Nonterminals (lowercase)
|
|
48
|
+
[/\b[a-z_][a-z0-9_]*\b/, 'variable'],
|
|
49
|
+
|
|
50
|
+
// Strings
|
|
51
|
+
[/"[^"]*"/, 'string'],
|
|
52
|
+
[/'[^']*'/, 'string'],
|
|
53
|
+
|
|
54
|
+
// Actions
|
|
55
|
+
[/\{/, 'delimiter.curly', '@action'],
|
|
56
|
+
],
|
|
57
|
+
|
|
58
|
+
comment: [
|
|
59
|
+
[/\*\//, 'comment', '@pop'],
|
|
60
|
+
[/./, 'comment']
|
|
61
|
+
],
|
|
62
|
+
|
|
63
|
+
action: [
|
|
64
|
+
[/\}/, 'delimiter.curly', '@pop'],
|
|
65
|
+
[/./, 'embedded']
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Create editor
|
|
71
|
+
this.editor = monaco.editor.create(document.getElementById(containerId), {
|
|
72
|
+
value: '// Type your grammar here or load an example\n',
|
|
73
|
+
language: 'yacc',
|
|
74
|
+
theme: 'vs',
|
|
75
|
+
automaticLayout: true,
|
|
76
|
+
minimap: { enabled: false },
|
|
77
|
+
fontSize: 14,
|
|
78
|
+
lineNumbers: 'on',
|
|
79
|
+
scrollBeyondLastLine: false,
|
|
80
|
+
wordWrap: 'on',
|
|
81
|
+
tabSize: 4
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
resolve(this.editor);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getValue() {
|
|
90
|
+
return this.editor.getValue();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setValue(value) {
|
|
94
|
+
this.editor.setValue(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setMarkers(diagnostics) {
|
|
98
|
+
const model = this.editor.getModel();
|
|
99
|
+
const markers = diagnostics.map(diag => ({
|
|
100
|
+
severity: this.severityToMonaco(diag.severity),
|
|
101
|
+
startLineNumber: diag.location.line,
|
|
102
|
+
startColumn: diag.location.column,
|
|
103
|
+
endLineNumber: diag.location.line,
|
|
104
|
+
endColumn: diag.location.column + 10,
|
|
105
|
+
message: `[${diag.rule_name}] ${diag.message}`
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
monaco.editor.setModelMarkers(model, 'collie', markers);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
severityToMonaco(severity) {
|
|
112
|
+
switch (severity) {
|
|
113
|
+
case 'error':
|
|
114
|
+
return monaco.MarkerSeverity.Error;
|
|
115
|
+
case 'warning':
|
|
116
|
+
return monaco.MarkerSeverity.Warning;
|
|
117
|
+
case 'convention':
|
|
118
|
+
case 'info':
|
|
119
|
+
return monaco.MarkerSeverity.Info;
|
|
120
|
+
default:
|
|
121
|
+
return monaco.MarkerSeverity.Hint;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
clearMarkers() {
|
|
126
|
+
const model = this.editor.getModel();
|
|
127
|
+
monaco.editor.setModelMarkers(model, 'collie', []);
|
|
128
|
+
}
|
|
129
|
+
}
|