Skip to content

Commit 6a829e4

Browse files
authored
fix: do not fail style validation on warnings (#7941)
* Initial commit, needs the other PR to complete. * Add test * Update changelog * Improve import type * Remove comment * Remove unneeded test * Remove more tests * Remove comment * Update test * Remove comment * Remove tests keep coverage * Update tests * Move import * Consolidate validation error types. * Remove dead code. * Remove dead code. * Make the source validation more robust. * Update build size
1 parent 4dfb715 commit 6a829e4

14 files changed

Lines changed: 318 additions & 127 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
## main
22
### ✨ Features and improvements
3+
- Validate the terrain passed to `map.setTerrain`, which was previously applied unchecked ([#7941](https://github.com/maplibre/maplibre-gl-js/pull/7941)) (by [@HarelM](https://github.com/HarelM))
34
- Improve runtime error warnings to point at the offending style location (e.g. `layers[3].paint.line-color`, `layers[3].filter`) instead of just logging the bare error message ([#7869](https://github.com/maplibre/maplibre-gl-js/pull/7869)) (by [@CommanderStorm](https://github.com/CommanderStorm))
45
- ⚠️ Interpolate the light position in spherical coordinates instead of cartesian ones, so that a transition keeps its radial distance. ([#7919](https://github.com/maplibre/maplibre-gl-js/pull/7919)) (by [@HarelM](https://github.com/HarelM))
56
- _...Add new stuff here..._
67

78
### 🐞 Bug fixes
9+
- Log style validation warnings instead of treating them as errors, so that a filter mixing deprecated and expression syntax no longer aborts the style load and blanks the map ([#7941](https://github.com/maplibre/maplibre-style-spec/pull/7941)) (by [@HarelM](https://github.com/HarelM))
10+
- Validate `raster-dem` sources passed to `map.addSource`, which were previously skipped. Stop a source type the style spec has no schema for, such as one registered with `addSourceType`, from failing the whole style. Previously only `canvas` was let through ([#7941](https://github.com/maplibre/maplibre-gl-js/pull/7941)) (by [@HarelM](https://github.com/HarelM))
811
- _...Add new stuff here..._
912

1013
## 6.0.0-21

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/style/light.ts

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
import {latest as styleSpec} from '@maplibre/maplibre-gl-style-spec';
2-
31
import {sphericalToCartesian} from '../util/util.ts';
42
import {Evented} from '../util/evented.ts';
5-
import {
6-
validateStyle,
7-
validateLight,
8-
emitValidationErrors
9-
} from './validate_style.ts';
3+
import {validateStyle, validateAndEmit, type Validator} from './validate_style.ts';
104
import {getProperties, type LightProps, type LightPropsPossiblyEvaluated} from './light_properties.g.ts';
115

126
import type {vec3} from 'gl-matrix';
@@ -44,7 +38,7 @@ export class Light extends Evented {
4438
}
4539

4640
setLight(light?: LightSpecification, options: StyleSetterOptions = {}): void {
47-
if (this._validate(validateLight, light, options)) {
41+
if (this._validate(validateStyle.light, light, options)) {
4842
return;
4943
}
5044

@@ -70,18 +64,7 @@ export class Light extends Evented {
7064
this.properties = this._transitioning.possiblyEvaluate(parameters);
7165
}
7266

73-
_validate(validate: Function, value: unknown, options?: {
74-
validate?: boolean;
75-
}): boolean {
76-
if (options?.validate === false) {
77-
return false;
78-
}
79-
80-
return emitValidationErrors(this, validate.call(validateStyle, {
81-
value,
82-
// Workaround for https://github.com/mapbox/mapbox-gl-js/issues/2407
83-
style: {glyphs: true, sprite: true},
84-
styleSpec
85-
}));
67+
_validate(validate: Validator, value: unknown, options?: StyleSetterOptions): boolean {
68+
return validateAndEmit(this, validate, {value}, options);
8669
}
8770
}

src/style/sky.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import {type PossiblyEvaluated, TRANSITION_SUFFIX, Transitionable, type Transitioning, type TransitionParameters} from './properties.ts';
22
import {Evented} from '../util/evented.ts';
33
import {EvaluationParameters} from './evaluation_parameters.ts';
4-
import {emitValidationErrors, validateSky, validateStyle} from './validate_style.ts';
5-
import {extend} from '../util/util.ts';
6-
import {latest as styleSpec} from '@maplibre/maplibre-gl-style-spec';
7-
import {type Mesh} from '../render/mesh.ts';
4+
import {validateStyle, validateAndEmit, type Validator} from './validate_style.ts';
85
import {getProperties, type SkyProps, type SkyPropsPossiblyEvaluated} from './sky_properties.g.ts';
6+
import type {Mesh} from '../render/mesh.ts';
97
import type {SkySpecification} from '@maplibre/maplibre-gl-style-spec';
108
import type {StyleSetterOptions} from './style.ts';
119

@@ -29,7 +27,7 @@ export class Sky extends Evented {
2927
}
3028

3129
setSky(sky?: SkySpecification, options: StyleSetterOptions = {}): void {
32-
if (this._validate(validateSky, sky, options)) return;
30+
if (this._validate(validateStyle.sky, sky, options)) return;
3331

3432
sky ||= {
3533
'sky-color': 'transparent',
@@ -65,16 +63,8 @@ export class Sky extends Evented {
6563
this.properties = this._transitioning.possiblyEvaluate(parameters);
6664
}
6765

68-
_validate(validate: Function, value: unknown, options: StyleSetterOptions = {}): boolean {
69-
if (options?.validate === false) {
70-
return false;
71-
}
72-
return emitValidationErrors(this, validate.call(validateStyle, extend({
73-
value,
74-
// Workaround for https://github.com/mapbox/mapbox-gl-js/issues/2407
75-
style: {glyphs: true, sprite: true},
76-
styleSpec
77-
})));
66+
_validate(validate: Validator, value: unknown, options: StyleSetterOptions = {}): boolean {
67+
return validateAndEmit(this, validate, {value}, options);
7868
}
7969

8070
/**

src/style/style.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ function createGeoJSONSource(): GeoJSONSourceSpecification {
5151
};
5252
}
5353

54+
const mixedLegacyAndExpressionFilter = [
55+
'all',
56+
['==', ['get', 'class'], 'rail'],
57+
['in', 'name', '']
58+
] as any as FilterSpecification;
59+
5460
const getStubMap = () => new StubMap() as any;
5561

5662
function createStyle(map = getStubMap()) {
@@ -227,6 +233,25 @@ describe('Style.loadJSON', () => {
227233
expect(style.serialize()).toEqual(createStyleJSON());
228234
});
229235

236+
test('loads a style whose filter mixes legacy and expression syntax, warning instead of blanking the map', async () => {
237+
const style = new Style(getStubMap());
238+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
239+
const errorSpy = vi.fn();
240+
style.on('error', errorSpy);
241+
242+
style.loadJSON(createStyleJSON({
243+
sources: {geojson: createGeoJSONSource()},
244+
layers: [{id: 'symbol', type: 'symbol', source: 'geojson', filter: mixedLegacyAndExpressionFilter}]
245+
}));
246+
247+
await style.once('style.load');
248+
249+
expect(errorSpy).not.toHaveBeenCalled();
250+
expect(style.getLayer('symbol')).toBeTruthy();
251+
expect(style.getFilter('symbol')).toEqual(mixedLegacyAndExpressionFilter);
252+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Mixing deprecated filter syntax with expression syntax'));
253+
});
254+
230255
test('fires "dataloading" (synchronously)', () => {
231256
const style = new Style(getStubMap());
232257
const spy = vi.fn();
@@ -3037,6 +3062,20 @@ describe('Style.setFilter', () => {
30373062
expect(spy.mock.calls[0][1]['layers'][0].id).toBe('symbol');
30383063
expect(spy.mock.calls[0][1]['layers'][0].filter).toBe('notafilter');
30393064
});
3065+
3066+
test('warns instead of emitting for a filter that mixes legacy and expression syntax', async () => {
3067+
const style = createStyle();
3068+
await style.once('style.load');
3069+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
3070+
const errorSpy = vi.fn();
3071+
style.on('error', errorSpy);
3072+
3073+
style.setFilter('symbol', mixedLegacyAndExpressionFilter);
3074+
3075+
expect(style.getFilter('symbol')).toEqual(mixedLegacyAndExpressionFilter);
3076+
expect(errorSpy).not.toHaveBeenCalled();
3077+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Mixing deprecated filter syntax with expression syntax'));
3078+
});
30403079
});
30413080

30423081
describe('Style.setLayerZoomRange', () => {

src/style/style.ts

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ import {ResourceType} from '../util/request_manager.ts';
1616
import {browser} from '../util/browser.ts';
1717
import {now} from '../util/time_control.ts';
1818
import {Dispatcher} from '../util/dispatcher.ts';
19-
import {validateStyle, emitValidationErrors as _emitValidationErrors} from './validate_style.ts';
19+
import {validateStyle, validateStyleAndEmit, validateAndEmit, emitValidationErrors, SPEC_SOURCE_TYPES} from './validate_style.ts';
2020
import {type QueryRenderedFeaturesOptions, type QueryRenderedFeaturesOptionsStrict, type QueryRenderedFeaturesResults, type QueryRenderedFeaturesResultsItem, type QuerySourceFeatureOptions, queryRenderedFeatures, queryRenderedSymbols, querySourceFeatures} from '../source/query_features.ts';
2121
import {TileManager} from '../tile/tile_manager.ts';
22-
import {latest as styleSpec, derefLayers, emptyStyle, diff as diffStyles, type DiffCommand} from '@maplibre/maplibre-gl-style-spec';
22+
import {derefLayers, emptyStyle, diff as diffStyles, type DiffCommand} from '@maplibre/maplibre-gl-style-spec';
2323
import {getGlobalWorkerPool} from '../util/global_worker_pool.ts';
2424
import {rtlMainThreadPluginFactory} from '../source/rtl_text_plugin_main_thread.ts';
2525
import {RTLPluginLoadedEventName} from '../source/rtl_text_plugin_status.ts';
@@ -34,15 +34,6 @@ import type {StyleLayer} from './style_layer.ts';
3434
import type {MapGeoJSONFeature, GeoJSONFeature} from '../util/vectortile_to_geojson.ts';
3535
import type Point from '@mapbox/point-geometry';
3636

37-
// We're skipping validation errors with the `source.canvas` identifier in order
38-
// to continue to allow canvas sources to be added at runtime/updated in
39-
// smart setStyle (see https://github.com/mapbox/mapbox-gl-js/pull/6424):
40-
const emitValidationErrors = (evented: Evented, errors?: ReadonlyArray<{
41-
message: string;
42-
identifier?: string;
43-
}> | null) =>
44-
_emitValidationErrors(evented, errors?.filter(error => error.identifier !== 'source.canvas'));
45-
4637
import type {Map} from '../ui/map.ts';
4738
import type {IReadonlyTransform, ITransform} from '../geo/transform_interface.ts';
4839
import type {StyleImage} from './style_image.ts';
@@ -478,7 +469,7 @@ export class Style extends Evented<MapEventType> {
478469

479470
_load(json: StyleSpecification, options: StyleSwapOptions & StyleSetterOptions, previousStyle?: StyleSpecification): void {
480471
let nextState = options.transformStyle ? options.transformStyle(previousStyle, json) : json;
481-
if (options.validate && emitValidationErrors(this, validateStyle(nextState))) {
472+
if (options.validate && validateStyleAndEmit(this, nextState)) {
482473
return;
483474
}
484475

@@ -505,7 +496,8 @@ export class Style extends Evented<MapEventType> {
505496

506497
this.sky = new Sky(this.stylesheet.sky);
507498

508-
this.map.setTerrain(this.stylesheet.terrain ?? null);
499+
// The stylesheet's terrain was already validated as part of the style itself.
500+
this.map.setTerrain(this.stylesheet.terrain ?? null, {validate: false});
509501

510502
this.fire(new MapStyleDataEvent('data'));
511503
this.fire(new MapStyleLoadEvent());
@@ -879,7 +871,7 @@ export class Style extends Evented<MapEventType> {
879871
const serializedStyle = this.serialize();
880872
nextState = options.transformStyle ? options.transformStyle(serializedStyle, nextState) : nextState;
881873
const validate = options.validate ?? true;
882-
if (validate && emitValidationErrors(this, validateStyle(nextState))) return false;
874+
if (validate && validateStyleAndEmit(this, nextState)) return false;
883875

884876
nextState = clone(nextState);
885877
nextState.layers = derefLayers(nextState.layers);
@@ -1038,8 +1030,7 @@ export class Style extends Evented<MapEventType> {
10381030
throw new Error(`The type property must be defined, but only the following properties were given: ${Object.keys(source).join(', ')}.`);
10391031
}
10401032

1041-
const builtIns = ['vector', 'raster', 'geojson', 'video', 'image'];
1042-
const shouldValidate = builtIns.includes(source.type);
1033+
const shouldValidate = SPEC_SOURCE_TYPES.has(source.type);
10431034
if (shouldValidate && this._validate(validateStyle.source, `sources.${id}`, source, null, options)) return;
10441035
if (this.map?._collectResourceTiming) (source as any).collectResourceTiming = true;
10451036
const tileManager = this.tileManagers[id] = new TileManager(id, source, this.dispatcher);
@@ -1777,18 +1768,13 @@ export class Style extends Evented<MapEventType> {
17771768
}
17781769
}
17791770

1780-
_validate(validate: Validator, key: string, value: any, props: any, options: {
1781-
validate?: boolean;
1782-
} = {}): boolean {
1783-
if (options?.validate === false) {
1784-
return false;
1785-
}
1786-
return emitValidationErrors(this, validate.call(validateStyle, extend({
1771+
_validate(validate: Validator, key: string, value: any, props: any, options: StyleSetterOptions = {}): boolean {
1772+
return validateAndEmit(this, validate, {
17871773
key,
17881774
style: this.serialize(),
17891775
value,
1790-
styleSpec
1791-
}, props)));
1776+
...props
1777+
}, options);
17921778
}
17931779

17941780
_remove(mapRemoved: boolean = true): void {

src/style/style_layer.ts

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import {filterObject} from '../util/util.ts';
22

3-
import {createVisibilityExpression, featureFilter, latest as styleSpec, supportsPropertyExpression} from '@maplibre/maplibre-gl-style-spec';
4-
import {
5-
validateStyle,
6-
validateLayoutProperty,
7-
validatePaintProperty,
8-
emitValidationErrors
9-
} from './validate_style.ts';
3+
import {createVisibilityExpression, featureFilter, supportsPropertyExpression} from '@maplibre/maplibre-gl-style-spec';
4+
import {validateStyle, validateAndEmit, type Validator} from './validate_style.ts';
105
import {Evented, ErrorEvent} from '../util/evented.ts';
116
import {Layout, Transitionable, type Transitioning, type Properties, PossiblyEvaluated, PossiblyEvaluatedPropertyValue, TRANSITION_SUFFIX} from './properties.ts';
127

@@ -262,7 +257,7 @@ export abstract class StyleLayer extends Evented {
262257
return;
263258
}
264259

265-
if (value !== null && value !== undefined && this._validate(validateLayoutProperty, `layers.${this.id}.layout.${name}`, name, value, options)) return;
260+
if (value !== null && value !== undefined && this._validate(validateStyle.layoutProperty, `layers.${this.id}.layout.${name}`, name, value, options)) return;
266261

267262
this._unevaluatedLayout.setValue(name, value);
268263
}
@@ -290,7 +285,7 @@ export abstract class StyleLayer extends Evented {
290285
return false;
291286
}
292287

293-
if (value !== null && value !== undefined && this._validate(validatePaintProperty, `layers.${this.id}.paint.${name}`, name, value, options)) return false;
288+
if (value !== null && value !== undefined && this._validate(validateStyle.paintProperty, `layers.${this.id}.paint.${name}`, name, value, options)) return false;
294289

295290
if (name.endsWith(TRANSITION_SUFFIX)) {
296291
this._transitionablePaint.setTransition(name.slice(0, -TRANSITION_SUFFIX.length), (value as any) || undefined);
@@ -381,19 +376,13 @@ export abstract class StyleLayer extends Evented {
381376
});
382377
}
383378

384-
_validate(validate: Function, key: string, name: string, value: unknown, options: StyleSetterOptions = {}): boolean {
385-
if (options?.validate === false) {
386-
return false;
387-
}
388-
return emitValidationErrors(this, validate.call(validateStyle, {
379+
_validate(validate: Validator, key: string, name: string, value: unknown, options: StyleSetterOptions = {}): boolean {
380+
return validateAndEmit(this, validate, {
389381
key,
390382
layerType: this.type,
391383
objectKey: name,
392-
value,
393-
styleSpec,
394-
// Workaround for https://github.com/mapbox/mapbox-gl-js/issues/2407
395-
style: {glyphs: true, sprite: true}
396-
}));
384+
value
385+
}, options);
397386
}
398387

399388
is3D(): boolean {

src/style/style_layer/custom_style_layer.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {StyleLayer} from '../style_layer.ts';
2+
import {ValidationError} from '@maplibre/maplibre-gl-style-spec';
23
import type {Map} from '../../ui/map.ts';
3-
import {type mat4} from 'gl-matrix';
4-
import {type LayerSpecification} from '@maplibre/maplibre-gl-style-spec';
4+
import type {mat4} from 'gl-matrix';
5+
import type {LayerSpecification} from '@maplibre/maplibre-gl-style-spec';
56
import type {CustomLayerProjectionData, RendererProjectionData} from '../../geo/projection/projection_data.ts';
67

78
/**
@@ -292,28 +293,22 @@ export interface CustomLayerInterface {
292293
onRemove?(map: Map, gl: WebGL2RenderingContext): void;
293294
}
294295

295-
export function validateCustomStyleLayer(layerObject: CustomLayerInterface): Array<{message: string}> {
296-
const errors: Array<{message: string}> = [];
296+
export function validateCustomStyleLayer(layerObject: CustomLayerInterface): ValidationError[] {
297+
const errors: ValidationError[] = [];
297298
const id = layerObject.id;
298299

299300
if (id === undefined) {
300-
errors.push({
301-
message: `layers.${id}: missing required property "id"`
302-
});
301+
errors.push(new ValidationError(`layers.${id}`, null, 'missing required property "id"'));
303302
}
304303

305304
if (layerObject.render === undefined) {
306-
errors.push({
307-
message: `layers.${id}: missing required method "render"`
308-
});
305+
errors.push(new ValidationError(`layers.${id}`, null, 'missing required method "render"'));
309306
}
310307

311308
if (layerObject.renderingMode &&
312309
layerObject.renderingMode !== '2d' &&
313310
layerObject.renderingMode !== '3d') {
314-
errors.push({
315-
message: `layers.${id}: property "renderingMode" must be either "2d" or "3d"`
316-
});
311+
errors.push(new ValidationError(`layers.${id}`, null, 'property "renderingMode" must be either "2d" or "3d"'));
317312
}
318313

319314
return errors;

0 commit comments

Comments
 (0)