jsonSchemaService.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import * as Json from 'jsonc-parser';
  6. import { URI } from 'vscode-uri';
  7. import * as Strings from '../utils/strings';
  8. import * as Parser from '../parser/jsonParser';
  9. import * as nls from 'vscode-nls';
  10. import { createRegex } from '../utils/glob';
  11. var localize = nls.loadMessageBundle();
  12. var BANG = '!';
  13. var PATH_SEP = '/';
  14. var FilePatternAssociation = /** @class */ (function () {
  15. function FilePatternAssociation(pattern, uris) {
  16. this.globWrappers = [];
  17. try {
  18. for (var _i = 0, pattern_1 = pattern; _i < pattern_1.length; _i++) {
  19. var patternString = pattern_1[_i];
  20. var include = patternString[0] !== BANG;
  21. if (!include) {
  22. patternString = patternString.substring(1);
  23. }
  24. if (patternString.length > 0) {
  25. if (patternString[0] === PATH_SEP) {
  26. patternString = patternString.substring(1);
  27. }
  28. this.globWrappers.push({
  29. regexp: createRegex('**/' + patternString, { extended: true, globstar: true }),
  30. include: include,
  31. });
  32. }
  33. }
  34. ;
  35. this.uris = uris;
  36. }
  37. catch (e) {
  38. this.globWrappers.length = 0;
  39. this.uris = [];
  40. }
  41. }
  42. FilePatternAssociation.prototype.matchesPattern = function (fileName) {
  43. var match = false;
  44. for (var _i = 0, _a = this.globWrappers; _i < _a.length; _i++) {
  45. var _b = _a[_i], regexp = _b.regexp, include = _b.include;
  46. if (regexp.test(fileName)) {
  47. match = include;
  48. }
  49. }
  50. return match;
  51. };
  52. FilePatternAssociation.prototype.getURIs = function () {
  53. return this.uris;
  54. };
  55. return FilePatternAssociation;
  56. }());
  57. var SchemaHandle = /** @class */ (function () {
  58. function SchemaHandle(service, url, unresolvedSchemaContent) {
  59. this.service = service;
  60. this.url = url;
  61. this.dependencies = {};
  62. if (unresolvedSchemaContent) {
  63. this.unresolvedSchema = this.service.promise.resolve(new UnresolvedSchema(unresolvedSchemaContent));
  64. }
  65. }
  66. SchemaHandle.prototype.getUnresolvedSchema = function () {
  67. if (!this.unresolvedSchema) {
  68. this.unresolvedSchema = this.service.loadSchema(this.url);
  69. }
  70. return this.unresolvedSchema;
  71. };
  72. SchemaHandle.prototype.getResolvedSchema = function () {
  73. var _this = this;
  74. if (!this.resolvedSchema) {
  75. this.resolvedSchema = this.getUnresolvedSchema().then(function (unresolved) {
  76. return _this.service.resolveSchemaContent(unresolved, _this.url, _this.dependencies);
  77. });
  78. }
  79. return this.resolvedSchema;
  80. };
  81. SchemaHandle.prototype.clearSchema = function () {
  82. this.resolvedSchema = undefined;
  83. this.unresolvedSchema = undefined;
  84. this.dependencies = {};
  85. };
  86. return SchemaHandle;
  87. }());
  88. var UnresolvedSchema = /** @class */ (function () {
  89. function UnresolvedSchema(schema, errors) {
  90. if (errors === void 0) { errors = []; }
  91. this.schema = schema;
  92. this.errors = errors;
  93. }
  94. return UnresolvedSchema;
  95. }());
  96. export { UnresolvedSchema };
  97. var ResolvedSchema = /** @class */ (function () {
  98. function ResolvedSchema(schema, errors) {
  99. if (errors === void 0) { errors = []; }
  100. this.schema = schema;
  101. this.errors = errors;
  102. }
  103. ResolvedSchema.prototype.getSection = function (path) {
  104. var schemaRef = this.getSectionRecursive(path, this.schema);
  105. if (schemaRef) {
  106. return Parser.asSchema(schemaRef);
  107. }
  108. return undefined;
  109. };
  110. ResolvedSchema.prototype.getSectionRecursive = function (path, schema) {
  111. if (!schema || typeof schema === 'boolean' || path.length === 0) {
  112. return schema;
  113. }
  114. var next = path.shift();
  115. if (schema.properties && typeof schema.properties[next]) {
  116. return this.getSectionRecursive(path, schema.properties[next]);
  117. }
  118. else if (schema.patternProperties) {
  119. for (var _i = 0, _a = Object.keys(schema.patternProperties); _i < _a.length; _i++) {
  120. var pattern = _a[_i];
  121. var regex = Strings.extendedRegExp(pattern);
  122. if (regex.test(next)) {
  123. return this.getSectionRecursive(path, schema.patternProperties[pattern]);
  124. }
  125. }
  126. }
  127. else if (typeof schema.additionalProperties === 'object') {
  128. return this.getSectionRecursive(path, schema.additionalProperties);
  129. }
  130. else if (next.match('[0-9]+')) {
  131. if (Array.isArray(schema.items)) {
  132. var index = parseInt(next, 10);
  133. if (!isNaN(index) && schema.items[index]) {
  134. return this.getSectionRecursive(path, schema.items[index]);
  135. }
  136. }
  137. else if (schema.items) {
  138. return this.getSectionRecursive(path, schema.items);
  139. }
  140. }
  141. return undefined;
  142. };
  143. return ResolvedSchema;
  144. }());
  145. export { ResolvedSchema };
  146. var JSONSchemaService = /** @class */ (function () {
  147. function JSONSchemaService(requestService, contextService, promiseConstructor) {
  148. this.contextService = contextService;
  149. this.requestService = requestService;
  150. this.promiseConstructor = promiseConstructor || Promise;
  151. this.callOnDispose = [];
  152. this.contributionSchemas = {};
  153. this.contributionAssociations = [];
  154. this.schemasById = {};
  155. this.filePatternAssociations = [];
  156. this.registeredSchemasIds = {};
  157. }
  158. JSONSchemaService.prototype.getRegisteredSchemaIds = function (filter) {
  159. return Object.keys(this.registeredSchemasIds).filter(function (id) {
  160. var scheme = URI.parse(id).scheme;
  161. return scheme !== 'schemaservice' && (!filter || filter(scheme));
  162. });
  163. };
  164. Object.defineProperty(JSONSchemaService.prototype, "promise", {
  165. get: function () {
  166. return this.promiseConstructor;
  167. },
  168. enumerable: false,
  169. configurable: true
  170. });
  171. JSONSchemaService.prototype.dispose = function () {
  172. while (this.callOnDispose.length > 0) {
  173. this.callOnDispose.pop()();
  174. }
  175. };
  176. JSONSchemaService.prototype.onResourceChange = function (uri) {
  177. var _this = this;
  178. var hasChanges = false;
  179. uri = normalizeId(uri);
  180. var toWalk = [uri];
  181. var all = Object.keys(this.schemasById).map(function (key) { return _this.schemasById[key]; });
  182. while (toWalk.length) {
  183. var curr = toWalk.pop();
  184. for (var i = 0; i < all.length; i++) {
  185. var handle = all[i];
  186. if (handle && (handle.url === curr || handle.dependencies[curr])) {
  187. if (handle.url !== curr) {
  188. toWalk.push(handle.url);
  189. }
  190. handle.clearSchema();
  191. all[i] = undefined;
  192. hasChanges = true;
  193. }
  194. }
  195. }
  196. return hasChanges;
  197. };
  198. JSONSchemaService.prototype.setSchemaContributions = function (schemaContributions) {
  199. if (schemaContributions.schemas) {
  200. var schemas = schemaContributions.schemas;
  201. for (var id in schemas) {
  202. var normalizedId = normalizeId(id);
  203. this.contributionSchemas[normalizedId] = this.addSchemaHandle(normalizedId, schemas[id]);
  204. }
  205. }
  206. if (Array.isArray(schemaContributions.schemaAssociations)) {
  207. var schemaAssociations = schemaContributions.schemaAssociations;
  208. for (var _i = 0, schemaAssociations_1 = schemaAssociations; _i < schemaAssociations_1.length; _i++) {
  209. var schemaAssociation = schemaAssociations_1[_i];
  210. var uris = schemaAssociation.uris.map(normalizeId);
  211. var association = this.addFilePatternAssociation(schemaAssociation.pattern, uris);
  212. this.contributionAssociations.push(association);
  213. }
  214. }
  215. };
  216. JSONSchemaService.prototype.addSchemaHandle = function (id, unresolvedSchemaContent) {
  217. var schemaHandle = new SchemaHandle(this, id, unresolvedSchemaContent);
  218. this.schemasById[id] = schemaHandle;
  219. return schemaHandle;
  220. };
  221. JSONSchemaService.prototype.getOrAddSchemaHandle = function (id, unresolvedSchemaContent) {
  222. return this.schemasById[id] || this.addSchemaHandle(id, unresolvedSchemaContent);
  223. };
  224. JSONSchemaService.prototype.addFilePatternAssociation = function (pattern, uris) {
  225. var fpa = new FilePatternAssociation(pattern, uris);
  226. this.filePatternAssociations.push(fpa);
  227. return fpa;
  228. };
  229. JSONSchemaService.prototype.registerExternalSchema = function (uri, filePatterns, unresolvedSchemaContent) {
  230. var id = normalizeId(uri);
  231. this.registeredSchemasIds[id] = true;
  232. this.cachedSchemaForResource = undefined;
  233. if (filePatterns) {
  234. this.addFilePatternAssociation(filePatterns, [uri]);
  235. }
  236. return unresolvedSchemaContent ? this.addSchemaHandle(id, unresolvedSchemaContent) : this.getOrAddSchemaHandle(id);
  237. };
  238. JSONSchemaService.prototype.clearExternalSchemas = function () {
  239. this.schemasById = {};
  240. this.filePatternAssociations = [];
  241. this.registeredSchemasIds = {};
  242. this.cachedSchemaForResource = undefined;
  243. for (var id in this.contributionSchemas) {
  244. this.schemasById[id] = this.contributionSchemas[id];
  245. this.registeredSchemasIds[id] = true;
  246. }
  247. for (var _i = 0, _a = this.contributionAssociations; _i < _a.length; _i++) {
  248. var contributionAssociation = _a[_i];
  249. this.filePatternAssociations.push(contributionAssociation);
  250. }
  251. };
  252. JSONSchemaService.prototype.getResolvedSchema = function (schemaId) {
  253. var id = normalizeId(schemaId);
  254. var schemaHandle = this.schemasById[id];
  255. if (schemaHandle) {
  256. return schemaHandle.getResolvedSchema();
  257. }
  258. return this.promise.resolve(undefined);
  259. };
  260. JSONSchemaService.prototype.loadSchema = function (url) {
  261. if (!this.requestService) {
  262. var errorMessage = localize('json.schema.norequestservice', 'Unable to load schema from \'{0}\'. No schema request service available', toDisplayString(url));
  263. return this.promise.resolve(new UnresolvedSchema({}, [errorMessage]));
  264. }
  265. return this.requestService(url).then(function (content) {
  266. if (!content) {
  267. var errorMessage = localize('json.schema.nocontent', 'Unable to load schema from \'{0}\': No content.', toDisplayString(url));
  268. return new UnresolvedSchema({}, [errorMessage]);
  269. }
  270. var schemaContent = {};
  271. var jsonErrors = [];
  272. schemaContent = Json.parse(content, jsonErrors);
  273. var errors = jsonErrors.length ? [localize('json.schema.invalidFormat', 'Unable to parse content from \'{0}\': Parse error at offset {1}.', toDisplayString(url), jsonErrors[0].offset)] : [];
  274. return new UnresolvedSchema(schemaContent, errors);
  275. }, function (error) {
  276. var errorMessage = error.toString();
  277. var errorSplit = error.toString().split('Error: ');
  278. if (errorSplit.length > 1) {
  279. // more concise error message, URL and context are attached by caller anyways
  280. errorMessage = errorSplit[1];
  281. }
  282. if (Strings.endsWith(errorMessage, '.')) {
  283. errorMessage = errorMessage.substr(0, errorMessage.length - 1);
  284. }
  285. return new UnresolvedSchema({}, [localize('json.schema.nocontent', 'Unable to load schema from \'{0}\': {1}.', toDisplayString(url), errorMessage)]);
  286. });
  287. };
  288. JSONSchemaService.prototype.resolveSchemaContent = function (schemaToResolve, schemaURL, dependencies) {
  289. var _this = this;
  290. var resolveErrors = schemaToResolve.errors.slice(0);
  291. var schema = schemaToResolve.schema;
  292. if (schema.$schema) {
  293. var id = normalizeId(schema.$schema);
  294. if (id === 'http://json-schema.org/draft-03/schema') {
  295. return this.promise.resolve(new ResolvedSchema({}, [localize('json.schema.draft03.notsupported', "Draft-03 schemas are not supported.")]));
  296. }
  297. else if (id === 'https://json-schema.org/draft/2019-09/schema') {
  298. resolveErrors.push(localize('json.schema.draft201909.notsupported', "Draft 2019-09 schemas are not yet fully supported."));
  299. }
  300. }
  301. var contextService = this.contextService;
  302. var findSection = function (schema, path) {
  303. if (!path) {
  304. return schema;
  305. }
  306. var current = schema;
  307. if (path[0] === '/') {
  308. path = path.substr(1);
  309. }
  310. path.split('/').some(function (part) {
  311. part = part.replace(/~1/g, '/').replace(/~0/g, '~');
  312. current = current[part];
  313. return !current;
  314. });
  315. return current;
  316. };
  317. var merge = function (target, sourceRoot, sourceURI, refSegment) {
  318. var path = refSegment ? decodeURIComponent(refSegment) : undefined;
  319. var section = findSection(sourceRoot, path);
  320. if (section) {
  321. for (var key in section) {
  322. if (section.hasOwnProperty(key) && !target.hasOwnProperty(key)) {
  323. target[key] = section[key];
  324. }
  325. }
  326. }
  327. else {
  328. resolveErrors.push(localize('json.schema.invalidref', '$ref \'{0}\' in \'{1}\' can not be resolved.', path, sourceURI));
  329. }
  330. };
  331. var resolveExternalLink = function (node, uri, refSegment, parentSchemaURL, parentSchemaDependencies) {
  332. if (contextService && !/^[A-Za-z][A-Za-z0-9+\-.+]*:\/\/.*/.test(uri)) {
  333. uri = contextService.resolveRelativePath(uri, parentSchemaURL);
  334. }
  335. uri = normalizeId(uri);
  336. var referencedHandle = _this.getOrAddSchemaHandle(uri);
  337. return referencedHandle.getUnresolvedSchema().then(function (unresolvedSchema) {
  338. parentSchemaDependencies[uri] = true;
  339. if (unresolvedSchema.errors.length) {
  340. var loc = refSegment ? uri + '#' + refSegment : uri;
  341. resolveErrors.push(localize('json.schema.problemloadingref', 'Problems loading reference \'{0}\': {1}', loc, unresolvedSchema.errors[0]));
  342. }
  343. merge(node, unresolvedSchema.schema, uri, refSegment);
  344. return resolveRefs(node, unresolvedSchema.schema, uri, referencedHandle.dependencies);
  345. });
  346. };
  347. var resolveRefs = function (node, parentSchema, parentSchemaURL, parentSchemaDependencies) {
  348. if (!node || typeof node !== 'object') {
  349. return Promise.resolve(null);
  350. }
  351. var toWalk = [node];
  352. var seen = [];
  353. var openPromises = [];
  354. var collectEntries = function () {
  355. var entries = [];
  356. for (var _i = 0; _i < arguments.length; _i++) {
  357. entries[_i] = arguments[_i];
  358. }
  359. for (var _a = 0, entries_1 = entries; _a < entries_1.length; _a++) {
  360. var entry = entries_1[_a];
  361. if (typeof entry === 'object') {
  362. toWalk.push(entry);
  363. }
  364. }
  365. };
  366. var collectMapEntries = function () {
  367. var maps = [];
  368. for (var _i = 0; _i < arguments.length; _i++) {
  369. maps[_i] = arguments[_i];
  370. }
  371. for (var _a = 0, maps_1 = maps; _a < maps_1.length; _a++) {
  372. var map = maps_1[_a];
  373. if (typeof map === 'object') {
  374. for (var k in map) {
  375. var key = k;
  376. var entry = map[key];
  377. if (typeof entry === 'object') {
  378. toWalk.push(entry);
  379. }
  380. }
  381. }
  382. }
  383. };
  384. var collectArrayEntries = function () {
  385. var arrays = [];
  386. for (var _i = 0; _i < arguments.length; _i++) {
  387. arrays[_i] = arguments[_i];
  388. }
  389. for (var _a = 0, arrays_1 = arrays; _a < arrays_1.length; _a++) {
  390. var array = arrays_1[_a];
  391. if (Array.isArray(array)) {
  392. for (var _b = 0, array_1 = array; _b < array_1.length; _b++) {
  393. var entry = array_1[_b];
  394. if (typeof entry === 'object') {
  395. toWalk.push(entry);
  396. }
  397. }
  398. }
  399. }
  400. };
  401. var handleRef = function (next) {
  402. var seenRefs = [];
  403. while (next.$ref) {
  404. var ref = next.$ref;
  405. var segments = ref.split('#', 2);
  406. delete next.$ref;
  407. if (segments[0].length > 0) {
  408. openPromises.push(resolveExternalLink(next, segments[0], segments[1], parentSchemaURL, parentSchemaDependencies));
  409. return;
  410. }
  411. else {
  412. if (seenRefs.indexOf(ref) === -1) {
  413. merge(next, parentSchema, parentSchemaURL, segments[1]); // can set next.$ref again, use seenRefs to avoid circle
  414. seenRefs.push(ref);
  415. }
  416. }
  417. }
  418. collectEntries(next.items, next.additionalItems, next.additionalProperties, next.not, next.contains, next.propertyNames, next.if, next.then, next.else);
  419. collectMapEntries(next.definitions, next.properties, next.patternProperties, next.dependencies);
  420. collectArrayEntries(next.anyOf, next.allOf, next.oneOf, next.items);
  421. };
  422. while (toWalk.length) {
  423. var next = toWalk.pop();
  424. if (seen.indexOf(next) >= 0) {
  425. continue;
  426. }
  427. seen.push(next);
  428. handleRef(next);
  429. }
  430. return _this.promise.all(openPromises);
  431. };
  432. return resolveRefs(schema, schema, schemaURL, dependencies).then(function (_) { return new ResolvedSchema(schema, resolveErrors); });
  433. };
  434. JSONSchemaService.prototype.getSchemaForResource = function (resource, document) {
  435. // first use $schema if present
  436. if (document && document.root && document.root.type === 'object') {
  437. var schemaProperties = document.root.properties.filter(function (p) { return (p.keyNode.value === '$schema') && p.valueNode && p.valueNode.type === 'string'; });
  438. if (schemaProperties.length > 0) {
  439. var valueNode = schemaProperties[0].valueNode;
  440. if (valueNode && valueNode.type === 'string') {
  441. var schemeId = Parser.getNodeValue(valueNode);
  442. if (schemeId && Strings.startsWith(schemeId, '.') && this.contextService) {
  443. schemeId = this.contextService.resolveRelativePath(schemeId, resource);
  444. }
  445. if (schemeId) {
  446. var id = normalizeId(schemeId);
  447. return this.getOrAddSchemaHandle(id).getResolvedSchema();
  448. }
  449. }
  450. }
  451. }
  452. if (this.cachedSchemaForResource && this.cachedSchemaForResource.resource === resource) {
  453. return this.cachedSchemaForResource.resolvedSchema;
  454. }
  455. var seen = Object.create(null);
  456. var schemas = [];
  457. var normalizedResource = normalizeResourceForMatching(resource);
  458. for (var _i = 0, _a = this.filePatternAssociations; _i < _a.length; _i++) {
  459. var entry = _a[_i];
  460. if (entry.matchesPattern(normalizedResource)) {
  461. for (var _b = 0, _c = entry.getURIs(); _b < _c.length; _b++) {
  462. var schemaId = _c[_b];
  463. if (!seen[schemaId]) {
  464. schemas.push(schemaId);
  465. seen[schemaId] = true;
  466. }
  467. }
  468. }
  469. }
  470. var resolvedSchema = schemas.length > 0 ? this.createCombinedSchema(resource, schemas).getResolvedSchema() : this.promise.resolve(undefined);
  471. this.cachedSchemaForResource = { resource: resource, resolvedSchema: resolvedSchema };
  472. return resolvedSchema;
  473. };
  474. JSONSchemaService.prototype.createCombinedSchema = function (resource, schemaIds) {
  475. if (schemaIds.length === 1) {
  476. return this.getOrAddSchemaHandle(schemaIds[0]);
  477. }
  478. else {
  479. var combinedSchemaId = 'schemaservice://combinedSchema/' + encodeURIComponent(resource);
  480. var combinedSchema = {
  481. allOf: schemaIds.map(function (schemaId) { return ({ $ref: schemaId }); })
  482. };
  483. return this.addSchemaHandle(combinedSchemaId, combinedSchema);
  484. }
  485. };
  486. JSONSchemaService.prototype.getMatchingSchemas = function (document, jsonDocument, schema) {
  487. if (schema) {
  488. var id = schema.id || ('schemaservice://untitled/matchingSchemas/' + idCounter++);
  489. return this.resolveSchemaContent(new UnresolvedSchema(schema), id, {}).then(function (resolvedSchema) {
  490. return jsonDocument.getMatchingSchemas(resolvedSchema.schema).filter(function (s) { return !s.inverted; });
  491. });
  492. }
  493. return this.getSchemaForResource(document.uri, jsonDocument).then(function (schema) {
  494. if (schema) {
  495. return jsonDocument.getMatchingSchemas(schema.schema).filter(function (s) { return !s.inverted; });
  496. }
  497. return [];
  498. });
  499. };
  500. return JSONSchemaService;
  501. }());
  502. export { JSONSchemaService };
  503. var idCounter = 0;
  504. function normalizeId(id) {
  505. // remove trailing '#', normalize drive capitalization
  506. try {
  507. return URI.parse(id).toString();
  508. }
  509. catch (e) {
  510. return id;
  511. }
  512. }
  513. function normalizeResourceForMatching(resource) {
  514. // remove queries and fragments, normalize drive capitalization
  515. try {
  516. return URI.parse(resource).with({ fragment: null, query: null }).toString();
  517. }
  518. catch (e) {
  519. return resource;
  520. }
  521. }
  522. function toDisplayString(url) {
  523. try {
  524. var uri = URI.parse(url);
  525. if (uri.scheme === 'file') {
  526. return uri.fsPath;
  527. }
  528. }
  529. catch (e) {
  530. // ignore
  531. }
  532. return url;
  533. }