Skip to content

Commit 8df5aaa

Browse files
committed
Ruby: Model private class methods
`Module#private_class_method` takes a symbol representing the name of a method in the current module scope and makes that module private. This is similar to `private`, but applies only to class (singleton) methods. Unlike `private`, it must be called with an argument, and does not change the ambient visibility for any subsequent method definitions. class Foo def public end def private1 end private_class_method :private1 # This alternate form works because method definition # returns its name as a symbol: private_class_method def private2 end end
1 parent e811ba1 commit 8df5aaa

File tree

4 files changed

+90
-47
lines changed

4 files changed

+90
-47
lines changed

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

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,37 @@ class MethodBase extends Callable, BodyStmt, Scope, TMethodBase {
3535
or
3636
result = BodyStmt.super.getAChild(pred)
3737
}
38-
}
3938

40-
/** A call to `private`. */
41-
private class Private extends MethodCall {
42-
Private() { this.getMethodName() = "private" }
39+
/** Holds if this method is private. */
40+
predicate isPrivate() { none() }
41+
}
4342

44-
/** Gets the method that this `private` call applies to, if any */
45-
Expr getMethod() { result = this.getArgument(0) }
43+
/**
44+
* A method call which modifies another method in some way.
45+
* For example, `private :foo` makes the method `foo` private.
46+
*/
47+
private class MethodModifier extends MethodCall {
48+
/** Gets the name of the method that this call applies to. */
49+
Expr getMethodArgument() { result = this.getArgument(0) }
4650

47-
/**
48-
* Holds if this `private` call happens inside `c`, and refers to a
49-
* method named `name`.
50-
*/
51-
pragma[noinline]
52-
predicate isRef(Namespace c, string name) {
53-
this = c.getAStmt() and
54-
name = this.getMethod().(SymbolLiteral).getValueText()
51+
/** Gets the method that this call applies to. */
52+
MethodBase getMethod() {
53+
result = this.getMethodArgument()
54+
or
55+
exists(Namespace n |
56+
n.getAStmt() = this and
57+
n.getAStmt() = result and
58+
result.getName() = this.getMethodArgument().(StringlikeLiteral).getValueText()
59+
)
5560
}
61+
}
62+
63+
/** A call to `private` or `private_class_method`. */
64+
private class Private extends MethodModifier {
65+
Private() { this.getMethodName() = "private" }
5666

5767
/**
58-
* Holds if this `private` call happens at position `i` inside `c`,
68+
* Holds if this call happens at position `i` inside `c`,
5969
* and the call has no arguments.
6070
*/
6171
pragma[noinline]
@@ -65,6 +75,11 @@ private class Private extends MethodCall {
6575
}
6676
}
6777

78+
/** A call to `private_class_method`. */
79+
private class PrivateClassMethod extends MethodModifier {
80+
PrivateClassMethod() { this.getMethodName() = "private_class_method" }
81+
}
82+
6883
/** A normal method. */
6984
class Method extends MethodBase, TMethod {
7085
private Ruby::Method g;
@@ -90,12 +105,6 @@ class Method extends MethodBase, TMethod {
90105
*/
91106
final predicate isSetter() { g.getName() instanceof Ruby::Setter }
92107

93-
pragma[noinline]
94-
private predicate isDeclaredIn(Namespace c, string name) {
95-
this = c.getAStmt() and
96-
name = this.getName()
97-
}
98-
99108
/**
100109
* Holds if this method is private. All methods with the name prefix
101110
* `private` are private below:
@@ -122,14 +131,9 @@ class Method extends MethodBase, TMethod {
122131
* end
123132
* ```
124133
*/
125-
predicate isPrivate() {
134+
override predicate isPrivate() {
126135
this = any(Private p).getMethod()
127136
or
128-
exists(Namespace c, Private p, string name |
129-
this.isDeclaredIn(c, name) and
130-
p.isRef(c, name)
131-
)
132-
or
133137
exists(Namespace c, Private p, int i, int j |
134138
p.hasNoArg(c, i) and
135139
this = c.getStmt(j) and
@@ -175,6 +179,31 @@ class SingletonMethod extends MethodBase, TSingletonMethod {
175179
or
176180
pred = "getObject" and result = this.getObject()
177181
}
182+
183+
/**
184+
* Holds if this method is private. All methods with the name prefix
185+
* `private` are private below:
186+
*
187+
* ```rb
188+
* class C
189+
* private_class_method def self.private1
190+
* end
191+
*
192+
* def self.public
193+
* end
194+
*
195+
* def self.private2
196+
* end
197+
* private_class_method :private2
198+
*
199+
* private # this has no effect on singleton methods
200+
*
201+
* def self.public2
202+
* end
203+
* end
204+
* ```
205+
*/
206+
override predicate isPrivate() { this = any(PrivateClassMethod p).getMethod() }
178207
}
179208

180209
/**

ruby/ql/test/library-tests/modules/callgraph.expected

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,16 @@ getTarget
7878
| private.rb:2:3:3:5 | call to private | calls.rb:94:5:94:20 | private |
7979
| private.rb:10:3:10:19 | call to private | calls.rb:94:5:94:20 | private |
8080
| private.rb:12:3:12:9 | call to private | calls.rb:94:5:94:20 | private |
81-
| private.rb:24:1:24:5 | call to new | calls.rb:99:5:99:16 | new |
82-
| private.rb:25:1:25:5 | call to new | calls.rb:99:5:99:16 | new |
83-
| private.rb:26:1:26:5 | call to new | calls.rb:99:5:99:16 | new |
84-
| private.rb:27:1:27:5 | call to new | calls.rb:99:5:99:16 | new |
85-
| private.rb:28:1:28:5 | call to new | calls.rb:99:5:99:16 | new |
86-
| private.rb:28:1:28:12 | call to public | private.rb:5:3:6:5 | public |
87-
| private.rb:30:1:30:15 | call to private_on_main | private.rb:21:1:22:3 | private_on_main |
88-
| private.rb:33:3:34:5 | call to private | calls.rb:94:5:94:20 | private |
89-
| private.rb:41:3:41:19 | call to private | calls.rb:94:5:94:20 | private |
90-
| private.rb:43:3:43:9 | call to private | calls.rb:94:5:94:20 | private |
81+
| private.rb:34:1:34:5 | call to new | calls.rb:99:5:99:16 | new |
82+
| private.rb:35:1:35:5 | call to new | calls.rb:99:5:99:16 | new |
83+
| private.rb:36:1:36:5 | call to new | calls.rb:99:5:99:16 | new |
84+
| private.rb:37:1:37:5 | call to new | calls.rb:99:5:99:16 | new |
85+
| private.rb:38:1:38:5 | call to new | calls.rb:99:5:99:16 | new |
86+
| private.rb:38:1:38:12 | call to public | private.rb:5:3:6:5 | public |
87+
| private.rb:40:1:40:15 | call to private_on_main | private.rb:31:1:32:3 | private_on_main |
88+
| private.rb:43:3:44:5 | call to private | calls.rb:94:5:94:20 | private |
89+
| private.rb:51:3:51:19 | call to private | calls.rb:94:5:94:20 | private |
90+
| private.rb:53:3:53:9 | call to private | calls.rb:94:5:94:20 | private |
9191
unresolvedCall
9292
| calls.rb:19:5:19:14 | call to instance_m |
9393
| calls.rb:20:5:20:19 | call to instance_m |
@@ -113,10 +113,12 @@ unresolvedCall
113113
| hello.rb:20:16:20:26 | ... + ... |
114114
| hello.rb:20:16:20:34 | ... + ... |
115115
| hello.rb:20:16:20:40 | ... + ... |
116-
| private.rb:24:1:24:14 | call to private1 |
117-
| private.rb:25:1:25:14 | call to private2 |
118-
| private.rb:26:1:26:14 | call to private3 |
119-
| private.rb:27:1:27:14 | call to private4 |
116+
| private.rb:23:3:24:5 | call to private_class_method |
117+
| private.rb:28:3:28:32 | call to private_class_method |
118+
| private.rb:34:1:34:14 | call to private1 |
119+
| private.rb:35:1:35:14 | call to private2 |
120+
| private.rb:36:1:36:14 | call to private3 |
121+
| private.rb:37:1:37:14 | call to private4 |
120122
privateMethod
121123
| calls.rb:1:1:3:3 | foo |
122124
| calls.rb:62:1:65:3 | optional_arg |
@@ -129,8 +131,10 @@ privateMethod
129131
| private.rb:8:3:9:5 | private2 |
130132
| private.rb:14:3:15:5 | private3 |
131133
| private.rb:17:3:18:5 | private4 |
132-
| private.rb:21:1:22:3 | private_on_main |
133-
| private.rb:33:11:34:5 | private1 |
134-
| private.rb:39:3:40:5 | private2 |
135-
| private.rb:45:3:46:5 | private3 |
136-
| private.rb:48:3:49:5 | private4 |
134+
| private.rb:23:24:24:5 | private5 |
135+
| private.rb:26:3:27:5 | private6 |
136+
| private.rb:31:1:32:3 | private_on_main |
137+
| private.rb:43:11:44:5 | private1 |
138+
| private.rb:49:3:50:5 | private2 |
139+
| private.rb:55:3:56:5 | private3 |
140+
| private.rb:58:3:59:5 | private4 |

ruby/ql/test/library-tests/modules/callgraph.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ query Callable getTarget(Call call) { result = call.getATarget() }
44

55
query predicate unresolvedCall(Call call) { not exists(call.getATarget()) }
66

7-
query predicate privateMethod(Method m) { m.isPrivate() }
7+
query predicate privateMethod(MethodBase m) { m.isPrivate() }

ruby/ql/test/library-tests/modules/private.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ def private3
1616

1717
def private4
1818
end
19+
20+
def self.public2
21+
end
22+
23+
private_class_method def self.private5
24+
end
25+
26+
def self.private6
27+
end
28+
private_class_method :private6
1929
end
2030

2131
def private_on_main

0 commit comments

Comments
 (0)