Skip to content

Commit c555736

Browse files
authored
Merge pull request #4 from agius/leaf-node-locatable
Leaf Node Locatable
2 parents e1e2c93 + 26ad666 commit c555736

File tree

11 files changed

+346
-7
lines changed

11 files changed

+346
-7
lines changed

.codeqlmanifest.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1-
{ "provide": [ "ql/src/qlpack.yml",
2-
"extractor/codeql-extractor.yml" ],
3-
"ignore": [ "the-extractor-which-needs-to-be-built" ] }
1+
{
2+
"provide": [
3+
"ql/src/qlpack.yml",
4+
"extractor/codeql-extractor.yml"
5+
],
6+
"ignore": [
7+
"the-extractor-which-needs-to-be-built"
8+
]
9+
}

.github/workflows/continuous-integration.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
bundle config path vendor/bundle
8181
bundle install --jobs 4 --retry 3
8282
83-
- name: Build & run specs
83+
- name: Build & run Ruby specs
8484
env:
8585
CODEQL_PATH: "$GITHUB_WORKSPACE/codeql/codeql"
8686
working-directory: ./codeql-ruby

lib/codeql_ruby/extractor_file.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'forwardable'
2+
require 'pathname'
23

34
module CodeqlRuby
45
class ExtractorFile

