Skip to content

Commit 9d65fa7

Browse files
authored
Merge branch 'main' into fix-ref-to-id
2 parents a4cc925 + 7d9158a commit 9d65fa7

File tree

10 files changed

+181
-38
lines changed

10 files changed

+181
-38
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11

2+
3+
4.2.0 /
4+
================
5+
* new API `LanguageService.getLanguageStatus`
6+
27
4.1.6 / 2021-07-16
38
================
49
* Replace minimatch with glob-to-regexp

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.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vscode-json-languageservice",
3-
"version": "4.1.10",
3+
"version": "4.2.0-next.1",
44
"description": "Language service for JSON",
55
"main": "./lib/umd/jsonLanguageService.js",
66
"typings": "./lib/umd/jsonLanguageService",

src/jsonLanguageService.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
FoldingRange, JSONSchema, SelectionRange, FoldingRangesContext, DocumentSymbolsContext, ColorInformationContext as DocumentColorsContext,
2424
TextDocument,
2525
Position, CompletionItem, CompletionList, Hover, Range, SymbolInformation, Diagnostic,
26-
TextEdit, FormattingOptions, DocumentSymbol, DefinitionLink, MatchingSchema
26+
TextEdit, FormattingOptions, DocumentSymbol, DefinitionLink, MatchingSchema, JSONLanguageStatus
2727
} from './jsonLanguageTypes';
2828
import { findLinks } from './services/jsonLinks';
2929
import { DocumentLink } from 'vscode-languageserver-types';
@@ -41,6 +41,7 @@ export interface LanguageService {
4141
newJSONDocument(rootNode: ASTNode, syntaxDiagnostics?: Diagnostic[]): JSONDocument;
4242
resetSchema(uri: string): boolean;
4343
getMatchingSchemas(document: TextDocument, jsonDocument: JSONDocument, schema?: JSONSchema): Thenable<MatchingSchema[]>;
44+
getLanguageStatus(document: TextDocument, jsonDocument: JSONDocument): JSONLanguageStatus;
4445
doResolve(item: CompletionItem): Thenable<CompletionItem>;
4546
doComplete(document: TextDocument, position: Position, doc: JSONDocument): Thenable<CompletionList | null>;
4647
findDocumentSymbols(document: TextDocument, doc: JSONDocument, context?: DocumentSymbolsContext): SymbolInformation[];
@@ -79,6 +80,7 @@ export function getLanguageService(params: LanguageServiceParams): LanguageServi
7980
},
8081
resetSchema: (uri: string) => jsonSchemaService.onResourceChange(uri),
8182
doValidation: jsonValidation.doValidation.bind(jsonValidation),
83+
getLanguageStatus: jsonValidation.getLanguageStatus.bind(jsonValidation),
8284
parseJSONDocument: (document: TextDocument) => parseJSON(document, { collectComments: true }),
8385
newJSONDocument: (root: ASTNode, diagnostics: Diagnostic[]) => newJSONDocument(root, diagnostics),
8486
getMatchingSchemas: jsonSchemaService.getMatchingSchemas.bind(jsonSchemaService),

src/jsonLanguageTypes.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
SymbolInformation, SymbolKind, DocumentSymbol, Location, Hover, MarkedString, FormattingOptions as LSPFormattingOptions, DefinitionLink,
1616
CodeActionContext, Command, CodeAction,
1717
DocumentHighlight, DocumentLink, WorkspaceEdit,
18-
TextEdit, CodeActionKind,
18+
TextEdit, CodeActionKind,
1919
TextDocumentEdit, VersionedTextDocumentIdentifier, DocumentHighlightKind
2020
} from 'vscode-languageserver-types';
2121

@@ -33,7 +33,7 @@ export {
3333
SymbolInformation, SymbolKind, DocumentSymbol, Location, Hover, MarkedString,
3434
CodeActionContext, Command, CodeAction,
3535
DocumentHighlight, DocumentLink, WorkspaceEdit,
36-
TextEdit, CodeActionKind,
36+
TextEdit, CodeActionKind,
3737
TextDocumentEdit, VersionedTextDocumentIdentifier, DocumentHighlightKind
3838
};
3939

