Skip to content

Commit 84068c4

Browse files
ibesoragithub-actions[bot]
authored andcommitted
Update appearance validation (internal-6344)
GitOrigin-RevId: 2859f84ea7d57598e95573300ddd224b31cee3c2
1 parent c976ff4 commit 84068c4

7 files changed

Lines changed: 158 additions & 7 deletions

File tree

src/style-spec/validate/validate_appearance.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import validateObject from './validate_object';
22
import ValidationError from '../error/validation_error';
33
import validateProperty from './validate_property';
44
import {unbundle} from '../util/unbundle_jsonlint';
5+
import validateExpression from './validate_expression';
6+
import latest from '../reference/latest';
57

6-
import type {StyleSpecification, LayerSpecification} from '../types';
8+
import type {StyleSpecification, LayerSpecification, AppearanceSpecification} from '../types';
79
import type {StyleReference} from '../reference/latest';
810

911
export type AppearanceValidatorOptions = {
@@ -19,7 +21,9 @@ export type AppearanceValidatorOptions = {
1921

2022
export default function validateAppearance(options: AppearanceValidatorOptions): Array<ValidationError> {
2123
const {key, layer, layerType} = options;
22-
const value = unbundle(options.value);
24+
const value = unbundle(options.value) as AppearanceSpecification;
25+
const name = unbundle(value.name);
26+
const condition = unbundle(value.condition);
2327

2428
const errors = validateObject({
2529
key,
@@ -28,10 +32,15 @@ export default function validateAppearance(options: AppearanceValidatorOptions):
2832
style: options.style,
2933
styleSpec: options.styleSpec,
3034
objectElementValidators: {
35+
condition: (options) => validateCondition(Object.assign({layer, layerType}, options)),
3136
properties: (options) => validateProperties(Object.assign({layer, layerType}, options)),
3237
}
3338
});
3439

40+
if (name !== 'hidden' && !condition) {
41+
errors.push(new ValidationError(options.key, 'name', `Appearance with name different than "hidden" must have a condition`));
42+
}
43+
3544
return errors;
3645
}
3746

@@ -73,3 +82,20 @@ function validateProperties(options: AppearanceValidatorOptions): Array<Validati
7382

7483
return errors;
7584
}
85+
86+
function validateCondition(options: AppearanceValidatorOptions): Array<ValidationError> {
87+
const errors: Array<ValidationError> = [];
88+
89+
const appearance = options.object as AppearanceSpecification;
90+
const condition = appearance.condition;
91+
92+
errors.push(...validateExpression({
93+
key: options.key,
94+
value: condition,
95+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
96+
valueSpec: latest['appearance']['condition'],
97+
expressionContext: 'appearance'
98+
}));
99+
100+
return errors;
101+
}

src/style-spec/validate/validate_expression.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import {deepUnbundle} from '../util/unbundle_jsonlint';
44
import {isStateConstant, isGlobalPropertyConstant, isFeatureConstant} from '../expression/is_constant';
55
import CompoundExpression from '../expression/compound_expression';
66

7+
import type {StylePropertySpecification} from '../../style-spec/style-spec';
78
import type {Expression} from '../expression/expression';
89
import type {StyleReference} from '../reference/latest';
910
import type {StyleSpecification} from '../types';
10-
import type {StylePropertySpecification} from '../style-spec';
1111

1212
export type ExpressionValidatorOptions = {
1313
key: string;
@@ -17,7 +17,7 @@ export type ExpressionValidatorOptions = {
1717
propertyType?: 'layout' | 'paint' | 'filter';
1818
style?: Partial<StyleSpecification>;
1919
styleSpec?: StyleReference;
20-
expressionContext?: 'property' | 'filter' | 'cluster-initial' | 'cluster-reduce' | 'cluster-map';
20+
expressionContext?: 'property' | 'filter' | 'cluster-initial' | 'cluster-reduce' | 'cluster-map' | 'appearance';
2121
};
2222

2323
export default function validateExpression(options: ExpressionValidatorOptions): ValidationError[] {
@@ -48,6 +48,11 @@ export default function validateExpression(options: ExpressionValidatorOptions):
4848
return disallowedFilterParameters(expressionObj, options);
4949
}
5050

51+
if (options.expressionContext === 'appearance') {
52+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
53+
return checkDisallowedParameters(expressionObj, options);
54+
}
55+
5156
if (options.expressionContext && options.expressionContext.indexOf('cluster') === 0) {
5257
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
5358
if (!isGlobalPropertyConstant(expressionObj, ['zoom', 'feature-state'])) {
@@ -97,3 +102,34 @@ export function disallowedFilterParameters(e: Expression, options: any): Validat
97102

98103
return errors;
99104
}
105+
106+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
107+
function checkDisallowedParameters(e: Expression, options: any): ValidationError[] {
108+
const allowedParameters = new Set<string>();
109+
110+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
111+
if (options.valueSpec && options.valueSpec.expression) {
112+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
113+
for (const param of options.valueSpec.expression.parameters) {
114+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
115+
allowedParameters.add(param);
116+
}
117+
}
118+
119+
if (allowedParameters.size === 0) {
120+
return [];
121+
}
122+
const errors: ValidationError[] = [];
123+
124+
if (e instanceof CompoundExpression) {
125+
if (!allowedParameters.has(e.name)) {
126+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
127+
return [new ValidationError(options.key, options.value, `["${e.name}"] is not an allowed parameter`)];
128+
}
129+
}
130+
e.eachChild((arg) => {
131+
errors.push(...checkDisallowedParameters(arg, options));
132+
});
133+
134+
return errors;
135+
}

src/style-spec/validate/validate_layer.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,32 @@ export default function validateLayer(options: LayerValidatorOptions): Validatio
160160
});
161161
},
162162
appearances(options) {
163-
return validateArray({
163+
const validationErrors = validateArray({
164164
key: options.key,
165165
value: options.value,
166+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
166167
valueSpec: options.valueSpec,
167168
style: options.style,
168169
styleSpec: options.styleSpec,
169170
arrayElementValidator: (options) => validateAppearance(Object.assign({layerType: type, layer}, options) as AppearanceValidatorOptions)
170171
});
172+
// Check non-repeated names on a given layer
173+
const appearances = Array.isArray(options.value) ? options.value : [];
174+
const dedupedNames = new Set<string>();
175+
appearances.forEach((a, index) => {
176+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
177+
const name: string | undefined = unbundle(a.name) as string | undefined;
178+
if (name) {
179+
if (dedupedNames.has(name)) {
180+
const layerId = unbundle((layer as LayerSpecification).id) as string;
181+
validationErrors.push(new ValidationError(options.key, name, `Duplicated appearance name "${name}" for layer "${layerId}"`));
182+
} else {
183+
dedupedNames.add(name);
184+
}
185+
}
186+
});
187+
188+
return validationErrors;
171189
}
172190
}
173191
}));

