UNPKG

16 kBJavaScriptView Raw
1"use strict";
2
3const {
4 hasNewline,
5 skipEverythingButNewLine,
6 skipNewline,
7 isNextLineEmpty: _isNextLineEmpty,
8 isPreviousLineEmpty: _isPreviousLineEmpty,
9 getNextNonSpaceNonCommentCharacterIndex: _getNextNonSpaceNonCommentCharacterIndex,
10} = require("prettier").util;
11
12const prettierVersion = require("prettier").version;
13
14function lookupIfPrettier2(options, prop) {
15 return parseInt(prettierVersion[0]) > 1 ? options[prop] : options;
16}
17
18function isPreviousLineEmpty(text, node, options) {
19 return _isPreviousLineEmpty(
20 text,
21 node,
22 lookupIfPrettier2(options, "locStart")
23 );
24}
25
26function isNextLineEmpty(text, node, options) {
27 return _isNextLineEmpty(text, node, lookupIfPrettier2(options, "locEnd"));
28}
29
30function getNextNonSpaceNonCommentCharacterIndex(text, node, options) {
31 return _getNextNonSpaceNonCommentCharacterIndex(
32 text,
33 node,
34 lookupIfPrettier2(options, "locEnd")
35 );
36}
37
38function printNumber(rawNumber) {
39 return (
40 rawNumber
41 .toLowerCase()
42 // Remove unnecessary plus and zeroes from scientific notation.
43 .replace(/^([+-]?[\d.]+e)(?:\+|(-))?0*(\d)/, "$1$2$3")
44 // Remove unnecessary scientific notation (1e0).
45 .replace(/^([+-]?[\d.]+)e[+-]?0+$/, "$1")
46 // Make sure numbers always start with a digit.
47 .replace(/^([+-])?\./, "$10.")
48 // Remove extraneous trailing decimal zeroes.
49 .replace(/(\.\d+?)0+(?=e|$)/, "$1")
50 // Remove unnecessary .e notation
51 .replace(/\.(?=e)/, "")
52 );
53}
54
55// http://php.net/manual/en/language.operators.precedence.php
56const PRECEDENCE = {};
57[
58 ["or"],
59 ["xor"],
60 ["and"],
61 [
62 "=",
63 "+=",
64 "-=",
65 "*=",
66 "**=",
67 "/=",
68 ".=",
69 "%=",
70 "&=",
71 "|=",
72 "^=",
73 "<<=",
74 ">>=",
75 ],
76 ["??"],
77 ["||"],
78 ["&&"],
79 ["|"],
80 ["^"],
81 ["&"],
82 ["==", "===", "!=", "!==", "<>", "<=>"],
83 ["<", ">", "<=", ">="],
84 [">>", "<<"],
85 ["+", "-", "."],
86 ["*", "/", "%"],
87 ["!"],
88 ["instanceof"],
89 ["++", "--", "~"],
90 ["**"],
91].forEach((tier, i) => {
92 tier.forEach((op) => {
93 PRECEDENCE[op] = i;
94 });
95});
96
97function getPrecedence(op) {
98 return PRECEDENCE[op];
99}
100
101const equalityOperators = ["==", "!=", "===", "!==", "<>", "<=>"];
102const multiplicativeOperators = ["*", "/", "%"];
103const bitshiftOperators = [">>", "<<"];
104
105function isBitwiseOperator(operator) {
106 return (
107 !!bitshiftOperators[operator] ||
108 operator === "|" ||
109 operator === "^" ||
110 operator === "&"
111 );
112}
113
114function shouldFlatten(parentOp, nodeOp) {
115 if (getPrecedence(nodeOp) !== getPrecedence(parentOp)) {
116 return false;
117 }
118
119 // ** is right-associative
120 // x ** y ** z --> x ** (y ** z)
121 if (parentOp === "**") {
122 return false;
123 }
124
125 // x == y == z --> (x == y) == z
126 if (
127 equalityOperators.includes(parentOp) &&
128 equalityOperators.includes(nodeOp)
129 ) {
130 return false;
131 }
132
133 // x * y % z --> (x * y) % z
134 if (
135 (nodeOp === "%" && multiplicativeOperators.includes(parentOp)) ||
136 (parentOp === "%" && multiplicativeOperators.includes(nodeOp))
137 ) {
138 return false;
139 }
140
141 // x * y / z --> (x * y) / z
142 // x / y * z --> (x / y) * z
143 if (
144 nodeOp !== parentOp &&
145 multiplicativeOperators.includes(nodeOp) &&
146 multiplicativeOperators.includes(parentOp)
147 ) {
148 return false;
149 }
150
151 // x << y << z --> (x << y) << z
152 if (
153 bitshiftOperators.includes(parentOp) &&
154 bitshiftOperators.includes(nodeOp)
155 ) {
156 return false;
157 }
158
159 return true;
160}
161
162function nodeHasStatement(node) {
163 return [
164 "block",
165 "program",
166 "namespace",
167 "class",
168 "interface",
169 "trait",
170 "traituse",
171 "declare",
172 ].includes(node.kind);
173}
174
175function getBodyFirstChild({ body }) {
176 if (!body) {
177 return null;
178 }
179 if (body.kind === "block") {
180 body = body.children;
181 }
182 return body[0];
183}
184
185function getNodeListProperty(node) {
186 const body = node.children || node.body || node.adaptations;
187 return Array.isArray(body) ? body : null;
188}
189
190function getParentNodeListProperty(path) {
191 const parent = path.getParentNode();
192 if (!parent) {
193 return null;
194 }
195 return getNodeListProperty(parent);
196}
197
198function getLast(arr) {
199 if (arr.length > 0) {
200 return arr[arr.length - 1];
201 }
202 return null;
203}
204
205function getPenultimate(arr) {
206 if (arr.length > 1) {
207 return arr[arr.length - 2];
208 }
209 return null;
210}
211
212function isLastStatement(path) {
213 const body = getParentNodeListProperty(path);
214 if (!body) {
215 return true;
216 }
217 const node = path.getValue();
218 return body[body.length - 1] === node;
219}
220
221function isFirstChildrenInlineNode(path) {
222 const node = path.getValue();
223
224 if (node.kind === "program") {
225 const children = getNodeListProperty(node);
226
227 if (!children || children.length === 0) {
228 return false;
229 }
230
231 return children[0].kind === "inline";
232 }
233
234 if (node.kind === "switch") {
235 if (!node.body) {
236 return false;
237 }
238
239 const children = getNodeListProperty(node.body);
240
241 if (children.length === 0) {
242 return false;
243 }
244
245 const [firstCase] = children;
246
247 if (!firstCase.body) {
248 return false;
249 }
250
251 const firstCaseChildren = getNodeListProperty(firstCase.body);
252
253 if (firstCaseChildren.length === 0) {
254 return false;
255 }
256
257 return firstCaseChildren[0].kind === "inline";
258 }
259
260 const firstChild = getBodyFirstChild(node);
261
262 if (!firstChild) {
263 return false;
264 }
265
266 return firstChild.kind === "inline";
267}
268
269function isDocNode(node) {
270 return (
271 node.kind === "nowdoc" ||
272 (node.kind === "encapsed" && node.type === "heredoc")
273 );
274}
275
276/**
277 * Heredoc/Nowdoc nodes need a trailing linebreak if they
278 * appear as function arguments or array elements
279 */
280function docShouldHaveTrailingNewline(path, recurse = 0) {
281 const node = path.getNode(recurse);
282 const parent = path.getNode(recurse + 1);
283 const parentParent = path.getNode(recurse + 2);
284
285 if (!parent) {
286 return false;
287 }
288
289 if (
290 (parentParent &&
291 ["call", "new", "echo"].includes(parentParent.kind) &&
292 !["call", "array"].includes(parent.kind)) ||
293 parent.kind === "parameter"
294 ) {
295 const lastIndex = parentParent.arguments.length - 1;
296 const index = parentParent.arguments.indexOf(parent);
297
298 return index !== lastIndex;
299 }
300
301 if (parentParent && parentParent.kind === "for") {
302 const initIndex = parentParent.init.indexOf(parent);
303
304 if (initIndex !== -1) {
305 return initIndex !== parentParent.init.length - 1;
306 }
307
308 const testIndex = parentParent.test.indexOf(parent);
309
310 if (testIndex !== -1) {
311 return testIndex !== parentParent.test.length - 1;
312 }
313
314 const incrementIndex = parentParent.increment.indexOf(parent);
315
316 if (incrementIndex !== -1) {
317 return incrementIndex !== parentParent.increment.length - 1;
318 }
319 }
320
321 if (parent.kind === "bin") {
322 return (
323 parent.left === node || docShouldHaveTrailingNewline(path, recurse + 1)
324 );
325 }
326
327 if (parent.kind === "case" && parent.test === node) {
328 return true;
329 }
330
331 if (parent.kind === "staticvariable") {
332 const lastIndex = parentParent.variables.length - 1;
333 const index = parentParent.variables.indexOf(parent);
334
335 return index !== lastIndex;
336 }
337
338 if (parent.kind === "entry") {
339 if (parent.key === node) {
340 return true;
341 }
342
343 const lastIndex = parentParent.items.length - 1;
344 const index = parentParent.items.indexOf(parent);
345
346 return index !== lastIndex;
347 }
348
349 if (["call", "new"].includes(parent.kind)) {
350 const lastIndex = parent.arguments.length - 1;
351 const index = parent.arguments.indexOf(node);
352
353 return index !== lastIndex;
354 }
355
356 if (parent.kind === "echo") {
357 const lastIndex = parent.expressions.length - 1;
358 const index = parent.expressions.indexOf(node);
359
360 return index !== lastIndex;
361 }
362
363 if (parent.kind === "array") {
364 const lastIndex = parent.items.length - 1;
365 const index = parent.items.indexOf(node);
366
367 return index !== lastIndex;
368 }
369
370 if (parent.kind === "retif") {
371 return docShouldHaveTrailingNewline(path, recurse + 1);
372 }
373
374 return false;
375}
376
377function lineShouldEndWithSemicolon(path) {
378 const node = path.getValue();
379 const parentNode = path.getParentNode();
380 if (!parentNode) {
381 return false;
382 }
383 // for single line control structures written in a shortform (ie without a block),
384 // we need to make sure the single body node gets a semicolon
385 if (
386 ["for", "foreach", "while", "do", "if", "switch"].includes(
387 parentNode.kind
388 ) &&
389 node.kind !== "block" &&
390 node.kind !== "if" &&
391 (parentNode.body === node || parentNode.alternate === node)
392 ) {
393 return true;
394 }
395 if (!nodeHasStatement(parentNode)) {
396 return false;
397 }
398 if (node.kind === "echo" && node.shortForm) {
399 return false;
400 }
401 if (node.kind === "traituse") {
402 return !node.adaptations;
403 }
404 if (node.kind === "method" && node.isAbstract) {
405 return true;
406 }
407 if (node.kind === "method") {
408 const parent = path.getParentNode();
409 if (parent && parent.kind === "interface") {
410 return true;
411 }
412 }
413 return [
414 "expressionstatement",
415 "do",
416 "usegroup",
417 "classconstant",
418 "propertystatement",
419 "traitprecedence",
420 "traitalias",
421 "goto",
422 "constantstatement",
423 "global",
424 "static",
425 "echo",
426 "unset",
427 "return",
428 "break",
429 "continue",
430 "throw",
431 ].includes(node.kind);
432}
433
434function fileShouldEndWithHardline(path) {
435 const node = path.getValue();
436 const isProgramNode = node.kind === "program";
437 const lastNode = node.children && getLast(node.children);
438
439 if (!isProgramNode) {
440 return false;
441 }
442
443 if (lastNode && ["halt", "inline"].includes(lastNode.kind)) {
444 return false;
445 }
446
447 if (
448 lastNode &&
449 (lastNode.kind === "declare" || lastNode.kind === "namespace")
450 ) {
451 const lastNestedNode =
452 lastNode.children.length > 0 && getLast(lastNode.children);
453
454 if (lastNestedNode && ["halt", "inline"].includes(lastNestedNode.kind)) {
455 return false;
456 }
457 }
458
459 return true;
460}
461
462function maybeStripLeadingSlashFromUse(name) {
463 const nameWithoutLeadingSlash = name.replace(/^\\/, "");
464 if (nameWithoutLeadingSlash.indexOf("\\") !== -1) {
465 return nameWithoutLeadingSlash;
466 }
467 return name;
468}
469
470function hasDanglingComments(node) {
471 return (
472 node.comments &&
473 node.comments.some((comment) => !comment.leading && !comment.trailing)
474 );
475}
476
477function hasLeadingComment(node) {
478 return node.comments && node.comments.some((comment) => comment.leading);
479}
480
481function hasTrailingComment(node) {
482 return node.comments && node.comments.some((comment) => comment.trailing);
483}
484
485function isLookupNode(node) {
486 return (
487 node.kind === "propertylookup" ||
488 node.kind === "staticlookup" ||
489 node.kind === "offsetlookup"
490 );
491}
492
493function shouldPrintHardLineAfterStartInControlStructure(path) {
494 const node = path.getValue();
495
496 if (["try", "catch"].includes(node.kind)) {
497 return false;
498 }
499
500 return isFirstChildrenInlineNode(path);
501}
502
503function shouldPrintHardLineBeforeEndInControlStructure(path) {
504 const node = path.getValue();
505
506 if (["try", "catch"].includes(node.kind)) {
507 return true;
508 }
509
510 if (node.kind === "switch") {
511 const children = getNodeListProperty(node.body);
512
513 if (children.length === 0) {
514 return true;
515 }
516
517 const lastCase = getLast(children);
518
519 if (!lastCase.body) {
520 return true;
521 }
522
523 const childrenInCase = getNodeListProperty(lastCase.body);
524
525 if (childrenInCase.length === 0) {
526 return true;
527 }
528
529 return childrenInCase[0].kind !== "inline";
530 }
531
532 return !isFirstChildrenInlineNode(path);
533}
534
535function getAlignment(text) {
536 const lines = text.split("\n");
537 const lastLine = lines.pop();
538
539 return lastLine.length - lastLine.trimLeft().length + 1;
540}
541
542function getNextNode(path, node) {
543 const parent = path.getParentNode();
544 const children = getNodeListProperty(parent);
545
546 if (!children) {
547 return null;
548 }
549
550 const index = children.indexOf(node);
551
552 if (index === -1) {
553 return null;
554 }
555
556 return parent.children[index + 1];
557}
558
559function isProgramLikeNode(node) {
560 return ["program", "declare", "namespace"].includes(node.kind);
561}
562
563function isReferenceLikeNode(node) {
564 return [
565 "name",
566 "parentreference",
567 "selfreference",
568 "staticreference",
569 ].includes(node.kind);
570}
571
572// Return `logical` value for `bin` node containing `||` or `&&` type otherwise return kind of node.
573// Require for grouping logical and binary nodes in right way.
574function getNodeKindIncludingLogical(node) {
575 if (node.kind === "bin" && ["||", "&&"].includes(node.type)) {
576 return "logical";
577 }
578
579 return node.kind;
580}
581
582/**
583 * Check if string can safely be converted from double to single quotes, i.e.
584 *
585 * - no embedded variables ("foo $bar")
586 * - no linebreaks
587 * - no special characters like \n, \t, ...
588 * - no octal/hex/unicode characters
589 *
590 * See http://php.net/manual/en/language.types.string.php#language.types.string.syntax.double
591 */
592function useDoubleQuote(node, options) {
593 if (node.isDoubleQuote && options.singleQuote) {
594 const rawValue = node.raw.slice(node.raw[0] === "b" ? 2 : 1, -1);
595
596 return rawValue.match(
597 /\\([$nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3}|u{([0-9a-fA-F]+)})|\r?\n|'|"/
598 );
599 }
600
601 return node.isDoubleQuote;
602}
603
604function hasEmptyBody(path, name = "body") {
605 const node = path.getValue();
606
607 return (
608 node[name] &&
609 node[name].children &&
610 node[name].children.length === 0 &&
611 (!node[name].comments || node[name].comments.length === 0)
612 );
613}
614
615function isNextLineEmptyAfterNamespace(text, node, locStart) {
616 let idx = locStart(node);
617 idx = skipEverythingButNewLine(text, idx);
618 idx = skipNewline(text, idx);
619 return hasNewline(text, idx);
620}
621
622function shouldPrintHardlineBeforeTrailingComma(lastElem) {
623 if (
624 lastElem.kind === "nowdoc" ||
625 (lastElem.kind === "encapsed" && lastElem.type === "heredoc")
626 ) {
627 return true;
628 }
629
630 if (
631 lastElem.kind === "entry" &&
632 (lastElem.value.kind === "nowdoc" ||
633 (lastElem.value.kind === "encapsed" && lastElem.value.type === "heredoc"))
634 ) {
635 return true;
636 }
637
638 return false;
639}
640
641function getAncestorCounter(path, typeOrTypes) {
642 const types = [].concat(typeOrTypes);
643 let counter = -1;
644 let ancestorNode;
645 while ((ancestorNode = path.getParentNode(++counter))) {
646 if (types.indexOf(ancestorNode.kind) !== -1) {
647 return counter;
648 }
649 }
650 return -1;
651}
652
653function getAncestorNode(path, typeOrTypes) {
654 const counter = getAncestorCounter(path, typeOrTypes);
655 return counter === -1 ? null : path.getParentNode(counter);
656}
657
658const magicMethods = [
659 "__construct",
660 "__destruct",
661 "__call",
662 "__callStatic",
663 "__get",
664 "__set",
665 "__isset",
666 "__unset",
667 "__sleep",
668 "__wakeup",
669 "__toString",
670 "__invoke",
671 "__set_state",
672 "__clone",
673 "__debugInfo",
674];
675const MagicMethodsMap = magicMethods.reduce((map, obj) => {
676 map[obj.toLowerCase()] = obj;
677
678 return map;
679}, {});
680
681function normalizeMagicMethodName(name) {
682 const loweredName = name.toLowerCase();
683
684 if (MagicMethodsMap[loweredName]) {
685 return MagicMethodsMap[loweredName];
686 }
687
688 return name;
689}
690
691module.exports = {
692 printNumber,
693 getPrecedence,
694 isBitwiseOperator,
695 shouldFlatten,
696 nodeHasStatement,
697 getNodeListProperty,
698 getParentNodeListProperty,
699 getLast,
700 getPenultimate,
701 isLastStatement,
702 getBodyFirstChild,
703 lineShouldEndWithSemicolon,
704 fileShouldEndWithHardline,
705 maybeStripLeadingSlashFromUse,
706 hasDanglingComments,
707 hasLeadingComment,
708 hasTrailingComment,
709 docShouldHaveTrailingNewline,
710 isLookupNode,
711 isFirstChildrenInlineNode,
712 shouldPrintHardLineAfterStartInControlStructure,
713 shouldPrintHardLineBeforeEndInControlStructure,
714 getAlignment,
715 isProgramLikeNode,
716 isReferenceLikeNode,
717 getNodeKindIncludingLogical,
718 useDoubleQuote,
719 hasEmptyBody,
720 isNextLineEmptyAfterNamespace,
721 shouldPrintHardlineBeforeTrailingComma,
722 isDocNode,
723 getAncestorNode,
724 getNextNode,
725 normalizeMagicMethodName,
726 isPreviousLineEmpty,
727 isNextLineEmpty,
728 getNextNonSpaceNonCommentCharacterIndex,
729};