Skip to content

Commit

Permalink
Use php-parser to parse PHP
Browse files Browse the repository at this point in the history
fixes #91
  • Loading branch information
Hirse committed Apr 24, 2017
1 parent 22a5cc3 commit 8f2948e
Show file tree
Hide file tree
Showing 7 changed files with 8,591 additions and 414 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file.
This project tries to adhere to [Semantic Versioning](http://semver.org/).


## Unreleased
### Fixed
- PHP multiline strings (see [#91](https://github.com/Hirse/brackets-outline-list/issues/91))

### Changed
- Use php-parser to parse PHP


## 1.1.0 - 2017-04-20
### Added
- Arabic translation, thanks to [__@sadik-fattah__](https://github.com/sadik-fattah)
Expand Down
13 changes: 7 additions & 6 deletions README.md
Expand Up @@ -33,7 +33,7 @@
| Pug (Jade) | RegExp | :no_entry_sign: | :x: | :x: | :no_entry_sign: |
| JavaScript, JSX | Espree | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Markdown, GitHub-Flavored-Markdown | RegExp | :heavy_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: |
| PHP | Lexer | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| PHP | php-parser | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Python | RegExp | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: |
| Ruby | RegExp | :heavy_check_mark: | :x: | :heavy_check_mark: | :x: |
| Stylus | RegExp | :x: | :x: | :no_entry_sign: | :no_entry_sign: |
Expand All @@ -53,23 +53,24 @@ To install the latest _commit_ of this extension use the built-in Brackets [Exte
Brackets Outline List is licensed under the [MIT license][MIT].

Used thirdparty software:
* [Ionicons][Ionicons] is licensed under the [MIT license][MIT]
* [Lexer][Lexer] is licensed under the [MIT license][MIT]
* [Espree][Espree] is licensed under the [BSD 2-Clause License][BSD-2-Clause]
* [Ionicons][Ionicons] is licensed under the [MIT license][MIT]
* [PostCSS Safe Parser][PostCSS] is licensed under the [MIT license][MIT]
* [htmlparser2][htmlparser2] is licensed under the [MIT license][MIT]
* [php-parser][php-parser] is licensed under the [BSD-3-Clause license][BSD-3-Clause]


[Brackets]: http://brackets.io
[Brackets Extension Manager]: https://github.com/adobe/brackets/wiki/Brackets-Extensions
[Brackets Extension Registry]: https://brackets-registry.aboutweb.com
[Brackets npm Registry]: https://github.com/zaggino/brackets-npm-registry

[Ionicons]: http://ionicons.com
[Lexer]: https://github.com/aaditmshah/lexer
[Espree]: https://github.com/eslint/espree
[Ionicons]: http://ionicons.com
[PostCSS]: https://github.com/postcss/postcss-safe-parser
[htmlparser2]: https://github.com/fb55/htmlparser2
[php-parser]: https://github.com/glayzzle/php-parser

[MIT]: http://opensource.org/licenses/MIT
[BSD-2-Clause]: https://opensource.org/licenses/BSD-2-Clause
[BSD-3-Clause]: https://opensource.org/licenses/BSD-3-Clause
[MIT]: http://opensource.org/licenses/MIT
322 changes: 93 additions & 229 deletions src/lexers/PHPLexer.js
@@ -1,247 +1,111 @@
define(function PHPLexer(require, exports, module) {
"use strict";

var Lexer = require("thirdparty/lexer");
var phpParser = require("thirdparty/php-parser");

/** @const {string} Placeholder for unnamed functions. */
var UNNAMED_PLACEHOLDER = "function";

/**
* Parse a Parameter node.
* @private
* @param {object} argument Parameter node
* @returns {string} String representation of the Parameter.
*/
function _parseArg(argument) {
return "$" + argument.name;
}

/**
* Traverse a subtree recursivly.
* @private
* @param {object} node AST node
* @param {object[]} list List of objects for the parsed nodes
* @param {number} level Indentation level of the function
* @returns {object[]} List of objects for the parsed nodes
*/
function _traverse(node, list, level) {
if (node.kind === "class") {
list.push({
type: "class",
name: node.name,
args: [],
modifier: "public",
level: level,
isStatic: false,
line: node.loc.start.line - 1
});
level++;
} else if (node.kind === "function") {
list.push({
type: "function",
name: node.name,
args: node.arguments.map(_parseArg),
modifier: "public",
level: level,
isStatic: false,
line: node.loc.start.line - 1
});
level++;
} else if (node.kind === "method") {
list.push({
type: "function",
name: node.name,
args: node.arguments.map(_parseArg),
modifier: node.visibility,
level: level,
isStatic: node.isStatic,
line: node.loc.start.line - 1
});
level++;
} else if (node.kind === "closure") {
list.push({
type: "function",
name: UNNAMED_PLACEHOLDER,
args: node.arguments.map(_parseArg),
modifier: "unnamed",
level: level,
isStatic: false,
line: node.loc.start.line - 1
});
level++;
}
Object.keys(node).forEach(function (prop) {
var children = node[prop];
if (Array.isArray(children)) {
children.forEach(function (child) {
list = _traverse(child, list, level);
});
} else if (children instanceof Object) {
list = _traverse(children, list, level);
}
});
return list;
}

/**
* Parse the source and extract the code structure.
* @param {string} source the source code.
* @returns {object[]} the code structure.
*/
function parse(source) {
var line = 0; // line number.
var ns = []; // the namespace array.
var literal = true; // check if it's in literal area.
var comment = false; // the comment flag.
var state = []; // the state array.
var modifier = null; // the modifier.
var isStatic = false; // static flag.
var isAbstract = false; // abstract flag.
// saves the results object of the last class that
// extends another or implements an interface.
var lastChildClass = null;
// helper function to peek an item from an array.
var peek = function (array) {
if (array.length > 0) {
return array[array.length - 1];
}
return null;
};
var results = [];
var ignored = function () { /* noop */ };
var lexer = new Lexer();
lexer
// when it encounters `<?php` structure, turn off literal mode.
.addRule(/<\?(php)?/, function () {
literal = false;
})
// when it encounters `?>` structure, turn on literal mode.
.addRule(/\?>/, function () {
literal = true;
})
// toggle comment if necessary.
.addRule(/\/\*/, function () {
comment = true;
})
.addRule(/\*\//, function () {
comment = false;
})
// ignore the comments.
.addRule(/\/\/[^\n]*/, ignored)
// ignore strings (double quotes).
.addRule(/"((?:\\.|[^"\\])*)"/, ignored)
// ignore strings (single quotes).
.addRule(/'((?:\\.|[^'\\])*)'/, ignored)
// ignore execution operator (backticks).
.addRule(/`((?:\\.|[^`\\])*)`/, ignored)
// ignore 'class' word in static::class late static binding.
.addRule(/static::class/, ignored)
// detect abstract modifier, but treat it apart from the visibility modifiers
.addRule(/public|protected|private|abstract/, function (w) {
if (w === "abstract") {
isAbstract = true;
} else {
modifier = w;
}
})
.addRule(/static/, function () {
isStatic = true;
})
// when it encounters `function` and literal mode is off,
// 1. push 'function' into state array;
// 2. push a function structure in result.
.addRule(/function/, function () {
if (!literal && !comment) {
state.push("function");
results.push({
type: "function",
name: "",
args: [],
modifier: "unnamed",
level: 0,
isStatic: isStatic,
line: line
});
var ast;
try {
ast = phpParser.parseCode(source, {
parser: {
locations: true,
suppressErrors: true
},
ast: {
withPositions: true
}
})
// when it encounters `class` and literal mode is off.
// 1. push "class" into state array.
// 2. create a class structure into results array.
.addRule(/class/, function () {
if (!literal && !comment) {
state.push("class");
results.push({
type: "class",
name: "",
args: [],
modifier: "public",
level: 0,
isStatic: isStatic,
line: line
});
}
})
// support for extended classes and interface implementations
.addRule(/extends|implements/, function () {
if (!literal && !comment) {
if (peek(state) === "class") {
lastChildClass = results.pop();
state.push("inheriting");
}
}
})
// if it's a variable and it's in function args semantics, push it into args array.
.addRule(/\$[0-9a-zA-Z_]+/, function (w) {
if (!literal && !comment) {
if (peek(state) === "args") {
peek(results).args.push(w);
}
// reset modifiers when variable is parsed.
modifier = null;
isStatic = false;
}
})
// check if it's an identity term.
.addRule(/[0-9a-zA-Z_]+/, function (w) {
var ref;
if (!literal && !comment) {
switch (peek(state)) {
case "function":
ns.push(w);
ref = peek(results);
ref.name = w;
ref.level = ns.length - 1;
ref.modifier = modifier || "public";
break;
case "class":
ns.push(w);
ref = peek(results);
ref.name = w;
break;
case "inheriting":
state.pop();
results.push(lastChildClass);
break;
default:
break;
}
// reset modifier when identity term is parsed.
modifier = null;
isStatic = false;
}
})
// check if it's in function definition, turn on args mode.
.addRule(/\(/, function () {
if (!literal && !comment) {
if (peek(state) === "function") {
var ref = peek(results);
if (ref.modifier === "unnamed") {
ns.push(UNNAMED_PLACEHOLDER);
ref.name = UNNAMED_PLACEHOLDER;
ref.level = ns.length - 1;
}
if (!ref || ref.type !== "function") {
ns.push(UNNAMED_PLACEHOLDER);
results.push({
type: "function",
name: "",
args: [],
modifier: "unnamed",
level: 0,
isStatic: false,
line: line
});
}
state.push("args");
}
}
})
// turn off args mode.
.addRule(/\)/, function () {
if (!literal && !comment) {
if (peek(state) === "args") {
state.pop();
}
}
})
// ignore return types.
.addRule(/:\s*[0-9a-zA-Z_]+/, ignored)
// start function/class body definition or scoped code structure.
.addRule(/{/, function () {
if (!literal && !comment) {
var ref;
if ((ref = peek(state)) === "function" || ref === "class") {
var prefix = state.pop();
state.push(prefix + ":start");
} else {
state.push("start");
}
}
})
// pop name from namespace array if it's in a namespace.
.addRule(/}/, function () {
if (!literal && !comment && state.length > 0) {
var s = state.pop().split(":")[0];
if (s === "class" || s === "function") {
ns.pop();
}
}
})
// support for abstract methods
.addRule(/;/, function () {
if (!literal && !comment) {
if (peek(state) === "function" && isAbstract) {
state.pop();
ns.pop();
isAbstract = false; // reset abstract flag
} else if (peek(state) === "class") {
ns.pop();
}
}
})
// support for classes implementing multiple interfaces
.addRule(/,/, function () {
if (!literal && !comment) {
if (peek(state) === "class") {
state.push("inheriting");
results.pop();
}
}
})
// other terms are ignored.
.addRule(/./, ignored);
});
} catch (error) {
throw new Error("SyntaxError");
}

// parse the code to the end of the source.
source.split(/\r?\n/).forEach(function (sourceLine) {
lexer.setInput(sourceLine);
lexer.lex();
// line number increases.
line += 1;
});
return results;
var result = _traverse(ast, [], 0);
return result;
}

module.exports = {
Expand Down

0 comments on commit 8f2948e

Please sign in to comment.