src/style-spec/validate/validate_object.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ type ObjectElementValidatorOptions = {
1111
valueSpec?: unknown;
1212
style: Partial<StyleSpecification>;
1313
styleSpec: StyleReference;
14+
object?: object;
15+
objectKey?: string;
1416
};
1517

1618
type ObjectValidatorOptions = {

test/unit/style-spec/fixture/appearances.input.json

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@
3636
"icon-opacity": 1,
3737
"text-color": "red"
3838
}
39+
},
40+
{
41+
"name": "hidden",
42+
"properties": {
43+
"icon-image": ["image", "poi", {"params": {"fill": "red"}}],
44+
"icon-size": 1.3,
45+
"icon-opacity": 1,
46+
"text-color": "red"
47+
}
3948
}
4049
]
4150
},
@@ -51,6 +60,46 @@
5160
"bad": true,
5261
"icon-opacity": -1
5362
}
63+
},
64+
{
65+
"name": "not-hidden",
66+
"properties": {
67+
"icon-opacity": 0.5
68+
}
69+
},
70+
{
71+
"condition": ["==", ["line-progress"], 16],
72+
"properties": {
73+
"icon-opacity": 0.5
74+
}
75+
}
76+
]
77+
},
78+
{
79+
"id": "appearances-with-repeated-names",
80+
"type": "symbol",
81+
"source": "vector",
82+
"source-layer": "source-layer",
83+
"appearances": [
84+
{
85+
"name": "A",
86+
"condition": [">=", ["zoom"], 16],
87+
"properties": {
88+
"icon-opacity": 1
89+
}
90+
},
91+
{
92+
"condition": [">=", ["zoom"], 16],
93+
"properties": {
94+
"icon-opacity": 1
95+
}
96+
},
97+
{
98+
"name": "A",
99+
"condition": [">=", ["zoom"], 16],
100+
"properties": {
101+
"icon-opacity": 1
102+
}
54103
}
55104
]
56105
}

test/unit/style-spec/fixture/appearances.output-api-supported.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44
},
55
{
66
"message": "layers[1].appearances[0].properties.icon-opacity: -1 is less than the minimum value 0",
7-
"line": 52
7+
"line": 61
8+
},
9+
{
10+
"message": "layers[1].appearances[1]: Appearance with name different than \"hidden\" must have a condition"
11+
},
12+
{
13+
"message": "layers[1].appearances[2].condition: [\"line-progress\"] is not an allowed parameter",
14+
"line": 71
15+
},
16+
{
17+
"message": "layers[2].appearances: Duplicated appearance name \"A\" for layer \"appearances-with-repeated-names\""
818
}
919
]

test/unit/style-spec/fixture/appearances.output.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44
},
55
{
66
"message": "layers[1].appearances[0].properties.icon-opacity: -1 is less than the minimum value 0",
7-
"line": 52
7+
"line": 61
8+
},
9+
{
10+
"message": "layers[1].appearances[1]: Appearance with name different than \"hidden\" must have a condition"
11+
},
12+
{
13+
"message": "layers[1].appearances[2].condition: [\"line-progress\"] is not an allowed parameter",
14+
"line": 71
15+
},
16+
{
17+
"message": "layers[2].appearances: Duplicated appearance name \"A\" for layer \"appearances-with-repeated-names\""
818
}
919
]

0 commit comments

Comments
 (0)