blob: 3dd0ba082e5418ce0cfdff407257c2ebe11c17f9 (
plain)
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
|
Compile-time user checks
========================
It would be nice to be able to add custom checks, that get enforced
by the compiler. For example:
* Value conditions:
- "If parameter 'a' is non-none, then parameter 'b' must also be non-none"
* Local AST conditions:
- "Parameter 'a' must come from 'get_a()'"
- "The return value must be passed immediately to 'take_rv()'"
* Top-level AST conditions:
- "If you define a global constant 'a' of TypeXYZ, then you should
also define a function 'a_compare()'"
* Temporal conditions:
- "The return value must be used"
- "The return value must in all code paths (including/excluding
exceptions) be passed to 'take_rv()'"
Requirements
------------
Cross-module checks can be really useful.
- For "error" level checks, this requires strict versioning.
- For "warning" level checks, this could have two since-versions:
check Xyz since 1.2 warning_since 1.0 {
...
}
How should the checks be defined?
- Take inspiration from "declarative-imperative" API's like that of
EasyMock in the Java world.
- Take inspiration from XPath.
- Some things will need states
Syntax idea
-----------
Checks for usages of interfaces:
# Checks can apply to a top-level (types, functions and data)
# Value condition
check SomeType.do_stuff since 0.1 {
# "If parameter 'a' is non-none, then parameter 'b' must also be non-none"
if parameter("a").maybe_not_none() and parameter("b").maybe_none() {
error("'b' must always be non-none if 'a' might be non-none.")
}
}
# Local AST conditions
check SomeType.do_stuff since 0.1 {
# "Parameter 'a' must come from 'get_a()'"
if not parameter("a").expr().is_call_to("get_a") {
error("Parameter 'a' must come from 'get_a()'")
}
# "The return value must be passed immediately to 'take_rv()'"
if not return_value().target().is_parameter_to("take_rv", "a") {
error("The return value must be passed immediately to 'take_rv()'")
}
}
# Top-level AST condition
check TypeXYZ {
# "If you define a global constant 'a' of TypeXYZ, then you should
# also define a function 'a_compare()'"
ConstantDef def = definition()
if def.is_global().is_constant() {
# the concatenations operation needs an arena!
string fname = def.name() + "_compare"
Function f = def.module().find(fname)
if f == none {
error("'" + fname + "()' must also be defined")
}
}
}
# Temporal condition
check SomeType.get_value since 0.1 {
# "The return value must be used"
if return_value().is_unused() {
error("Return value must be used")
}
}
check SomeType.get_value since 0.1 {
# "The return value must in all code paths (including/excluding
# exceptions) be passed to 'take_rv()'"
if not return_value().all_paths(.exceptions=false).is_passed_to(.function("take_rv").param("x")) {
error("Return value must be passed to 'take_rv()'")
}
}
Self-checks for interfaces and for top-level implementation code:
# No since-version!
# It's NOT required to specify a top-level.
check {
Functions fs = type("SomeType").functions()
if fs.begins_with("get_").count() != fs.begins_with("set_").count() {
error("Different number of get_ and set_ methods")
}
# XXX these kinds of checks wouldn't be a very good to check for *usage checks*, which are since-versioned!
}
Self-checks inside a function:
func f(Thing t)
{
if (t.check_fluffiness(0) and t.name == "Box") {
...
} else if (t.check_fluffiness(1) and t.name == "Hat") {
...
} else if (t.check_fluffiness(2) and t.near == "Pillow") { # <--- oops, typo! ("near" instead of "name")
...
}
...
check {
StatementList sl = calls("check_fluffiness").statement(.if_stmt)
Matcher m = sl.first().matcher_without_literals()
assert sl.all_matches(m)
}
}
|