ql/src/Files.qll

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/**
2+
* Provides classes for working with files and folders.
3+
*
4+
* Stolen liberally from the Javascript QLL
5+
* https://github.com/github/codeql/blob/813d14791d6bea399bc96fa9b7143603eef6e6c4/javascript/ql/src/semmle/javascript/Files.qll
6+
*
7+
*/
8+
9+
import ruby
10+
11+
/** A file or folder. */
12+
abstract class Container extends @container {
13+
/**
14+
* Gets the absolute, canonical path of this container, using forward slashes
15+
* as path separator.
16+
*
17+
* The path starts with a _root prefix_ followed by zero or more _path
18+
* segments_ separated by forward slashes.
19+
*
20+
* The root prefix is of one of the following forms:
21+
*
22+
* 1. A single forward slash `/` (Unix-style)
23+
* 2. An upper-case drive letter followed by a colon and a forward slash,
24+
* such as `C:/` (Windows-style)
25+
* 3. Two forward slashes, a computer name, and then another forward slash,
26+
* such as `//FileServer/` (UNC-style)
27+
*
28+
* Path segments are never empty (that is, absolute paths never contain two
29+
* contiguous slashes, except as part of a UNC-style root prefix). Also, path
30+
* segments never contain forward slashes, and no path segment is of the
31+
* form `.` (one dot) or `..` (two dots).
32+
*
33+
* Note that an absolute path never ends with a forward slash, except if it is
34+
* a bare root prefix, that is, the path has no path segments. A container
35+
* whose absolute path has no segments is always a `Folder`, not a `File`.
36+
*/
37+
abstract string getAbsolutePath();
38+
39+
/**
40+
* Gets a URL representing the location of this container.
41+
*
42+
* For more information see [Providing URLs](https://help.semmle.com/QL/learn-ql/ql/locations.html#providing-urls).
43+
*/
44+
abstract string getURL();
45+
46+
/**
47+
* Gets the relative path of this file or folder from the root folder of the
48+
* analyzed source location. The relative path of the root folder itself is
49+
* the empty string.
50+
*
51+
* This has no result if the container is outside the source root, that is,
52+
* if the root folder is not a reflexive, transitive parent of this container.
53+
*/
54+
string getRelativePath() {
55+
exists(string absPath, string pref |
56+
absPath = getAbsolutePath() and sourceLocationPrefix(pref)
57+
|
58+
absPath = pref and result = ""
59+
or
60+
absPath = pref.regexpReplaceAll("/$", "") + "/" + result and
61+
not result.matches("/%")
62+
)
63+
}
64+
65+
/**
66+
* Gets the base name of this container including extension, that is, the last
67+
* segment of its absolute path, or the empty string if it has no segments.
68+
*
69+
* Here are some examples of absolute paths and the corresponding base names
70+
* (surrounded with quotes to avoid ambiguity):
71+
*
72+
* <table border="1">
73+
* <tr><th>Absolute path</th><th>Base name</th></tr>
74+
* <tr><td>"/tmp/tst.js"</td><td>"tst.js"</td></tr>
75+
* <tr><td>"C:/Program Files (x86)"</td><td>"Program Files (x86)"</td></tr>
76+
* <tr><td>"/"</td><td>""</td></tr>
77+
* <tr><td>"C:/"</td><td>""</td></tr>
78+
* <tr><td>"D:/"</td><td>""</td></tr>
79+
* <tr><td>"//FileServer/"</td><td>""</td></tr>
80+
* </table>
81+
*/
82+
string getBaseName() { result = getAbsolutePath().regexpCapture(".*/(([^/]*?)(\\.([^.]*))?)", 1) }
83+
84+
/**
85+
* Gets the extension of this container, that is, the suffix of its base name
86+
* after the last dot character, if any.
87+
*
88+
* In particular,
89+
*
90+
* - if the name does not include a dot, there is no extension, so this
91+
* predicate has no result;
92+
* - if the name ends in a dot, the extension is the empty string;
93+
* - if the name contains multiple dots, the extension follows the last dot.
94+
*
95+
* Here are some examples of absolute paths and the corresponding extensions
96+
* (surrounded with quotes to avoid ambiguity):
97+
*
98+
* <table border="1">
99+
* <tr><th>Absolute path</th><th>Extension</th></tr>
100+
* <tr><td>"/tmp/tst.js"</td><td>"js"</td></tr>
101+
* <tr><td>"/tmp/.classpath"</td><td>"classpath"</td></tr>
102+
* <tr><td>"/bin/bash"</td><td>not defined</td></tr>
103+
* <tr><td>"/tmp/tst2."</td><td>""</td></tr>
104+
* <tr><td>"/tmp/x.tar.gz"</td><td>"gz"</td></tr>
105+
* </table>
106+
*/
107+
string getExtension() {
108+
result = getAbsolutePath().regexpCapture(".*/(([^/]*?)(\\.([^.]*))?)", 4)
109+
}
110+
111+
/**
112+
* Gets the stem of this container, that is, the prefix of its base name up to
113+
* (but not including) the last dot character if there is one, or the entire
114+
* base name if there is not.
115+
*
116+
* Here are some examples of absolute paths and the corresponding stems
117+
* (surrounded with quotes to avoid ambiguity):
118+
*
119+
* <table border="1">
120+
* <tr><th>Absolute path</th><th>Stem</th></tr>
121+
* <tr><td>"/tmp/tst.js"</td><td>"tst"</td></tr>
122+
* <tr><td>"/tmp/.classpath"</td><td>""</td></tr>
123+
* <tr><td>"/bin/bash"</td><td>"bash"</td></tr>
124+
* <tr><td>"/tmp/tst2."</td><td>"tst2"</td></tr>
125+
* <tr><td>"/tmp/x.tar.gz"</td><td>"x.tar"</td></tr>
126+
* </table>
127+
*/
128+
string getStem() { result = getAbsolutePath().regexpCapture(".*/(([^/]*?)(\\.([^.]*))?)", 2) }
129+
130+
/** Gets the parent container of this file or folder, if any. */
131+
Container getParentContainer() { containerparent(result, this) }
132+
133+
/** Gets a file or sub-folder in this container. */
134+
Container getAChildContainer() { this = result.getParentContainer() }
135+
136+
/** Gets a file in this container. */
137+
File getAFile() { result = getAChildContainer() }
138+
139+
/** Gets the file in this container that has the given `baseName`, if any. */
140+
File getFile(string baseName) {
141+
result = getAFile() and
142+
result.getBaseName() = baseName
143+
}
144+
145+
/** Gets a sub-folder in this container. */
146+
Folder getAFolder() { result = getAChildContainer() }
147+
148+
/** Gets the sub-folder in this container that has the given `baseName`, if any. */
149+
Folder getFolder(string baseName) {
150+
result = getAFolder() and
151+
result.getBaseName() = baseName
152+
}
153+
154+
/**
155+
* Gets a textual representation of the path of this container.
156+
*
157+
* This is the absolute path of the container.
158+
*/
159+
string toString() { result = getAbsolutePath() }
160+
}
161+
162+
/** A folder. */
163+
class Folder extends Container, @folder {
164+
override string getAbsolutePath() { folders(this, result, _) }
165+
166+
/** Gets the file or subfolder in this folder that has the given `name`, if any. */
167+
Container getChildContainer(string name) {
168+
result = getAChildContainer() and
169+
result.getBaseName() = name
170+
}
171+
172+
/** Gets the file in this folder that has the given `stem` and `extension`, if any. */
173+
File getFile(string stem, string extension) {
174+
result = getAChildContainer() and
175+
result.getStem() = stem and
176+
result.getExtension() = extension
177+
}
178+
179+
/** Gets a subfolder contained in this folder. */
180+
Folder getASubFolder() { result = getAChildContainer() }
181+
182+
/** Gets the URL of this folder. */
183+
override string getURL() { result = "folder://" + getAbsolutePath() }
184+
}
185+
186+
/** A file. */
187+
class File extends Container, @file {
188+
override string getAbsolutePath() { files(this, result, _, _, _) }
189+
190+
/** Gets the number of lines in this file. */
191+
int getNumberOfLines() { result = sum(int loc | numlines(this, loc, _, _) | loc) }
192+
193+
/** Gets the number of lines containing code in this file. */
194+
int getNumberOfLinesOfCode() { result = sum(int loc | numlines(this, _, loc, _) | loc) }
195+
196+
/** Gets the number of lines containing comments in this file. */
197+
int getNumberOfLinesOfComments() { result = sum(int loc | numlines(this, _, _, loc) | loc) }
198+
199+
override string toString() { result = Container.super.toString() }
200+
201+
/** Gets the URL of this file. */
202+
override string getURL() { result = "file://" + this.getAbsolutePath() + ":0:0:0:0" }
203+
}