@@ -112,6 +112,10 @@ export interface MatchingSchema {
112112
schema: JSONSchema;
113113
}
114114

115+
export interface JSONLanguageStatus {
116+
schemas: string[];
117+
}
118+
115119
export interface LanguageSettings {
116120
/**
117121
* If set, the validator will return syntax and semantic errors.
@@ -158,12 +162,12 @@ export interface SchemaConfiguration {
158162
* The URI of the schema, which is also the identifier of the schema.
159163
*/
160164
uri: string;
161-
/**
162-
* A list of glob patterns that describe for which file URIs the JSON schema will be used.
163-
* '*' and '**' wildcards are supported. Exclusion patterns start with '!'.
164-
* For example '*.schema.json', 'package.json', '!foo*.schema.json', 'foo/**\/BADRESP.json'.
165-
* A match succeeds when there is at least one pattern matching and last matching pattern does not start with '!'.
166-
*/
165+
/**
166+
* A list of glob patterns that describe for which file URIs the JSON schema will be used.
167+
* '*' and '**' wildcards are supported. Exclusion patterns start with '!'.
168+
* For example '*.schema.json', 'package.json', '!foo*.schema.json', 'foo/**\/BADRESP.json'.
169+
* A match succeeds when there is at least one pattern matching and last matching pattern does not start with '!'.
170+
*/
167171
fileMatch?: string[];
168172
/**
169173
* The schema for the given URI.

src/parser/jsonParser.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ const formats = {
2323
'date-time': { errorMessage: localize('dateTimeFormatWarning', 'String is not a RFC3339 date-time.'), pattern: /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)([01][0-9]|2[0-3]):([0-5][0-9]))$/i },
2424
'date': { errorMessage: localize('dateFormatWarning', 'String is not a RFC3339 date.'), pattern: /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/i },
2525
'time': { errorMessage: localize('timeFormatWarning', 'String is not a RFC3339 time.'), pattern: /^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)([01][0-9]|2[0-3]):([0-5][0-9]))$/i },
26-
'email': { errorMessage: localize('emailFormatWarning', 'String is not an e-mail address.'), pattern: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ }
26+
'email': { errorMessage: localize('emailFormatWarning', 'String is not an e-mail address.'), pattern: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ },
27+
'hostname': { errorMessage: localize('hostnameFormatWarning', 'String is not a hostname.'), pattern: /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i },
28+
'ipv4': { errorMessage: localize('ipv4FormatWarning', 'String is not an IPv4 address.'), pattern: /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/ },
29+
'ipv6': { errorMessage: localize('ipv6FormatWarning', 'String is not an IPv6 address.'), pattern: /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i },
2730
};
2831

2932
export interface IProblem {
@@ -683,6 +686,9 @@ function validate(n: ASTNode | undefined, schema: JSONSchema, validationResult:
683686
case 'date':
684687
case 'time':
685688
case 'email':
689+
case 'hostname':
690+
case 'ipv4':
691+
case 'ipv6':
686692
const format = formats[schema.format];
687693
if (!node.value || !format.pattern.exec(node.value)) {
688694
validationResult.problems.push({

src/services/jsonSchemaService.ts

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ class SchemaHandle implements ISchemaHandle {
130130
public readonly uri: string;
131131
public readonly dependencies: SchemaDependencies;
132132
public readonly anchors: Map<string, JSONSchema>;
133-
134133
private resolvedSchema: Thenable<ResolvedSchema> | undefined;
135134
private unresolvedSchema: Thenable<UnresolvedSchema> | undefined;
136135
private readonly service: JSONSchemaService;
@@ -161,11 +160,13 @@ class SchemaHandle implements ISchemaHandle {
161160
return this.resolvedSchema;
162161
}
163162

164-
public clearSchema() {
163+
public clearSchema(): boolean {
164+
const hasChanges = !!this.unresolvedSchema;
165165
this.resolvedSchema = undefined;
166166
this.unresolvedSchema = undefined;
167167
this.dependencies.clear();
168168
this.anchors.clear();
169+
return hasChanges;
169170
}
170171
}
171172

@@ -293,9 +294,10 @@ export class JSONSchemaService implements IJSONSchemaService {
293294
if (handle.uri !== curr) {
294295
toWalk.push(handle.uri);
295296
}
296-
handle.clearSchema();
297+
if (handle.clearSchema()) {
298+
hasChanges = true;
299+
}
297300
all[i] = undefined;
298-
hasChanges = true;
299301
}
300302
}
301303
}
@@ -415,6 +417,8 @@ export class JSONSchemaService implements IJSONSchemaService {
415417
return this.promise.resolve(new ResolvedSchema({}, [localize('json.schema.draft03.notsupported', "Draft-03 schemas are not supported.")]));
416418
} else if (id === 'https://json-schema.org/draft/2019-09/schema') {
417419
resolveErrors.push(localize('json.schema.draft201909.notsupported', "Draft 2019-09 schemas are not yet fully supported."));
420+
} else if (id === 'https://json-schema.org/draft/2020-12/schema') {
421+
resolveErrors.push(localize('json.schema.draft202012.notsupported', "Draft 2020-12 schemas are not yet fully supported."));
418422
}
419423
}
420424

@@ -639,31 +643,22 @@ export class JSONSchemaService implements IJSONSchemaService {
639643
return new ResolvedSchema(schema, resolveErrors);
640644
});
641645
}
642-
643-
public getSchemaForResource(resource: string, document?: Parser.JSONDocument): Thenable<ResolvedSchema | undefined> {
644-
645-
// first use $schema if present
646-
if (document && document.root && document.root.type === 'object') {
647-
const schemaProperties = document.root.properties.filter(p => (p.keyNode.value === '$schema') && p.valueNode && p.valueNode.type === 'string');
648-
if (schemaProperties.length > 0) {
649-
const valueNode = schemaProperties[0].valueNode;
650-
if (valueNode && valueNode.type === 'string') {
651-
let schemeId = <string>Parser.getNodeValue(valueNode);
652-
if (schemeId && Strings.startsWith(schemeId, '.') && this.contextService) {
653-
schemeId = this.contextService.resolveRelativePath(schemeId, resource);
654-
}
655-
if (schemeId) {
656-
const id = normalizeId(schemeId);
657-
return this.getOrAddSchemaHandle(id).getResolvedSchema();
646+
private getSchemaFromProperty(resource: string, document: Parser.JSONDocument): string | undefined {
647+
if (document.root?.type === 'object') {
648+
for (const p of document.root.properties) {
649+
if (p.keyNode.value === '$schema' && p.valueNode?.type === 'string') {
650+
let schemaId = p.valueNode.value;
651+
if (this.contextService && !/^\w[\w\d+.-]*:/.test(schemaId)) { // has scheme
652+
schemaId = this.contextService.resolveRelativePath(schemaId, resource);
658653
}
654+
return schemaId;
659655
}
660656
}
661657
}
658+
return undefined;
659+
}
662660

663-
if (this.cachedSchemaForResource && this.cachedSchemaForResource.resource === resource) {
664-
return this.cachedSchemaForResource.resolvedSchema;
665-
}
666-
661+
private getAssociatedSchemas(resource: string): string[] {
667662
const seen: { [schemaId: string]: boolean } = Object.create(null);
668663
const schemas: string[] = [];
669664
const normalizedResource = normalizeResourceForMatching(resource);
@@ -677,6 +672,30 @@ export class JSONSchemaService implements IJSONSchemaService {
677672
}
678673
}
679674
}
675+
return schemas;
676+
}
677+
678+
public getSchemaURIsForResource(resource: string, document?: Parser.JSONDocument): string[] {
679+
let schemeId = document && this.getSchemaFromProperty(resource, document);
680+
if (schemeId) {
681+
return [schemeId];
682+
}
683+
return this.getAssociatedSchemas(resource);
684+
}
685+
686+
public getSchemaForResource(resource: string, document?: Parser.JSONDocument): Thenable<ResolvedSchema | undefined> {
687+
if (document) {
688+
// first use $schema if present
689+
let schemeId = this.getSchemaFromProperty(resource, document);
690+
if (schemeId) {
691+
const id = normalizeId(schemeId);
692+
return this.getOrAddSchemaHandle(id).getResolvedSchema();
693+
}
694+
}
695+
if (this.cachedSchemaForResource && this.cachedSchemaForResource.resource === resource) {
696+
return this.cachedSchemaForResource.resolvedSchema;
697+
}
698+
const schemas = this.getAssociatedSchemas(resource);
680699
const resolvedSchema = schemas.length > 0 ? this.createCombinedSchema(resource, schemas).getResolvedSchema() : this.promise.resolve(undefined);
681700
this.cachedSchemaForResource = { resource, resolvedSchema };
682701
return resolvedSchema;

src/services/jsonValidation.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { JSONSchemaService, ResolvedSchema, UnresolvedSchema } from './jsonSchemaService';
77
import { JSONDocument } from '../parser/jsonParser';
88

9-
import { TextDocument, ErrorCode, PromiseConstructor, Thenable, LanguageSettings, DocumentLanguageSettings, SeverityLevel, Diagnostic, DiagnosticSeverity, Range } from '../jsonLanguageTypes';
9+
import { TextDocument, ErrorCode, PromiseConstructor, Thenable, LanguageSettings, DocumentLanguageSettings, SeverityLevel, Diagnostic, DiagnosticSeverity, Range, JSONLanguageStatus } from '../jsonLanguageTypes';
1010
import * as nls from 'vscode-nls';
1111
import { JSONSchemaRef, JSONSchema } from '../jsonSchema';
1212
import { isBoolean } from '../utils/objects';
@@ -112,6 +112,10 @@ export class JSONValidation {
112112
return getDiagnostics(schema);
113113
});
114114
}
115+
116+
public getLanguageStatus(textDocument: TextDocument, jsonDocument: JSONDocument): JSONLanguageStatus {
117+
return { schemas: this.jsonSchemaService.getSchemaURIsForResource(textDocument.uri, jsonDocument) };
118+
}
115119
}
116120

117121
let idCounter = 0;

src/test/parser.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,57 @@ suite('JSON Parser', () => {
656656
semanticErrors = validate('{"one":"//foo/bar"}', schemaWithURIReference);
657657
assert.strictEqual(semanticErrors!.length, 0, 'uri-reference');
658658

659+
const schemaWithHostname = {
660+
type: 'object',
661+
properties: {
662+
"hostname": {
663+
type: 'string',
664+
format: 'hostname'
665+
}
666+
}
667+
};
668+
669+
semanticErrors = validate('{"hostname":"code.visualstudio.com"}', schemaWithHostname);
670+
assert.strictEqual(semanticErrors!.length, 0, "hostname");
671+
672+
semanticErrors = validate('{"hostname":"foo/bar"}', schemaWithHostname);
673+
assert.strictEqual(semanticErrors!.length, 1, "hostname");
674+
assert.strictEqual(semanticErrors![0].message, 'String is not a hostname.');
675+
676+
const schemaWithIPv4 = {
677+
type: 'object',
678+
properties: {
679+
"hostaddr4": {
680+
type: 'string',
681+
format: 'ipv4'
682+
}
683+
}
684+
};
685+
686+
semanticErrors = validate('{"hostaddr4":"127.0.0.1"}', schemaWithIPv4);
687+
assert.strictEqual(semanticErrors!.length, 0, "hostaddr4");
688+
689+
semanticErrors = validate('{"hostaddr4":"1916:0:0:0:0:F00:1:81AE"}', schemaWithIPv4);
690+
assert.strictEqual(semanticErrors!.length, 1, "hostaddr4");
691+
assert.strictEqual(semanticErrors![0].message, 'String is not an IPv4 address.');
692+
693+
const schemaWithIPv6 = {
694+
type: 'object',
695+
properties: {
696+
"hostaddr6": {
697+
type: 'string',
698+
format: 'ipv6'
699+
}
700+
}
701+
};
702+
703+
semanticErrors = validate('{"hostaddr6":"1916:0:0:0:0:F00:1:81AE"}', schemaWithIPv6);
704+
assert.strictEqual(semanticErrors!.length, 0, "hostaddr6");
705+
706+
semanticErrors = validate('{"hostaddr6":"127.0.0.1"}', schemaWithIPv6);
707+
assert.strictEqual(semanticErrors!.length, 1, "hostaddr6");
708+
assert.strictEqual(semanticErrors![0].message, 'String is not an IPv6 address.');
709+
659710
const schemaWithEMail = {
660711
type: 'object',
661712
properties: {

src/test/schema.test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as fs from 'fs';
1010
import * as url from 'url';
1111
import * as path from 'path';
1212
import { getLanguageService, JSONSchema, SchemaRequestService, TextDocument, MatchingSchema } from '../jsonLanguageService';
13-
import { DiagnosticSeverity } from '../jsonLanguageTypes';
13+
import { DiagnosticSeverity, SchemaConfiguration } from '../jsonLanguageTypes';
1414

1515
function toDocument(text: string, config?: Parser.JSONDocumentConfig, uri = 'foo://bar/file.json'): { textDoc: TextDocument, jsonDoc: Parser.JSONDocument } {
1616

@@ -1831,5 +1831,57 @@ suite('JSON Schema', () => {
18311831
}
18321832
});
18331833

1834+
test('getLanguageStatus', async function () {
1835+
const schemas: SchemaConfiguration[] = [{
1836+
uri: 'https://myschemastore/schema1.json',
1837+
fileMatch: ['**/*.json'],
1838+
schema: {
1839+
type: 'object',
1840+
}
1841+
},
1842+
{
1843+
uri: 'https://myschemastore/schema2.json',
1844+
fileMatch: ['**/bar.json'],
1845+
schema: {
1846+
type: 'object',
1847+
}
1848+
},
1849+
{
1850+
uri: 'https://myschemastore/schema3.json',
1851+
schema: {
1852+
type: 'object',
1853+
}
1854+
}
1855+
];
1856+
const ls = getLanguageService({ workspaceContext });
1857+
ls.configure({ schemas });
1858+
1859+
{
1860+
const { textDoc, jsonDoc } = toDocument('{ }', undefined, 'foo://bar/folder/foo.json');
1861+
const info = ls.getLanguageStatus(textDoc, jsonDoc);
1862+
assert.deepStrictEqual(info.schemas, ['https://myschemastore/schema1.json']);
1863+
}
1864+
{
1865+
const { textDoc, jsonDoc } = toDocument('{ }', undefined, 'foo://bar/folder/bar.json');
1866+
const info = ls.getLanguageStatus(textDoc, jsonDoc);
1867+
assert.deepStrictEqual(info.schemas, ['https://myschemastore/schema1.json', 'https://myschemastore/schema2.json']);
1868+
}
1869+
{
1870+
const { textDoc, jsonDoc } = toDocument('{ $schema: "https://myschemastore/schema3.json" }', undefined, 'foo://bar/folder/bar.json');
1871+
const info = ls.getLanguageStatus(textDoc, jsonDoc);
1872+
assert.deepStrictEqual(info.schemas, ['https://myschemastore/schema3.json']);
1873+
}
1874+
{
1875+
const { textDoc, jsonDoc } = toDocument('{ $schema: "schema3.json" }', undefined, 'foo://bar/folder/bar.json');
1876+
const info = ls.getLanguageStatus(textDoc, jsonDoc);
1877+
assert.deepStrictEqual(info.schemas, ['foo://bar/folder/schema3.json']);
1878+
}
1879+
{
1880+
const { textDoc, jsonDoc } = toDocument('{ $schema: "./schema3.json" }', undefined, 'foo://bar/folder/bar.json');
1881+
const info = ls.getLanguageStatus(textDoc, jsonDoc);
1882+
assert.deepStrictEqual(info.schemas, ['foo://bar/folder/schema3.json']);
1883+
}
1884+
1885+
});
18341886

18351887
});

0 commit comments

Comments
 (0)