Skip to content

Commit 8607377

Browse files
authored
Merge pull request #249 from github/erb-lib
Add codeql_ruby.ast.Erb library
2 parents b8ec5d7 + 41e7ef1 commit 8607377

File tree

13 files changed

+543
-24
lines changed

13 files changed

+543
-24
lines changed

ql/lib/codeql/Locations.qll

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,12 @@ class Location extends @location {
5050
filepath = f.getAbsolutePath()
5151
)
5252
}
53+
54+
/** Holds if this location starts strictly before the specified location. */
55+
pragma[inline]
56+
predicate strictlyBefore(Location other) {
57+
this.getStartLine() < other.getStartLine()
58+
or
59+
this.getStartLine() = other.getStartLine() and this.getStartColumn() < other.getStartColumn()
60+
}
5361
}

ql/lib/codeql/ruby/AST.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import codeql.Locations
22
import ast.Call
33
import ast.Control
44
import ast.Constant
5+
import ast.Erb
56
import ast.Expr
67
import ast.Literal
78
import ast.Method

ql/lib/codeql/ruby/ast/Erb.qll

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
private import codeql.Locations
2+
private import codeql.ruby.AST
3+
private import internal.Erb
4+
private import internal.TreeSitter
5+
6+
/**
7+
* A node in the ERB abstract syntax tree. This class is the base class for all
8+
* ERB elements.
9+
*/
10+
class ErbAstNode extends TAstNode {
11+
/** Gets a textual representation of this node. */
12+
cached
13+
string toString() { none() }
14+
15+
/** Gets the location of this node. */
16+
Location getLocation() { result = getLocation(this) }
17+
18+
/**
19+
* Gets the name of a primary CodeQL class to which this node belongs.
20+
*
21+
* This predicate always has a result. If no primary class can be
22+
* determined, the result is `"???"`. If multiple primary classes match,
23+
* this predicate can have multiple results.
24+
*/
25+
string getAPrimaryQlClass() { result = "???" }
26+
}
27+
28+
/**
29+
* An ERB template. This can contain multiple directives to be executed when
30+
* the template is compiled.
31+
*/
32+
class ErbTemplate extends TTemplate, ErbAstNode {
33+
private Erb::Template g;
34+
35+
ErbTemplate() { this = TTemplate(g) }
36+
37+
override string toString() { result = "erb template" }
38+
39+
final override string getAPrimaryQlClass() { result = "ErbTemplate" }
40+
41+
ErbAstNode getAChildNode() { toGenerated(result) = g.getChild(_) }
42+
}
43+
44+
// Truncate the token string value to 32 char max
45+
bindingset[val]
46+
private string displayToken(string val) {
47+
val.length() <= 32 and result = val
48+
or
49+
val.length() > 32 and result = val.prefix(29) + "..."
50+
}
51+
52+
/**
53+
* An ERB token. This could be embedded code, a comment, or arbitrary text.
54+
*/
55+
class ErbToken extends TTokenNode, ErbAstNode {
56+
override string toString() { result = displayToken(this.getValue()) }
57+
58+
/** Gets the string value of this token. */
59+
string getValue() { exists(Erb::Token g | this = fromGenerated(g) | result = g.getValue()) }
60+
61+
override string getAPrimaryQlClass() { result = "ErbToken" }
62+
}
63+
64+
/**
65+
* An ERB token appearing within a comment directive.
66+
*/
67+
class ErbComment extends ErbToken {
68+
private Erb::Comment g;
69+
70+
ErbComment() { this = TComment(g) }
71+
72+
override string getValue() { result = g.getValue() }
73+
74+
final override string getAPrimaryQlClass() { result = "ErbComment" }
75+
}
76+
77+
/**
78+
* An ERB token appearing within a code directive. This will typically be
79+
* interpreted as Ruby code or a GraphQL query, depending on context.
80+
*/
81+
class ErbCode extends ErbToken {
82+
private Erb::Code g;
83+
84+
ErbCode() { this = TCode(g) }
85+
86+
override string getValue() { result = g.getValue() }
87+
88+
final override string getAPrimaryQlClass() { result = "ErbCode" }
89+
}
90+
91+
bindingset[line, col]
92+
private predicate locationIncludesPosition(Location loc, int line, int col) {
93+
// position between start and end line, exclusive
94+
line > loc.getStartLine() and
95+
line < loc.getEndLine()
96+
or
97+
// position on start line, multi line location
98+
line = loc.getStartLine() and
99+
not loc.getStartLine() = loc.getEndLine() and
100+
col >= loc.getStartColumn()
101+
or
102+
// position on end line, multi line location
103+
line = loc.getEndLine() and
104+
not loc.getStartLine() = loc.getEndLine() and
105+
col <= loc.getEndColumn()
106+
or
107+
// single line location, position between start and end column
108+
line = loc.getStartLine() and
109+
loc.getStartLine() = loc.getEndLine() and
110+
col >= loc.getStartColumn() and
111+
col <= loc.getEndColumn()
112+
}
113+
114+
/**
115+
* A directive in an ERB template.
116+
*/
117+
class ErbDirective extends TDirectiveNode, ErbAstNode {
118+
private predicate containsStartOf(Location loc) {
119+
loc.getFile() = this.getLocation().getFile() and
120+
locationIncludesPosition(this.getLocation(), loc.getStartLine(), loc.getStartColumn())
121+
}
122+
123+
private predicate containsStmtStart(Stmt s) {
124+
this.containsStartOf(s.getLocation()) and
125+
// `Toplevel` statements are not contained within individual directives,
126+
// though their start location may appear within a directive location
127+
not s instanceof Toplevel
128+
}
129+
130+
/**
131+
* Gets a statement that starts in directive that is not a child of any other
132+
* statement starting in this directive.
133+
*/
134+
Stmt getAChildStmt() {
135+
this.containsStmtStart(result) and
136+
not this.containsStmtStart(result.getParent())
137+
}
138+
139+
/**
140+
* Gets the last child statement in this directive.
141+
* See `getAChildStmt` for more details.
142+
*/
143+
Stmt getTerminalStmt() {
144+
result = this.getAChildStmt() and
145+
forall(Stmt s | s = this.getAChildStmt() and not s = result |
146+
s.getLocation().strictlyBefore(result.getLocation())
147+
)
148+
}
149+
150+
/** Gets the child token of this directive. */
151+
ErbToken getToken() {
152+
exists(Erb::Directive g | this = fromGenerated(g) | toGenerated(result) = g.getChild())
153+
}
154+
155+
override string toString() { result = "erb directive" }
156+
157+
override string getAPrimaryQlClass() { result = "ErbDirective" }
158+
}
159+
160+
/**
161+
* A comment directive in an ERB template.
162+
* ```erb
163+
* <%#= 2 + 2 %>
164+
* <%# for x in xs do %>
165+
* ```
166+
*/
167+
class ErbCommentDirective extends ErbDirective {
168+
private Erb::CommentDirective g;
169+
170+
ErbCommentDirective() { this = TCommentDirective(g) }
171+
172+
override ErbComment getToken() { toGenerated(result) = g.getChild() }
173+
174+
final override string toString() { result = "<%#" + this.getToken().toString() + "%>" }
175+
176+
final override string getAPrimaryQlClass() { result = "ErbCommentDirective" }
177+
}
178+
179+
/**
180+
* A GraphQL directive in an ERB template.
181+
* ```erb
182+
* <%graphql
183+
* fragment Foo on Bar {
184+
* some {
185+
* queryText
186+
* moreProperties
187+
* }
188+
* }
189+
* %>
190+
* ```
191+
*/
192+
class ErbGraphqlDirective extends ErbDirective {
193+
private Erb::GraphqlDirective g;
194+
195+
ErbGraphqlDirective() { this = TGraphqlDirective(g) }
196+
197+
override ErbCode getToken() { toGenerated(result) = g.getChild() }
198+
199+
final override string toString() { result = "<%graphql" + this.getToken().toString() + "%>" }
200+
201+
final override string getAPrimaryQlClass() { result = "ErbGraphqlDirective" }
202+
}
203+
204+
/**
205+
* An output directive in an ERB template.
206+
* ```erb
207+
* <%=
208+
* fragment Foo on Bar {
209+
* some {
210+
* queryText
211+
* moreProperties
212+
* }
213+
* }
214+
* %>
215+
* ```
216+
*/
217+
class ErbOutputDirective extends ErbDirective {
218+
private Erb::OutputDirective g;
219+
220+
ErbOutputDirective() { this = TOutputDirective(g) }
221+
222+
override ErbCode getToken() { toGenerated(result) = g.getChild() }
223+
224+
final override string toString() { result = "<%=" + this.getToken().toString() + "%>" }
225+
226+
final override string getAPrimaryQlClass() { result = "ErbOutputDirective" }
227+
}
228+
229+
/**
230+
* An execution directive in an ERB template.
231+
* This code will be executed as Ruby, but not rendered.
232+
* ```erb
233+
* <% books = author.books
234+
* for book in books do %>
235+
* ```
236+
*/
237+
class ErbExecutionDirective extends ErbDirective {
238+
private Erb::Directive g;
239+
240+
ErbExecutionDirective() { this = TDirective(g) }
241+
242+
final override string toString() { result = "<%" + this.getToken().toString() + "%>" }
243+
244+
final override string getAPrimaryQlClass() { result = "ErbExecutionDirective" }
245+
}
246+
247+
/**
248+
* A `File` containing an Embedded Ruby template.
249+
* This is typically a file containing snippets of Ruby code that can be
250+
* evaluated to create a compiled version of the file.
251+
*/
252+
class ErbFile extends File {
253+
private ErbTemplate template;
254+
255+
ErbFile() { this = template.getLocation().getFile() }
256+
257+
/**
258+
* Holds if the file represents a partial to be rendered in the context of
259+
* another template.
260+
*/
261+
predicate isPartial() { this.getStem().charAt(0) = "_" }
262+
263+
/**
264+
* Gets the erb template contained within this file.
265+
*/
266+
ErbTemplate getTemplate() { result = template }
267+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import codeql.Locations
2+
private import TreeSitter
3+
private import codeql.ruby.ast.Erb
4+
5+
cached
6+
private module Cached {
7+
cached
8+
newtype TAstNode =
9+
TCommentDirective(Erb::CommentDirective g) or
10+
TDirective(Erb::Directive g) or
11+
TGraphqlDirective(Erb::GraphqlDirective g) or
12+
TOutputDirective(Erb::OutputDirective g) or
13+
TTemplate(Erb::Template g) or
14+
TToken(Erb::Token g) or
15+
TComment(Erb::Comment g) or
16+
TCode(Erb::Code g)
17+
18+
/**
19+
* Gets the underlying TreeSitter entity for a given erb AST node.
20+
*/
21+
cached
22+
Erb::AstNode toGenerated(ErbAstNode n) {
23+
n = TCommentDirective(result) or
24+
n = TDirective(result) or
25+
n = TGraphqlDirective(result) or
26+
n = TOutputDirective(result) or
27+
n = TTemplate(result) or
28+
n = TToken(result) or
29+
n = TComment(result) or
30+
n = TCode(result)
31+
}
32+
33+
cached
34+
Location getLocation(ErbAstNode n) { result = toGenerated(n).getLocation() }
35+
}
36+
37+
import Cached
38+
39+
TAstNode fromGenerated(Erb::AstNode n) { n = toGenerated(result) }
40+
41+
class TDirectiveNode = TCommentDirective or TDirective or TGraphqlDirective or TOutputDirective;
42+
43+
class TTokenNode = TToken or TComment or TCode;

ql/lib/codeql/ruby/ast/internal/Variable.qll

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,6 @@ private predicate scopeAssigns(Scope::Range scope, string name, Ruby::Identifier
100100
scope = scopeOf(i)
101101
}
102102

103-
/** Holds if location `one` starts strictly before location `two` */
104-
pragma[inline]
105-
private predicate strictlyBefore(Location one, Location two) {
106-
one.getStartLine() < two.getStartLine()
107-
or
108-
one.getStartLine() = two.getStartLine() and one.getStartColumn() < two.getStartColumn()
109-
}
110-
111103
cached
112104
private module Cached {
113105
cached
@@ -296,7 +288,7 @@ private module Cached {
296288
name = access.getValue()
297289
|
298290
variable.getDeclaringScope() = scopeOf(access) and
299-
not strictlyBefore(access.getLocation(), variable.getLocation()) and
291+
not access.getLocation().strictlyBefore(variable.getLocation()) and
300292
// In case of overlapping parameter names, later parameters should not
301293
// be considered accesses to the first parameter
302294
if parameterAssignment(_, _, access)
@@ -366,7 +358,7 @@ private predicate inherits(Scope::Range scope, string name, Scope::Range outer)
366358
or
367359
exists(Ruby::Identifier i |
368360
scopeAssigns(outer, name, i) and
369-
strictlyBefore(i.getLocation(), scope.getLocation())
361+
i.getLocation().strictlyBefore(scope.getLocation())
370362
)
371363
)
372364
or

ql/lib/codeql/ruby/frameworks/ActionController.qll

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@ class ActionControllerActionMethod extends Method, HTTP::Server::RequestHandler:
6868
* corresponding template file at
6969
* `<sourcePrefix>app/views/<subpath>/<method_name>.html.erb`.
7070
*/
71-
// TODO: result should be `ErbFile`
72-
File getDefaultTemplateFile() {
71+
ErbFile getDefaultTemplateFile() {
7372
controllerTemplatesFolder(this.getControllerClass(), result.getParentContainer()) and
7473
result.getBaseName() = this.getName() + ".html.erb"
7574
}
@@ -194,8 +193,7 @@ class ActionControllerHelperMethod extends Method {
194193
* mapped to a controller class in `app/controllers/foo/bar/baz_controller.rb`,
195194
* if such a controller class exists.
196195
*/
197-
// TODO: parameter should be `ErbFile`
198-
ActionControllerControllerClass getAssociatedControllerClass(File f) {
196+
ActionControllerControllerClass getAssociatedControllerClass(ErbFile f) {
199197
controllerTemplatesFolder(result, f.getParentContainer())
200198
}
201199

0 commit comments

Comments
 (0)