ql/src/LeafNode.qll

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import ruby
1414
* 1
1515
* ```
1616
*/
17-
class LeafNode extends @leaf_node {
17+
class LeafNode extends @leaf_node, Locatable {
1818
string getText() { leaf_nodes(this, result, _, _) }
1919

20-
string toString() { result = "LeafNode" }
20+
override Location getLocation() { has_location(this, result) }
21+
22+
override string toString() { result = "LeafNode" }
2123
}

ql/src/Locations.qll

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Provides classes for working with locations and program elements that have locations.
3+
*
4+
* Stolen liberally from the Javascript QL library:
5+
* https://github.com/github/codeql/blob/813d14791d6bea399bc96fa9b7143603eef6e6c4/javascript/ql/src/semmle/javascript/Locations.qll
6+
*
7+
*/
8+
9+
import ruby
10+
11+
/**
12+
* A location as given by a file, a start line, a start column,
13+
* an end line, and an end column.
14+
*
15+
* For more information about locations see [Locations](https://help.semmle.com/QL/learn-ql/ql/locations.html).
16+
*/
17+
class Location extends @location {
18+
/** Gets the file for this location. */
19+
File getFile() { locations_default(this, result, _, _, _, _) }
20+
21+
/** Gets the 1-based line number (inclusive) where this location starts. */
22+
int getStartLine() { locations_default(this, _, result, _, _, _) }
23+
24+
/** Gets the 1-based column number (inclusive) where this location starts. */
25+
int getStartColumn() { locations_default(this, _, _, result, _, _) }
26+
27+
/** Gets the 1-based line number (inclusive) where this location ends. */
28+
int getEndLine() { locations_default(this, _, _, _, result, _) }
29+
30+
/** Gets the 1-based column number (inclusive) where this location ends. */
31+
int getEndColumn() { locations_default(this, _, _, _, _, result) }
32+
33+
/** Gets the number of lines covered by this location. */
34+
int getNumLines() { result = getEndLine() - getStartLine() + 1 }
35+
36+
/** Holds if this location starts before location `that`. */
37+
pragma[inline]
38+
predicate startsBefore(Location that) {
39+
exists(File f, int sl1, int sc1, int sl2, int sc2 |
40+
locations_default(this, f, sl1, sc1, _, _) and
41+
locations_default(that, f, sl2, sc2, _, _)
42+
|
43+
sl1 < sl2
44+
or
45+
sl1 = sl2 and sc1 < sc2
46+
)
47+
}
48+
49+
/** Holds if this location ends after location `that`. */
50+
pragma[inline]
51+
predicate endsAfter(Location that) {
52+
exists(File f, int el1, int ec1, int el2, int ec2 |
53+
locations_default(this, f, _, _, el1, ec1) and
54+
locations_default(that, f, _, _, el2, ec2)
55+
|
56+
el1 > el2
57+
or
58+
el1 = el2 and ec1 > ec2
59+
)
60+
}
61+
62+
/**
63+
* Holds if this location contains location `that`, meaning that it starts
64+
* before and ends after it.
65+
*/
66+
predicate contains(Location that) { this.startsBefore(that) and this.endsAfter(that) }
67+
68+
/** Holds if this location is empty. */
69+
predicate isEmpty() { exists(int l, int c | locations_default(this, _, l, c, l, c - 1)) }
70+
71+
/** Gets a textual representation of this element. */
72+
string toString() { result = this.getFile().getBaseName() + ":" + this.getStartLine().toString() }
73+
74+
/**
75+
* Holds if this element is at the specified location.
76+
* The location spans column `startcolumn` of line `startline` to
77+
* column `endcolumn` of line `endline` in file `filepath`.
78+
* For more information, see
79+
* [Locations](https://help.semmle.com/QL/learn-ql/ql/locations.html).
80+
*/
81+
predicate hasLocationInfo(
82+
string filepath, int startline, int startcolumn, int endline, int endcolumn
83+
) {
84+
exists(File f |
85+
locations_default(this, f, startline, startcolumn, endline, endcolumn) and
86+
filepath = f.getAbsolutePath()
87+
)
88+
}
89+
}
90+
91+
/** A program element with a location. */
92+
class Locatable extends @locatable {
93+
/** Gets the file this program element comes from. */
94+
File getFile() { result = getLocation().getFile() }
95+
96+
/** Gets this element's location. */
97+
Location getLocation() {
98+
// overridden by subclasses
99+
none()
100+
}
101+
102+
/** Gets the number of lines covered by this element. */
103+
int getNumLines() { result = getLocation().getNumLines() }
104+
105+
/** Gets a textual representation of this element. */
106+
string toString() {
107+
// to be overridden by subclasses
108+
none()
109+
}
110+
}

ql/src/ruby.qll

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11

2-
import LeafNode
2+
import Files
3+
import Locations
4+
import LeafNode

spec/codeql_ruby_spec.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,11 @@
2626

2727
expect(results).to be_a(String)
2828
end
29+
30+
it "extracts Location info from LeafNodes" do
31+
results = CodeqlRunner.results_for_db('leaf_node_location')
32+
tuples = results.dig('#select', 'tuples')
33+
34+
expect(tuples).to include([{'label'=>'LeafNode'}, 'puts', 'leaf_node_location.rb:1'])
35+
end
2936
end

spec/leaf_node_location/example.ql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import ruby
2+
3+
from LeafNode n
4+
select n, n.getText(), n.getLocation().toString()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
puts 'this file contains some leaf nodes'

0 commit comments

Comments
 (0)