comments.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. import { visit, TokenKind, } from 'graphql';
  2. const MAX_LINE_LENGTH = 80;
  3. let commentsRegistry = {};
  4. export function resetComments() {
  5. commentsRegistry = {};
  6. }
  7. export function collectComment(node) {
  8. var _a;
  9. const entityName = (_a = node.name) === null || _a === void 0 ? void 0 : _a.value;
  10. if (entityName == null) {
  11. return;
  12. }
  13. pushComment(node, entityName);
  14. switch (node.kind) {
  15. case 'EnumTypeDefinition':
  16. if (node.values) {
  17. for (const value of node.values) {
  18. pushComment(value, entityName, value.name.value);
  19. }
  20. }
  21. break;
  22. case 'ObjectTypeDefinition':
  23. case 'InputObjectTypeDefinition':
  24. case 'InterfaceTypeDefinition':
  25. if (node.fields) {
  26. for (const field of node.fields) {
  27. pushComment(field, entityName, field.name.value);
  28. if (isFieldDefinitionNode(field) && field.arguments) {
  29. for (const arg of field.arguments) {
  30. pushComment(arg, entityName, field.name.value, arg.name.value);
  31. }
  32. }
  33. }
  34. }
  35. break;
  36. }
  37. }
  38. export function pushComment(node, entity, field, argument) {
  39. const comment = getComment(node);
  40. if (typeof comment !== 'string' || comment.length === 0) {
  41. return;
  42. }
  43. const keys = [entity];
  44. if (field) {
  45. keys.push(field);
  46. if (argument) {
  47. keys.push(argument);
  48. }
  49. }
  50. const path = keys.join('.');
  51. if (!commentsRegistry[path]) {
  52. commentsRegistry[path] = [];
  53. }
  54. commentsRegistry[path].push(comment);
  55. }
  56. export function printComment(comment) {
  57. return '\n# ' + comment.replace(/\n/g, '\n# ');
  58. }
  59. /**
  60. * Copyright (c) 2015-present, Facebook, Inc.
  61. *
  62. * This source code is licensed under the MIT license found in the
  63. * LICENSE file in the root directory of this source tree.
  64. */
  65. /**
  66. * NOTE: ==> This file has been modified just to add comments to the printed AST
  67. * This is a temp measure, we will move to using the original non modified printer.js ASAP.
  68. */
  69. /**
  70. * Given maybeArray, print an empty string if it is null or empty, otherwise
  71. * print all items together separated by separator if provided
  72. */
  73. function join(maybeArray, separator) {
  74. return maybeArray ? maybeArray.filter(x => x).join(separator || '') : '';
  75. }
  76. function hasMultilineItems(maybeArray) {
  77. var _a;
  78. return (_a = maybeArray === null || maybeArray === void 0 ? void 0 : maybeArray.some(str => str.includes('\n'))) !== null && _a !== void 0 ? _a : false;
  79. }
  80. function addDescription(cb) {
  81. return (node, _key, _parent, path, ancestors) => {
  82. var _a;
  83. const keys = [];
  84. const parent = path.reduce((prev, key) => {
  85. if (['fields', 'arguments', 'values'].includes(key) && prev.name) {
  86. keys.push(prev.name.value);
  87. }
  88. return prev[key];
  89. }, ancestors[0]);
  90. const key = [...keys, (_a = parent === null || parent === void 0 ? void 0 : parent.name) === null || _a === void 0 ? void 0 : _a.value].filter(Boolean).join('.');
  91. const items = [];
  92. if (node.kind.includes('Definition') && commentsRegistry[key]) {
  93. items.push(...commentsRegistry[key]);
  94. }
  95. return join([...items.map(printComment), node.description, cb(node, _key, _parent, path, ancestors)], '\n');
  96. };
  97. }
  98. function indent(maybeString) {
  99. return maybeString && ` ${maybeString.replace(/\n/g, '\n ')}`;
  100. }
  101. /**
  102. * Given array, print each item on its own line, wrapped in an
  103. * indented "{ }" block.
  104. */
  105. function block(array) {
  106. return array && array.length !== 0 ? `{\n${indent(join(array, '\n'))}\n}` : '';
  107. }
  108. /**
  109. * If maybeString is not null or empty, then wrap with start and end, otherwise
  110. * print an empty string.
  111. */
  112. function wrap(start, maybeString, end) {
  113. return maybeString ? start + maybeString + (end || '') : '';
  114. }
  115. /**
  116. * Print a block string in the indented block form by adding a leading and
  117. * trailing blank line. However, if a block string starts with whitespace and is
  118. * a single-line, adding a leading blank line would strip that whitespace.
  119. */
  120. function printBlockString(value, isDescription = false) {
  121. const escaped = value.replace(/"""/g, '\\"""');
  122. return (value[0] === ' ' || value[0] === '\t') && value.indexOf('\n') === -1
  123. ? `"""${escaped.replace(/"$/, '"\n')}"""`
  124. : `"""\n${isDescription ? escaped : indent(escaped)}\n"""`;
  125. }
  126. const printDocASTReducer = {
  127. Name: { leave: node => node.value },
  128. Variable: { leave: node => '$' + node.name },
  129. // Document
  130. Document: {
  131. leave: node => join(node.definitions, '\n\n'),
  132. },
  133. OperationDefinition: {
  134. leave: node => {
  135. const varDefs = wrap('(', join(node.variableDefinitions, ', '), ')');
  136. const prefix = join([node.operation, join([node.name, varDefs]), join(node.directives, ' ')], ' ');
  137. // the query short form.
  138. return prefix + ' ' + node.selectionSet;
  139. },
  140. },
  141. VariableDefinition: {
  142. leave: ({ variable, type, defaultValue, directives }) => variable + ': ' + type + wrap(' = ', defaultValue) + wrap(' ', join(directives, ' ')),
  143. },
  144. SelectionSet: { leave: ({ selections }) => block(selections) },
  145. Field: {
  146. leave({ alias, name, arguments: args, directives, selectionSet }) {
  147. const prefix = wrap('', alias, ': ') + name;
  148. let argsLine = prefix + wrap('(', join(args, ', '), ')');
  149. if (argsLine.length > MAX_LINE_LENGTH) {
  150. argsLine = prefix + wrap('(\n', indent(join(args, '\n')), '\n)');
  151. }
  152. return join([argsLine, join(directives, ' '), selectionSet], ' ');
  153. },
  154. },
  155. Argument: { leave: ({ name, value }) => name + ': ' + value },
  156. // Fragments
  157. FragmentSpread: {
  158. leave: ({ name, directives }) => '...' + name + wrap(' ', join(directives, ' ')),
  159. },
  160. InlineFragment: {
  161. leave: ({ typeCondition, directives, selectionSet }) => join(['...', wrap('on ', typeCondition), join(directives, ' '), selectionSet], ' '),
  162. },
  163. FragmentDefinition: {
  164. leave: ({ name, typeCondition, variableDefinitions, directives, selectionSet }) =>
  165. // Note: fragment variable definitions are experimental and may be changed
  166. // or removed in the future.
  167. `fragment ${name}${wrap('(', join(variableDefinitions, ', '), ')')} ` +
  168. `on ${typeCondition} ${wrap('', join(directives, ' '), ' ')}` +
  169. selectionSet,
  170. },
  171. // Value
  172. IntValue: { leave: ({ value }) => value },
  173. FloatValue: { leave: ({ value }) => value },
  174. StringValue: {
  175. leave: ({ value, block: isBlockString }) => {
  176. if (isBlockString) {
  177. return printBlockString(value);
  178. }
  179. return JSON.stringify(value);
  180. },
  181. },
  182. BooleanValue: { leave: ({ value }) => (value ? 'true' : 'false') },
  183. NullValue: { leave: () => 'null' },
  184. EnumValue: { leave: ({ value }) => value },
  185. ListValue: { leave: ({ values }) => '[' + join(values, ', ') + ']' },
  186. ObjectValue: { leave: ({ fields }) => '{' + join(fields, ', ') + '}' },
  187. ObjectField: { leave: ({ name, value }) => name + ': ' + value },
  188. // Directive
  189. Directive: {
  190. leave: ({ name, arguments: args }) => '@' + name + wrap('(', join(args, ', '), ')'),
  191. },
  192. // Type
  193. NamedType: { leave: ({ name }) => name },
  194. ListType: { leave: ({ type }) => '[' + type + ']' },
  195. NonNullType: { leave: ({ type }) => type + '!' },
  196. // Type System Definitions
  197. SchemaDefinition: {
  198. leave: ({ directives, operationTypes }) => join(['schema', join(directives, ' '), block(operationTypes)], ' '),
  199. },
  200. OperationTypeDefinition: {
  201. leave: ({ operation, type }) => operation + ': ' + type,
  202. },
  203. ScalarTypeDefinition: {
  204. leave: ({ name, directives }) => join(['scalar', name, join(directives, ' ')], ' '),
  205. },
  206. ObjectTypeDefinition: {
  207. leave: ({ name, interfaces, directives, fields }) => join(['type', name, wrap('implements ', join(interfaces, ' & ')), join(directives, ' '), block(fields)], ' '),
  208. },
  209. FieldDefinition: {
  210. leave: ({ name, arguments: args, type, directives }) => name +
  211. (hasMultilineItems(args)
  212. ? wrap('(\n', indent(join(args, '\n')), '\n)')
  213. : wrap('(', join(args, ', '), ')')) +
  214. ': ' +
  215. type +
  216. wrap(' ', join(directives, ' ')),
  217. },
  218. InputValueDefinition: {
  219. leave: ({ name, type, defaultValue, directives }) => join([name + ': ' + type, wrap('= ', defaultValue), join(directives, ' ')], ' '),
  220. },
  221. InterfaceTypeDefinition: {
  222. leave: ({ name, interfaces, directives, fields }) => join(['interface', name, wrap('implements ', join(interfaces, ' & ')), join(directives, ' '), block(fields)], ' '),
  223. },
  224. UnionTypeDefinition: {
  225. leave: ({ name, directives, types }) => join(['union', name, join(directives, ' '), wrap('= ', join(types, ' | '))], ' '),
  226. },
  227. EnumTypeDefinition: {
  228. leave: ({ name, directives, values }) => join(['enum', name, join(directives, ' '), block(values)], ' '),
  229. },
  230. EnumValueDefinition: {
  231. leave: ({ name, directives }) => join([name, join(directives, ' ')], ' '),
  232. },
  233. InputObjectTypeDefinition: {
  234. leave: ({ name, directives, fields }) => join(['input', name, join(directives, ' '), block(fields)], ' '),
  235. },
  236. DirectiveDefinition: {
  237. leave: ({ name, arguments: args, repeatable, locations }) => 'directive @' +
  238. name +
  239. (hasMultilineItems(args)
  240. ? wrap('(\n', indent(join(args, '\n')), '\n)')
  241. : wrap('(', join(args, ', '), ')')) +
  242. (repeatable ? ' repeatable' : '') +
  243. ' on ' +
  244. join(locations, ' | '),
  245. },
  246. SchemaExtension: {
  247. leave: ({ directives, operationTypes }) => join(['extend schema', join(directives, ' '), block(operationTypes)], ' '),
  248. },
  249. ScalarTypeExtension: {
  250. leave: ({ name, directives }) => join(['extend scalar', name, join(directives, ' ')], ' '),
  251. },
  252. ObjectTypeExtension: {
  253. leave: ({ name, interfaces, directives, fields }) => join(['extend type', name, wrap('implements ', join(interfaces, ' & ')), join(directives, ' '), block(fields)], ' '),
  254. },
  255. InterfaceTypeExtension: {
  256. leave: ({ name, interfaces, directives, fields }) => join(['extend interface', name, wrap('implements ', join(interfaces, ' & ')), join(directives, ' '), block(fields)], ' '),
  257. },
  258. UnionTypeExtension: {
  259. leave: ({ name, directives, types }) => join(['extend union', name, join(directives, ' '), wrap('= ', join(types, ' | '))], ' '),
  260. },
  261. EnumTypeExtension: {
  262. leave: ({ name, directives, values }) => join(['extend enum', name, join(directives, ' '), block(values)], ' '),
  263. },
  264. InputObjectTypeExtension: {
  265. leave: ({ name, directives, fields }) => join(['extend input', name, join(directives, ' '), block(fields)], ' '),
  266. },
  267. };
  268. const printDocASTReducerWithComments = Object.keys(printDocASTReducer).reduce((prev, key) => ({
  269. ...prev,
  270. [key]: {
  271. leave: addDescription(printDocASTReducer[key].leave),
  272. },
  273. }), {});
  274. /**
  275. * Converts an AST into a string, using one set of reasonable
  276. * formatting rules.
  277. */
  278. export function printWithComments(ast) {
  279. return visit(ast, printDocASTReducerWithComments);
  280. }
  281. function isFieldDefinitionNode(node) {
  282. return node.kind === 'FieldDefinition';
  283. }
  284. // graphql < v13 and > v15 does not export getDescription
  285. export function getDescription(node, options) {
  286. if (node.description != null) {
  287. return node.description.value;
  288. }
  289. if (options === null || options === void 0 ? void 0 : options.commentDescriptions) {
  290. return getComment(node);
  291. }
  292. }
  293. export function getComment(node) {
  294. const rawValue = getLeadingCommentBlock(node);
  295. if (rawValue !== undefined) {
  296. return dedentBlockStringValue(`\n${rawValue}`);
  297. }
  298. }
  299. export function getLeadingCommentBlock(node) {
  300. const loc = node.loc;
  301. if (!loc) {
  302. return;
  303. }
  304. const comments = [];
  305. let token = loc.startToken.prev;
  306. while (token != null &&
  307. token.kind === TokenKind.COMMENT &&
  308. token.next != null &&
  309. token.prev != null &&
  310. token.line + 1 === token.next.line &&
  311. token.line !== token.prev.line) {
  312. const value = String(token.value);
  313. comments.push(value);
  314. token = token.prev;
  315. }
  316. return comments.length > 0 ? comments.reverse().join('\n') : undefined;
  317. }
  318. export function dedentBlockStringValue(rawString) {
  319. // Expand a block string's raw value into independent lines.
  320. const lines = rawString.split(/\r\n|[\n\r]/g);
  321. // Remove common indentation from all lines but first.
  322. const commonIndent = getBlockStringIndentation(lines);
  323. if (commonIndent !== 0) {
  324. for (let i = 1; i < lines.length; i++) {
  325. lines[i] = lines[i].slice(commonIndent);
  326. }
  327. }
  328. // Remove leading and trailing blank lines.
  329. while (lines.length > 0 && isBlank(lines[0])) {
  330. lines.shift();
  331. }
  332. while (lines.length > 0 && isBlank(lines[lines.length - 1])) {
  333. lines.pop();
  334. }
  335. // Return a string of the lines joined with U+000A.
  336. return lines.join('\n');
  337. }
  338. /**
  339. * @internal
  340. */
  341. export function getBlockStringIndentation(lines) {
  342. let commonIndent = null;
  343. for (let i = 1; i < lines.length; i++) {
  344. const line = lines[i];
  345. const indent = leadingWhitespace(line);
  346. if (indent === line.length) {
  347. continue; // skip empty lines
  348. }
  349. if (commonIndent === null || indent < commonIndent) {
  350. commonIndent = indent;
  351. if (commonIndent === 0) {
  352. break;
  353. }
  354. }
  355. }
  356. return commonIndent === null ? 0 : commonIndent;
  357. }
  358. function leadingWhitespace(str) {
  359. let i = 0;
  360. while (i < str.length && (str[i] === ' ' || str[i] === '\t')) {
  361. i++;
  362. }
  363. return i;
  364. }
  365. function isBlank(str) {
  366. return leadingWhitespace(str) === str.length;
  367. }