-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathRubyConverter.java
More file actions
201 lines (178 loc) · 7.79 KB
/
RubyConverter.java
File metadata and controls
201 lines (178 loc) · 7.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
/*
* SonarSource Ruby
* Copyright (C) 2018-2026 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
package org.sonarsource.ruby.converter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.jruby.Ruby;
import org.jruby.RubyRuntimeAdapter;
import org.jruby.exceptions.NoMethodError;
import org.jruby.exceptions.StandardError;
import org.jruby.javasupport.JavaEmbedUtils;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.specialized.RubyArrayTwoObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonarsource.ruby.converter.adapter.CommentAdapter;
import org.sonarsource.ruby.converter.adapter.RangeAdapter;
import org.sonarsource.ruby.converter.adapter.TokenAdapter;
import org.sonarsource.slang.api.ASTConverter;
import org.sonarsource.slang.api.Comment;
import org.sonarsource.slang.api.ParseException;
import org.sonarsource.slang.api.TextPointer;
import org.sonarsource.slang.api.TextRange;
import org.sonarsource.slang.api.Token;
import org.sonarsource.slang.api.Tree;
import org.sonarsource.slang.api.TreeMetaData;
import org.sonarsource.slang.impl.TextRanges;
import org.sonarsource.slang.impl.TopLevelTreeImpl;
import org.sonarsource.slang.impl.TreeMetaDataProvider;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
public class RubyConverter implements ASTConverter {
private static final Logger LOG = LoggerFactory.getLogger(RubyConverter.class);
private static final String SETUP_SCRIPT_PATH = "whitequark_parser_init.rb";
private static final String RACC_RUBYGEM_PATH = "racc-1.8.1-java/lib";
private static final String AST_RUBYGEM_PATH = "ast-2.4.3/lib";
private static final String PARSER_RUBYGEM_PATH = "parser-3.3.10.0/lib";
private static final String COMMENT_TOKEN_TYPE = "tCOMMENT";
static final String FILENAME = "(Analysis of Ruby)";
private final RubyRuntimeAdapter rubyRuntimeAdapter;
private final Ruby runtime;
RubyConverter(RubyRuntimeAdapter rubyRuntimeAdapter) {
this.rubyRuntimeAdapter = rubyRuntimeAdapter;
try {
runtime = initializeRubyRuntime();
} catch (IOException e) {
throw new IllegalStateException("Failed to initialized ruby runtime", e);
}
}
public RubyConverter() {
this(JavaEmbedUtils.newRuntimeAdapter());
}
@Override
public void terminate() {
// Shutdown and terminate ruby instance
if (runtime != null) {
JavaEmbedUtils.terminate(runtime);
}
}
@Override
public Tree parse(String content) {
try {
return parseContent(content);
} catch (StandardError e) {
throw new ParseException(e.getMessage(), getErrorLocation(e), e);
} catch (Exception e) {
throw new ParseException(e.getMessage(), null, e);
}
}
TextPointer getErrorLocation(StandardError e) {
try {
IRubyObject diagnostic = (IRubyObject) invokeMethod(e.getException(), "diagnostic", null);
IRubyObject location = (IRubyObject) invokeMethod(diagnostic, "location", null);
if (location != null) {
return new RangeAdapter(runtime, location).toTextRange().start();
}
} catch (NoMethodError nme) {
// location information could not be retrieved from ruby object
}
LOG.warn("No location information available for parse error");
return null;
}
Tree parseContent(String content) {
Object[] parameters = {content, FILENAME};
List<Object> rubyParseResult = (List<Object>) invokeMethod(runtime.getObject(), "parse_with_tokens", parameters);
if (rubyParseResult == null) {
throw new ParseException("Unable to parse file content");
}
Object rubyAst = rubyParseResult.get(0);
List<IRubyObject> rubyComments = (List<IRubyObject>) rubyParseResult.get(1);
List<IRubyObject> rubyTokens = (List<IRubyObject>) rubyParseResult.get(2);
List<Comment> comments = rubyComments.stream()
.map(rubyComment -> new CommentAdapter(runtime, rubyComment))
.map(CommentAdapter::toSlangComment)
.toList();
List<Token> tokens = rubyTokens.stream()
.map(rubyToken -> new TokenAdapter(runtime, (RubyArrayTwoObject) rubyToken))
.filter(tokenAdapter -> !COMMENT_TOKEN_TYPE.equals(tokenAdapter.getTokenType().asJavaString()))
.map(TokenAdapter::toSlangToken)
.filter(Objects::nonNull)
.toList();
TreeMetaDataProvider metaDataProvider = new TreeMetaDataProvider(comments, tokens);
if (tokens.isEmpty() && comments.isEmpty()) {
throw new ParseException("No AST node found");
}
Object[] visitParams = {rubyAst, new RubyVisitor(metaDataProvider)};
Tree tree = (Tree) invokeMethod(runtime.getObject(), "visit", visitParams);
TreeMetaData topTreeMetaData = metaDataProvider.metaData(getFullRange(tokens, comments));
if (tree == null) {
// only comments
return new TopLevelTreeImpl(topTreeMetaData, emptyList(), comments);
} else {
// singleton expression: we wrap it around a top level tree
return new TopLevelTreeImpl(topTreeMetaData, singletonList(tree), comments);
}
}
private static TextRange getFullRange(List<Token> tokens, List<Comment> comments) {
if (comments.isEmpty()) {
return TextRanges.merge(Arrays.asList(tokens.get(0).textRange(), tokens.get(tokens.size() - 1).textRange()));
} else if (tokens.isEmpty()) {
return TextRanges.merge(Arrays.asList(comments.get(0).textRange(), comments.get(comments.size() - 1).textRange()));
}
return TextRanges.merge(Arrays.asList(
tokens.get(0).textRange(),
tokens.get(tokens.size() - 1).textRange(),
comments.get(0).textRange(),
comments.get(comments.size() - 1).textRange()));
}
@Nullable
Object invokeMethod(@Nullable Object receiver, String methodName, @Nullable Object[] args) {
return JavaEmbedUtils.invokeMethod(runtime, receiver, methodName, args, Object.class);
}
private Ruby initializeRubyRuntime() throws IOException {
URL raccRubygem = RubyConverter.class.getResource(fromRoot(RACC_RUBYGEM_PATH));
URL astRubygem = RubyConverter.class.getResource(fromRoot(AST_RUBYGEM_PATH));
URL parserRubygem = RubyConverter.class.getResource(fromRoot(PARSER_RUBYGEM_PATH));
URL initParserScriptUrl = RubyConverter.class.getResource(fromRoot(SETUP_SCRIPT_PATH));
Ruby rubyRuntime = JavaEmbedUtils.initialize(Arrays.asList(raccRubygem.toString(), astRubygem.toString(), parserRubygem.toString()));
System.setProperty("jruby.thread.pool.enabled", "true");
String initParserScript = new String(getBytes(initParserScriptUrl), UTF_8);
rubyRuntimeAdapter.eval(rubyRuntime, initParserScript);
return rubyRuntime;
}
private static byte[] getBytes(URL url) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try(InputStream in = url.openStream()) {
byte[] buffer = new byte[8192];
for(int len = in.read(buffer); len != -1; len = in.read(buffer)) {
out.write(buffer, 0, len);
}
}
return out.toByteArray();
}
private static String fromRoot(String path) {
return "/" + path;
}
}