Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .codeqlmanifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
{ "provide": [ "ql/src/qlpack.yml",
"extractor/codeql-extractor.yml" ],
"ignore": [ "the-extractor-which-needs-to-be-built" ] }
{
"provide": [
"ql/src/qlpack.yml",
"extractor/codeql-extractor.yml"
],
"ignore": [
"the-extractor-which-needs-to-be-built"
]
}
2 changes: 1 addition & 1 deletion .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3

- name: Build & run specs
- name: Build & run Ruby specs
env:
CODEQL_PATH: "$GITHUB_WORKSPACE/codeql/codeql"
working-directory: ./codeql-ruby
Expand Down
1 change: 1 addition & 0 deletions lib/codeql_ruby/extractor_file.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'forwardable'
require 'pathname'

module CodeqlRuby
class ExtractorFile
Expand Down
203 changes: 203 additions & 0 deletions ql/src/Files.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/**
* Provides classes for working with files and folders.
*
* Stolen liberally from the Javascript QLL
* https://github.com/github/codeql/blob/813d14791d6bea399bc96fa9b7143603eef6e6c4/javascript/ql/src/semmle/javascript/Files.qll
*
*/

import ruby

/** A file or folder. */
abstract class Container extends @container {
/**
* Gets the absolute, canonical path of this container, using forward slashes
* as path separator.
*
* The path starts with a _root prefix_ followed by zero or more _path
* segments_ separated by forward slashes.
*
* The root prefix is of one of the following forms:
*
* 1. A single forward slash `/` (Unix-style)
* 2. An upper-case drive letter followed by a colon and a forward slash,
* such as `C:/` (Windows-style)
* 3. Two forward slashes, a computer name, and then another forward slash,
* such as `//FileServer/` (UNC-style)
*
* Path segments are never empty (that is, absolute paths never contain two
* contiguous slashes, except as part of a UNC-style root prefix). Also, path
* segments never contain forward slashes, and no path segment is of the
* form `.` (one dot) or `..` (two dots).
*
* Note that an absolute path never ends with a forward slash, except if it is
* a bare root prefix, that is, the path has no path segments. A container
* whose absolute path has no segments is always a `Folder`, not a `File`.
*/
abstract string getAbsolutePath();

/**
* Gets a URL representing the location of this container.
*
* For more information see [Providing URLs](https://help.semmle.com/QL/learn-ql/ql/locations.html#providing-urls).
*/
abstract string getURL();

/**
* Gets the relative path of this file or folder from the root folder of the
* analyzed source location. The relative path of the root folder itself is
* the empty string.
*
* This has no result if the container is outside the source root, that is,
* if the root folder is not a reflexive, transitive parent of this container.
*/
string getRelativePath() {
exists(string absPath, string pref |
absPath = getAbsolutePath() and sourceLocationPrefix(pref)
|
absPath = pref and result = ""
or
absPath = pref.regexpReplaceAll("/$", "") + "/" + result and
not result.matches("/%")
)
}

/**
* Gets the base name of this container including extension, that is, the last
* segment of its absolute path, or the empty string if it has no segments.
*
* Here are some examples of absolute paths and the corresponding base names
* (surrounded with quotes to avoid ambiguity):
*
* <table border="1">
* <tr><th>Absolute path</th><th>Base name</th></tr>
* <tr><td>"/tmp/tst.js"</td><td>"tst.js"</td></tr>
* <tr><td>"C:/Program Files (x86)"</td><td>"Program Files (x86)"</td></tr>
* <tr><td>"/"</td><td>""</td></tr>
* <tr><td>"C:/"</td><td>""</td></tr>
* <tr><td>"D:/"</td><td>""</td></tr>
* <tr><td>"//FileServer/"</td><td>""</td></tr>
* </table>
*/
string getBaseName() { result = getAbsolutePath().regexpCapture(".*/(([^/]*?)(\\.([^.]*))?)", 1) }

/**
* Gets the extension of this container, that is, the suffix of its base name
* after the last dot character, if any.
*
* In particular,
*
* - if the name does not include a dot, there is no extension, so this
* predicate has no result;
* - if the name ends in a dot, the extension is the empty string;
* - if the name contains multiple dots, the extension follows the last dot.
*
* Here are some examples of absolute paths and the corresponding extensions
* (surrounded with quotes to avoid ambiguity):
*
* <table border="1">
* <tr><th>Absolute path</th><th>Extension</th></tr>
* <tr><td>"/tmp/tst.js"</td><td>"js"</td></tr>
* <tr><td>"/tmp/.classpath"</td><td>"classpath"</td></tr>
* <tr><td>"/bin/bash"</td><td>not defined</td></tr>
* <tr><td>"/tmp/tst2."</td><td>""</td></tr>
* <tr><td>"/tmp/x.tar.gz"</td><td>"gz"</td></tr>
* </table>
*/
string getExtension() {
result = getAbsolutePath().regexpCapture(".*/(([^/]*?)(\\.([^.]*))?)", 4)
}

/**
* Gets the stem of this container, that is, the prefix of its base name up to
* (but not including) the last dot character if there is one, or the entire
* base name if there is not.
*
* Here are some examples of absolute paths and the corresponding stems
* (surrounded with quotes to avoid ambiguity):
*
* <table border="1">
* <tr><th>Absolute path</th><th>Stem</th></tr>
* <tr><td>"/tmp/tst.js"</td><td>"tst"</td></tr>
* <tr><td>"/tmp/.classpath"</td><td>""</td></tr>
* <tr><td>"/bin/bash"</td><td>"bash"</td></tr>
* <tr><td>"/tmp/tst2."</td><td>"tst2"</td></tr>
* <tr><td>"/tmp/x.tar.gz"</td><td>"x.tar"</td></tr>
* </table>
*/
string getStem() { result = getAbsolutePath().regexpCapture(".*/(([^/]*?)(\\.([^.]*))?)", 2) }

/** Gets the parent container of this file or folder, if any. */
Container getParentContainer() { containerparent(result, this) }

/** Gets a file or sub-folder in this container. */
Container getAChildContainer() { this = result.getParentContainer() }

/** Gets a file in this container. */
File getAFile() { result = getAChildContainer() }

/** Gets the file in this container that has the given `baseName`, if any. */
File getFile(string baseName) {
result = getAFile() and
result.getBaseName() = baseName
}

/** Gets a sub-folder in this container. */
Folder getAFolder() { result = getAChildContainer() }

/** Gets the sub-folder in this container that has the given `baseName`, if any. */
Folder getFolder(string baseName) {
result = getAFolder() and
result.getBaseName() = baseName
}

/**
* Gets a textual representation of the path of this container.
*
* This is the absolute path of the container.
*/
string toString() { result = getAbsolutePath() }
}

