-
Notifications
You must be signed in to change notification settings - Fork 2.3k
/
Copy pathparsing_context.js
204 lines (181 loc) · 7.85 KB
/
parsing_context.js
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
// @flow
const Scope = require('./scope');
const {checkSubtype} = require('./types');
const ParsingError = require('./parsing_error');
const Literal = require('./definitions/literal');
const Assertion = require('./definitions/assertion');
const ArrayAssertion = require('./definitions/array');
const Coercion = require('./definitions/coercion');
import type {Expression} from './expression';
import type {Type} from './types';
/**
* State associated parsing at a given point in an expression tree.
* @private
*/
class ParsingContext {
definitions: {[string]: Class<Expression>};
path: Array<number>;
key: string;
scope: Scope;
errors: Array<ParsingError>;
// The expected type of this expression. Provided only to allow Expression
// implementations to infer argument types: Expression#parse() need not
// check that the output type of the parsed expression matches
// `expectedType`.
expectedType: ?Type;
constructor(
definitions: *,
path: Array<number> = [],
expectedType: ?Type,
scope: Scope = new Scope(),
errors: Array<ParsingError> = []
) {
this.definitions = definitions;
this.path = path;
this.key = path.map(part => `[${part}]`).join('');
this.scope = scope;
this.errors = errors;
this.expectedType = expectedType;
}
/**
* @param expr the JSON expression to parse
* @param index the optional argument index if this expression is an argument of a parent expression that's being parsed
* @param options
* @param options.omitTypeAnnotations set true to omit inferred type annotations. Caller beware: with this option set, the parsed expression's type will NOT satisfy `expectedType` if it would normally be wrapped in an inferred annotation.
* @private
*/
parse(
expr: mixed,
index?: number,
expectedType?: ?Type,
bindings?: Array<[string, Expression]>,
options: {omitTypeAnnotations?: boolean} = {}
): ?Expression {
let context = this;
if (index) {
context = context.concat(index, expectedType, bindings);
}
if (expr === null || typeof expr === 'string' || typeof expr === 'boolean' || typeof expr === 'number') {
expr = ['literal', expr];
}
if (Array.isArray(expr)) {
if (expr.length === 0) {
return context.error(`Expected an array with at least one element. If you wanted a literal array, use ["literal", []].`);
}
const op = expr[0];
if (typeof op !== 'string') {
context.error(`Expression name must be a string, but found ${typeof op} instead. If you wanted a literal array, use ["literal", [...]].`, 0);
return null;
}
const Expr = context.definitions[op];
if (Expr) {
let parsed = Expr.parse(expr, context);
if (!parsed) return null;
if (context.expectedType) {
const expected = context.expectedType;
const actual = parsed.type;
// When we expect a number, string, boolean, or array but
// have a Value, we can wrap it in a refining assertion.
// When we expect a Color but have a String or Value, we
// can wrap it in "to-color" coercion.
// Otherwise, we do static type-checking.
if ((expected.kind === 'string' || expected.kind === 'number' || expected.kind === 'boolean') && actual.kind === 'value') {
if (!options.omitTypeAnnotations) {
parsed = new Assertion(expected, [parsed]);
}
} else if (expected.kind === 'array' && actual.kind === 'value') {
if (!options.omitTypeAnnotations) {
parsed = new ArrayAssertion(expected, parsed);
}
} else if (expected.kind === 'color' && (actual.kind === 'value' || actual.kind === 'string')) {
if (!options.omitTypeAnnotations) {
parsed = new Coercion(expected, [parsed]);
}
} else if (context.checkSubtype(context.expectedType, parsed.type)) {
return null;
}
}
// If an expression's arguments are all literals, we can evaluate
// it immediately and replace it with a literal value in the
// parsed/compiled result.
if (!(parsed instanceof Literal) && isConstant(parsed)) {
const ec = new (require('./evaluation_context'))();
try {
parsed = new Literal(parsed.type, parsed.evaluate(ec));
} catch (e) {
context.error(e.message);
return null;
}
}
return parsed;
}
return context.error(`Unknown expression "${op}". If you wanted a literal array, use ["literal", [...]].`, 0);
} else if (typeof expr === 'undefined') {
return context.error(`'undefined' value invalid. Use null instead.`);
} else if (typeof expr === 'object') {
return context.error(`Bare objects invalid. Use ["literal", {...}] instead.`);
} else {
return context.error(`Expected an array, but found ${typeof expr} instead.`);
}
}
/**
* Returns a copy of this context suitable for parsing the subexpression at
* index `index`, optionally appending to 'let' binding map.
*
* Note that `errors` property, intended for collecting errors while
* parsing, is copied by reference rather than cloned.
* @private
*/
concat(index: number, expectedType?: ?Type, bindings?: Array<[string, Expression]>) {
const path = typeof index === 'number' ? this.path.concat(index) : this.path;
const scope = bindings ? this.scope.concat(bindings) : this.scope;
return new ParsingContext(
this.definitions,
path,
expectedType || null,
scope,
this.errors
);
}
/**
* Push a parsing (or type checking) error into the `this.errors`
* @param error The message
* @param keys Optionally specify the source of the error at a child
* of the current expression at `this.key`.
* @private
*/
error(error: string, ...keys: Array<number>) {
const key = `${this.key}${keys.map(k => `[${k}]`).join('')}`;
this.errors.push(new ParsingError(key, error));
}
/**
* Returns null if `t` is a subtype of `expected`; otherwise returns an
* error message and also pushes it to `this.errors`.
*/
checkSubtype(expected: Type, t: Type): ?string {
const error = checkSubtype(expected, t);
if (error) this.error(error);
return error;
}
}
module.exports = ParsingContext;
function isConstant(expression: Expression) {
// requires within function body to workaround circular dependency
const {CompoundExpression} = require('./compound_expression');
const {isGlobalPropertyConstant, isFeatureConstant} = require('./is_constant');
const Var = require('./definitions/var');
if (expression instanceof Var) {
return false;
} else if (expression instanceof CompoundExpression && expression.name === 'error') {
return false;
}
let literalArgs = true;
expression.eachChild(arg => {
if (!(arg instanceof Literal)) { literalArgs = false; }
});
if (!literalArgs) {
return false;
}
return isFeatureConstant(expression) &&
isGlobalPropertyConstant(expression, ['zoom', 'heatmap-density']);
}