php.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. /*
  2. Language: PHP
  3. Author: Victor Karamzin <Victor.Karamzin@enterra-inc.com>
  4. Contributors: Evgeny Stepanischev <imbolk@gmail.com>, Ivan Sagalaev <maniac@softwaremaniacs.org>
  5. Website: https://www.php.net
  6. Category: common
  7. */
  8. /**
  9. * @param {HLJSApi} hljs
  10. * @returns {LanguageDetail}
  11. * */
  12. function php(hljs) {
  13. const regex = hljs.regex;
  14. // negative look-ahead tries to avoid matching patterns that are not
  15. // Perl at all like $ident$, @ident@, etc.
  16. const NOT_PERL_ETC = /(?![A-Za-z0-9])(?![$])/;
  17. const IDENT_RE = regex.concat(
  18. /[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/,
  19. NOT_PERL_ETC);
  20. // Will not detect camelCase classes
  21. const PASCAL_CASE_CLASS_NAME_RE = regex.concat(
  22. /(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/,
  23. NOT_PERL_ETC);
  24. const VARIABLE = {
  25. scope: 'variable',
  26. match: '\\$+' + IDENT_RE,
  27. };
  28. const PREPROCESSOR = {
  29. scope: 'meta',
  30. variants: [
  31. { begin: /<\?php/, relevance: 10 }, // boost for obvious PHP
  32. { begin: /<\?=/ },
  33. // less relevant per PSR-1 which says not to use short-tags
  34. { begin: /<\?/, relevance: 0.1 },
  35. { begin: /\?>/ } // end php tag
  36. ]
  37. };
  38. const SUBST = {
  39. scope: 'subst',
  40. variants: [
  41. { begin: /\$\w+/ },
  42. {
  43. begin: /\{\$/,
  44. end: /\}/
  45. }
  46. ]
  47. };
  48. const SINGLE_QUOTED = hljs.inherit(hljs.APOS_STRING_MODE, { illegal: null, });
  49. const DOUBLE_QUOTED = hljs.inherit(hljs.QUOTE_STRING_MODE, {
  50. illegal: null,
  51. contains: hljs.QUOTE_STRING_MODE.contains.concat(SUBST),
  52. });
  53. const HEREDOC = {
  54. begin: /<<<[ \t]*(?:(\w+)|"(\w+)")\n/,
  55. end: /[ \t]*(\w+)\b/,
  56. contains: hljs.QUOTE_STRING_MODE.contains.concat(SUBST),
  57. 'on:begin': (m, resp) => { resp.data._beginMatch = m[1] || m[2]; },
  58. 'on:end': (m, resp) => { if (resp.data._beginMatch !== m[1]) resp.ignoreMatch(); },
  59. };
  60. const NOWDOC = hljs.END_SAME_AS_BEGIN({
  61. begin: /<<<[ \t]*'(\w+)'\n/,
  62. end: /[ \t]*(\w+)\b/,
  63. });
  64. // list of valid whitespaces because non-breaking space might be part of a IDENT_RE
  65. const WHITESPACE = '[ \t\n]';
  66. const STRING = {
  67. scope: 'string',
  68. variants: [
  69. DOUBLE_QUOTED,
  70. SINGLE_QUOTED,
  71. HEREDOC,
  72. NOWDOC
  73. ]
  74. };
  75. const NUMBER = {
  76. scope: 'number',
  77. variants: [
  78. { begin: `\\b0[bB][01]+(?:_[01]+)*\\b` }, // Binary w/ underscore support
  79. { begin: `\\b0[oO][0-7]+(?:_[0-7]+)*\\b` }, // Octals w/ underscore support
  80. { begin: `\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b` }, // Hex w/ underscore support
  81. // Decimals w/ underscore support, with optional fragments and scientific exponent (e) suffix.
  82. { begin: `(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?` }
  83. ],
  84. relevance: 0
  85. };
  86. const LITERALS = [
  87. "false",
  88. "null",
  89. "true"
  90. ];
  91. const KWS = [
  92. // Magic constants:
  93. // <https://www.php.net/manual/en/language.constants.predefined.php>
  94. "__CLASS__",
  95. "__DIR__",
  96. "__FILE__",
  97. "__FUNCTION__",
  98. "__COMPILER_HALT_OFFSET__",
  99. "__LINE__",
  100. "__METHOD__",
  101. "__NAMESPACE__",
  102. "__TRAIT__",
  103. // Function that look like language construct or language construct that look like function:
  104. // List of keywords that may not require parenthesis
  105. "die",
  106. "echo",
  107. "exit",
  108. "include",
  109. "include_once",
  110. "print",
  111. "require",
  112. "require_once",
  113. // These are not language construct (function) but operate on the currently-executing function and can access the current symbol table
  114. // 'compact extract func_get_arg func_get_args func_num_args get_called_class get_parent_class ' +
  115. // Other keywords:
  116. // <https://www.php.net/manual/en/reserved.php>
  117. // <https://www.php.net/manual/en/language.types.type-juggling.php>
  118. "array",
  119. "abstract",
  120. "and",
  121. "as",
  122. "binary",
  123. "bool",
  124. "boolean",
  125. "break",
  126. "callable",
  127. "case",
  128. "catch",
  129. "class",
  130. "clone",
  131. "const",
  132. "continue",
  133. "declare",
  134. "default",
  135. "do",
  136. "double",
  137. "else",
  138. "elseif",
  139. "empty",
  140. "enddeclare",
  141. "endfor",
  142. "endforeach",
  143. "endif",
  144. "endswitch",
  145. "endwhile",
  146. "enum",
  147. "eval",
  148. "extends",
  149. "final",
  150. "finally",
  151. "float",
  152. "for",
  153. "foreach",
  154. "from",
  155. "global",
  156. "goto",
  157. "if",
  158. "implements",
  159. "instanceof",
  160. "insteadof",
  161. "int",
  162. "integer",
  163. "interface",
  164. "isset",
  165. "iterable",
  166. "list",
  167. "match|0",
  168. "mixed",
  169. "new",
  170. "never",
  171. "object",
  172. "or",
  173. "private",
  174. "protected",
  175. "public",
  176. "readonly",
  177. "real",
  178. "return",
  179. "string",
  180. "switch",
  181. "throw",
  182. "trait",
  183. "try",
  184. "unset",
  185. "use",
  186. "var",
  187. "void",
  188. "while",
  189. "xor",
  190. "yield"
  191. ];
  192. const BUILT_INS = [
  193. // Standard PHP library:
  194. // <https://www.php.net/manual/en/book.spl.php>
  195. "Error|0",
  196. "AppendIterator",
  197. "ArgumentCountError",
  198. "ArithmeticError",
  199. "ArrayIterator",
  200. "ArrayObject",
  201. "AssertionError",
  202. "BadFunctionCallException",
  203. "BadMethodCallException",
  204. "CachingIterator",
  205. "CallbackFilterIterator",
  206. "CompileError",
  207. "Countable",
  208. "DirectoryIterator",
  209. "DivisionByZeroError",
  210. "DomainException",
  211. "EmptyIterator",
  212. "ErrorException",
  213. "Exception",
  214. "FilesystemIterator",
  215. "FilterIterator",
  216. "GlobIterator",
  217. "InfiniteIterator",
  218. "InvalidArgumentException",
  219. "IteratorIterator",
  220. "LengthException",
  221. "LimitIterator",
  222. "LogicException",
  223. "MultipleIterator",
  224. "NoRewindIterator",
  225. "OutOfBoundsException",
  226. "OutOfRangeException",
  227. "OuterIterator",
  228. "OverflowException",
  229. "ParentIterator",
  230. "ParseError",
  231. "RangeException",
  232. "RecursiveArrayIterator",
  233. "RecursiveCachingIterator",
  234. "RecursiveCallbackFilterIterator",
  235. "RecursiveDirectoryIterator",
  236. "RecursiveFilterIterator",
  237. "RecursiveIterator",
  238. "RecursiveIteratorIterator",
  239. "RecursiveRegexIterator",
  240. "RecursiveTreeIterator",
  241. "RegexIterator",
  242. "RuntimeException",
  243. "SeekableIterator",
  244. "SplDoublyLinkedList",
  245. "SplFileInfo",
  246. "SplFileObject",
  247. "SplFixedArray",
  248. "SplHeap",
  249. "SplMaxHeap",
  250. "SplMinHeap",
  251. "SplObjectStorage",
  252. "SplObserver",
  253. "SplPriorityQueue",
  254. "SplQueue",
  255. "SplStack",
  256. "SplSubject",
  257. "SplTempFileObject",
  258. "TypeError",
  259. "UnderflowException",
  260. "UnexpectedValueException",
  261. "UnhandledMatchError",
  262. // Reserved interfaces:
  263. // <https://www.php.net/manual/en/reserved.interfaces.php>
  264. "ArrayAccess",
  265. "BackedEnum",
  266. "Closure",
  267. "Fiber",
  268. "Generator",
  269. "Iterator",
  270. "IteratorAggregate",
  271. "Serializable",
  272. "Stringable",
  273. "Throwable",
  274. "Traversable",
  275. "UnitEnum",
  276. "WeakReference",
  277. "WeakMap",
  278. // Reserved classes:
  279. // <https://www.php.net/manual/en/reserved.classes.php>
  280. "Directory",
  281. "__PHP_Incomplete_Class",
  282. "parent",
  283. "php_user_filter",
  284. "self",
  285. "static",
  286. "stdClass"
  287. ];
  288. /** Dual-case keywords
  289. *
  290. * ["then","FILE"] =>
  291. * ["then", "THEN", "FILE", "file"]
  292. *
  293. * @param {string[]} items */
  294. const dualCase = (items) => {
  295. /** @type string[] */
  296. const result = [];
  297. items.forEach(item => {
  298. result.push(item);
  299. if (item.toLowerCase() === item) {
  300. result.push(item.toUpperCase());
  301. } else {
  302. result.push(item.toLowerCase());
  303. }
  304. });
  305. return result;
  306. };
  307. const KEYWORDS = {
  308. keyword: KWS,
  309. literal: dualCase(LITERALS),
  310. built_in: BUILT_INS,
  311. };
  312. /**
  313. * @param {string[]} items */
  314. const normalizeKeywords = (items) => {
  315. return items.map(item => {
  316. return item.replace(/\|\d+$/, "");
  317. });
  318. };
  319. const CONSTRUCTOR_CALL = { variants: [
  320. {
  321. match: [
  322. /new/,
  323. regex.concat(WHITESPACE, "+"),
  324. // to prevent built ins from being confused as the class constructor call
  325. regex.concat("(?!", normalizeKeywords(BUILT_INS).join("\\b|"), "\\b)"),
  326. PASCAL_CASE_CLASS_NAME_RE,
  327. ],
  328. scope: {
  329. 1: "keyword",
  330. 4: "title.class",
  331. },
  332. }
  333. ] };
  334. const CONSTANT_REFERENCE = regex.concat(IDENT_RE, "\\b(?!\\()");
  335. const LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON = { variants: [
  336. {
  337. match: [
  338. regex.concat(
  339. /::/,
  340. regex.lookahead(/(?!class\b)/)
  341. ),
  342. CONSTANT_REFERENCE,
  343. ],
  344. scope: { 2: "variable.constant", },
  345. },
  346. {
  347. match: [
  348. /::/,
  349. /class/,
  350. ],
  351. scope: { 2: "variable.language", },
  352. },
  353. {
  354. match: [
  355. PASCAL_CASE_CLASS_NAME_RE,
  356. regex.concat(
  357. /::/,
  358. regex.lookahead(/(?!class\b)/)
  359. ),
  360. CONSTANT_REFERENCE,
  361. ],
  362. scope: {
  363. 1: "title.class",
  364. 3: "variable.constant",
  365. },
  366. },
  367. {
  368. match: [
  369. PASCAL_CASE_CLASS_NAME_RE,
  370. regex.concat(
  371. "::",
  372. regex.lookahead(/(?!class\b)/)
  373. ),
  374. ],
  375. scope: { 1: "title.class", },
  376. },
  377. {
  378. match: [
  379. PASCAL_CASE_CLASS_NAME_RE,
  380. /::/,
  381. /class/,
  382. ],
  383. scope: {
  384. 1: "title.class",
  385. 3: "variable.language",
  386. },
  387. }
  388. ] };
  389. const NAMED_ARGUMENT = {
  390. scope: 'attr',
  391. match: regex.concat(IDENT_RE, regex.lookahead(':'), regex.lookahead(/(?!::)/)),
  392. };
  393. const PARAMS_MODE = {
  394. relevance: 0,
  395. begin: /\(/,
  396. end: /\)/,
  397. keywords: KEYWORDS,
  398. contains: [
  399. NAMED_ARGUMENT,
  400. VARIABLE,
  401. LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON,
  402. hljs.C_BLOCK_COMMENT_MODE,
  403. STRING,
  404. NUMBER,
  405. CONSTRUCTOR_CALL,
  406. ],
  407. };
  408. const FUNCTION_INVOKE = {
  409. relevance: 0,
  410. match: [
  411. /\b/,
  412. // to prevent keywords from being confused as the function title
  413. regex.concat("(?!fn\\b|function\\b|", normalizeKeywords(KWS).join("\\b|"), "|", normalizeKeywords(BUILT_INS).join("\\b|"), "\\b)"),
  414. IDENT_RE,
  415. regex.concat(WHITESPACE, "*"),
  416. regex.lookahead(/(?=\()/)
  417. ],
  418. scope: { 3: "title.function.invoke", },
  419. contains: [ PARAMS_MODE ]
  420. };
  421. PARAMS_MODE.contains.push(FUNCTION_INVOKE);
  422. const ATTRIBUTE_CONTAINS = [
  423. NAMED_ARGUMENT,
  424. LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON,
  425. hljs.C_BLOCK_COMMENT_MODE,
  426. STRING,
  427. NUMBER,
  428. CONSTRUCTOR_CALL,
  429. ];
  430. const ATTRIBUTES = {
  431. begin: regex.concat(/#\[\s*/, PASCAL_CASE_CLASS_NAME_RE),
  432. beginScope: "meta",
  433. end: /]/,
  434. endScope: "meta",
  435. keywords: {
  436. literal: LITERALS,
  437. keyword: [
  438. 'new',
  439. 'array',
  440. ]
  441. },
  442. contains: [
  443. {
  444. begin: /\[/,
  445. end: /]/,
  446. keywords: {
  447. literal: LITERALS,
  448. keyword: [
  449. 'new',
  450. 'array',
  451. ]
  452. },
  453. contains: [
  454. 'self',
  455. ...ATTRIBUTE_CONTAINS,
  456. ]
  457. },
  458. ...ATTRIBUTE_CONTAINS,
  459. {
  460. scope: 'meta',
  461. match: PASCAL_CASE_CLASS_NAME_RE
  462. }
  463. ]
  464. };
  465. return {
  466. case_insensitive: false,
  467. keywords: KEYWORDS,
  468. contains: [
  469. ATTRIBUTES,
  470. hljs.HASH_COMMENT_MODE,
  471. hljs.COMMENT('//', '$'),
  472. hljs.COMMENT(
  473. '/\\*',
  474. '\\*/',
  475. { contains: [
  476. {
  477. scope: 'doctag',
  478. match: '@[A-Za-z]+'
  479. }
  480. ] }
  481. ),
  482. {
  483. match: /__halt_compiler\(\);/,
  484. keywords: '__halt_compiler',
  485. starts: {
  486. scope: "comment",
  487. end: hljs.MATCH_NOTHING_RE,
  488. contains: [
  489. {
  490. match: /\?>/,
  491. scope: "meta",
  492. endsParent: true
  493. }
  494. ]
  495. }
  496. },
  497. PREPROCESSOR,
  498. {
  499. scope: 'variable.language',
  500. match: /\$this\b/
  501. },
  502. VARIABLE,
  503. FUNCTION_INVOKE,
  504. LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON,
  505. {
  506. match: [
  507. /const/,
  508. /\s/,
  509. IDENT_RE,
  510. ],
  511. scope: {
  512. 1: "keyword",
  513. 3: "variable.constant",
  514. },
  515. },
  516. CONSTRUCTOR_CALL,
  517. {
  518. scope: 'function',
  519. relevance: 0,
  520. beginKeywords: 'fn function',
  521. end: /[;{]/,
  522. excludeEnd: true,
  523. illegal: '[$%\\[]',
  524. contains: [
  525. { beginKeywords: 'use', },
  526. hljs.UNDERSCORE_TITLE_MODE,
  527. {
  528. begin: '=>', // No markup, just a relevance booster
  529. endsParent: true
  530. },
  531. {
  532. scope: 'params',
  533. begin: '\\(',
  534. end: '\\)',
  535. excludeBegin: true,
  536. excludeEnd: true,
  537. keywords: KEYWORDS,
  538. contains: [
  539. 'self',
  540. VARIABLE,
  541. LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON,
  542. hljs.C_BLOCK_COMMENT_MODE,
  543. STRING,
  544. NUMBER
  545. ]
  546. },
  547. ]
  548. },
  549. {
  550. scope: 'class',
  551. variants: [
  552. {
  553. beginKeywords: "enum",
  554. illegal: /[($"]/
  555. },
  556. {
  557. beginKeywords: "class interface trait",
  558. illegal: /[:($"]/
  559. }
  560. ],
  561. relevance: 0,
  562. end: /\{/,
  563. excludeEnd: true,
  564. contains: [
  565. { beginKeywords: 'extends implements' },
  566. hljs.UNDERSCORE_TITLE_MODE
  567. ]
  568. },
  569. // both use and namespace still use "old style" rules (vs multi-match)
  570. // because the namespace name can include `\` and we still want each
  571. // element to be treated as its own *individual* title
  572. {
  573. beginKeywords: 'namespace',
  574. relevance: 0,
  575. end: ';',
  576. illegal: /[.']/,
  577. contains: [ hljs.inherit(hljs.UNDERSCORE_TITLE_MODE, { scope: "title.class" }) ]
  578. },
  579. {
  580. beginKeywords: 'use',
  581. relevance: 0,
  582. end: ';',
  583. contains: [
  584. // TODO: title.function vs title.class
  585. {
  586. match: /\b(as|const|function)\b/,
  587. scope: "keyword"
  588. },
  589. // TODO: could be title.class or title.function
  590. hljs.UNDERSCORE_TITLE_MODE
  591. ]
  592. },
  593. STRING,
  594. NUMBER,
  595. ]
  596. };
  597. }
  598. export { php as default };