/** A folder. */
class Folder extends Container, @folder {
override string getAbsolutePath() { folders(this, result, _) }

/** Gets the file or subfolder in this folder that has the given `name`, if any. */
Container getChildContainer(string name) {
result = getAChildContainer() and
result.getBaseName() = name
}

/** Gets the file in this folder that has the given `stem` and `extension`, if any. */
File getFile(string stem, string extension) {
result = getAChildContainer() and
result.getStem() = stem and
result.getExtension() = extension
}

/** Gets a subfolder contained in this folder. */
Folder getASubFolder() { result = getAChildContainer() }

/** Gets the URL of this folder. */
override string getURL() { result = "folder://" + getAbsolutePath() }
}

/** A file. */
class File extends Container, @file {
override string getAbsolutePath() { files(this, result, _, _, _) }

/** Gets the number of lines in this file. */
int getNumberOfLines() { result = sum(int loc | numlines(this, loc, _, _) | loc) }

/** Gets the number of lines containing code in this file. */
int getNumberOfLinesOfCode() { result = sum(int loc | numlines(this, _, loc, _) | loc) }

/** Gets the number of lines containing comments in this file. */
int getNumberOfLinesOfComments() { result = sum(int loc | numlines(this, _, _, loc) | loc) }

override string toString() { result = Container.super.toString() }

/** Gets the URL of this file. */
override string getURL() { result = "file://" + this.getAbsolutePath() + ":0:0:0:0" }
}
6 changes: 4 additions & 2 deletions ql/src/LeafNode.qll
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import ruby
* 1
* ```
*/
class LeafNode extends @leaf_node {
class LeafNode extends @leaf_node, Locatable {
string getText() { leaf_nodes(this, result, _, _) }

string toString() { result = "LeafNode" }
override Location getLocation() { has_location(this, result) }

override string toString() { result = "LeafNode" }
}
110 changes: 110 additions & 0 deletions ql/src/Locations.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Provides classes for working with locations and program elements that have locations.
*
* Stolen liberally from the Javascript QL library:
* https://github.com/github/codeql/blob/813d14791d6bea399bc96fa9b7143603eef6e6c4/javascript/ql/src/semmle/javascript/Locations.qll
*
*/

import ruby

/**
* A location as given by a file, a start line, a start column,
* an end line, and an end column.
*
* For more information about locations see [Locations](https://help.semmle.com/QL/learn-ql/ql/locations.html).
*/
class Location extends @location {
/** Gets the file for this location. */
File getFile() { locations_default(this, result, _, _, _, _) }

/** Gets the 1-based line number (inclusive) where this location starts. */
int getStartLine() { locations_default(this, _, result, _, _, _) }

/** Gets the 1-based column number (inclusive) where this location starts. */
int getStartColumn() { locations_default(this, _, _, result, _, _) }

/** Gets the 1-based line number (inclusive) where this location ends. */
int getEndLine() { locations_default(this, _, _, _, result, _) }

/** Gets the 1-based column number (inclusive) where this location ends. */
int getEndColumn() { locations_default(this, _, _, _, _, result) }

/** Gets the number of lines covered by this location. */
int getNumLines() { result = getEndLine() - getStartLine() + 1 }

/** Holds if this location starts before location `that`. */
pragma[inline]
predicate startsBefore(Location that) {
exists(File f, int sl1, int sc1, int sl2, int sc2 |
locations_default(this, f, sl1, sc1, _, _) and
locations_default(that, f, sl2, sc2, _, _)
|
sl1 < sl2
or
sl1 = sl2 and sc1 < sc2
)
}

/** Holds if this location ends after location `that`. */
pragma[inline]
predicate endsAfter(Location that) {
exists(File f, int el1, int ec1, int el2, int ec2 |
locations_default(this, f, _, _, el1, ec1) and
locations_default(that, f, _, _, el2, ec2)
|
el1 > el2
or
el1 = el2 and ec1 > ec2
)
}

/**
* Holds if this location contains location `that`, meaning that it starts
* before and ends after it.
*/
predicate contains(Location that) { this.startsBefore(that) and this.endsAfter(that) }

/** Holds if this location is empty. */
predicate isEmpty() { exists(int l, int c | locations_default(this, _, l, c, l, c - 1)) }

/** Gets a textual representation of this element. */
string toString() { result = this.getFile().getBaseName() + ":" + this.getStartLine().toString() }

/**
* Holds if this element is at the specified location.
* The location spans column `startcolumn` of line `startline` to
* column `endcolumn` of line `endline` in file `filepath`.
* For more information, see
* [Locations](https://help.semmle.com/QL/learn-ql/ql/locations.html).
*/
predicate hasLocationInfo(
string filepath, int startline, int startcolumn, int endline, int endcolumn
) {
exists(File f |
locations_default(this, f, startline, startcolumn, endline, endcolumn) and
filepath = f.getAbsolutePath()
)
}
}

/** A program element with a location. */
class Locatable extends @locatable {
/** Gets the file this program element comes from. */
File getFile() { result = getLocation().getFile() }

/** Gets this element's location. */
Location getLocation() {
// overridden by subclasses
none()
}

/** Gets the number of lines covered by this element. */
int getNumLines() { result = getLocation().getNumLines() }

/** Gets a textual representation of this element. */
string toString() {
// to be overridden by subclasses
none()
}
}
4 changes: 3 additions & 1 deletion ql/src/ruby.qll
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@

import LeafNode
import Files
import Locations
import LeafNode
7 changes: 7 additions & 0 deletions spec/codeql_ruby_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,11 @@

expect(results).to be_a(String)
end

it "extracts Location info from LeafNodes" do
results = CodeqlRunner.results_for_db('leaf_node_location')
tuples = results.dig('#select', 'tuples')

expect(tuples).to include([{'label'=>'LeafNode'}, 'puts', 'leaf_node_location.rb:1'])
end
end
4 changes: 4 additions & 0 deletions spec/leaf_node_location/example.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import ruby

from LeafNode n
select n, n.getText(), n.getLocation().toString()
1 change: 1 addition & 0 deletions spec/leaf_node_location/leaf_node_location.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
puts 'this file contains some leaf nodes'
3 changes: 3 additions & 0 deletions spec/leaf_node_location/qlpack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: leaf-node-location-ruby-queries
version: 0.0.0
libraryPathDependencies: codeql-ruby