diff --git a/CHANGELOG.md b/CHANGELOG.md
index 71f7e8f..d999003 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,26 @@
All notable changes to the "php-docblocker" extension will be documented in this file.
## [Unreleased]
+- Supported variable docblock
+```php
+# Example
+
+/** @var int $var */
+$var = 1;
+
+/** @var stdClass $object */
+$object = new stdClass;
+
+/** @var \Closure $callback */
+$callback = function () {};
+$callback = fn () => 1;
+
+/** @var [type] $item */
+foreach ($data as $item) {}
+
+/** @var [type] $item */
+while ($item = array_pop($data)) {}
+```
## [2.7.0] - 2022-02-11
- Allow configuration of default type
diff --git a/src/block/ForeachBlock.ts b/src/block/ForeachBlock.ts
new file mode 100644
index 0000000..12cb2d3
--- /dev/null
+++ b/src/block/ForeachBlock.ts
@@ -0,0 +1,24 @@
+import { Doc, Param } from "../doc";
+import VariableBlock from "./VariableBlock";
+
+/**
+ * Represents an var block for `foreach`
+ */
+export default class ForeachBlock extends VariableBlock
+{
+
+ /**
+ * @inheritdoc
+ */
+ protected pattern:RegExp = /^\s*foreach\s*\(.*?as\s+(\$[a-z_][a-z0-9_]*\s*=>\s*)?(\$[a-z_][a-z0-9_]*)\s*\)/im;
+
+ /**
+ * @inheritdoc
+ */
+ public parse():Doc
+ {
+ let params = this.match();
+ return this.parseVar(params[2]);
+ }
+}
+
diff --git a/src/block/VariableBlock.ts b/src/block/VariableBlock.ts
new file mode 100644
index 0000000..fd9248a
--- /dev/null
+++ b/src/block/VariableBlock.ts
@@ -0,0 +1,46 @@
+import { Block } from "../block";
+import { Doc, Param } from "../doc";
+import Config from "../util/config";
+import TypeUtil from "../util/TypeUtil";
+
+/**
+ * Represents an var block
+ */
+export default class VariableBlock extends Block
+{
+
+ /**
+ * @inheritdoc
+ */
+ protected pattern:RegExp = /^\s*(\$[a-z0-9_]+)\s*\=?\s*([^;]*)/im;
+
+ /**
+ * @inheritdoc
+ */
+ public parse():Doc
+ {
+ let params = this.match();
+ return this.parseVar(params[1], params[2]);
+ }
+
+ /**
+ * parse
+ *
+ * @param key e.g.`$key`
+ * @param value
+ * @returns
+ */
+ protected parseVar(key: string, value: any=undefined): Doc
+ {
+ let doc = new Doc(key.substring(1));
+ if (value) {
+ doc.var = TypeUtil.instance.getTypeFromValue(value);
+ } else {
+ doc.var = TypeUtil.instance.getDefaultType();
+ }
+ doc.inline = true;
+
+ return doc;
+ }
+}
+
diff --git a/src/block/WhileBlock.ts b/src/block/WhileBlock.ts
new file mode 100644
index 0000000..7cc6430
--- /dev/null
+++ b/src/block/WhileBlock.ts
@@ -0,0 +1,24 @@
+import { Doc, Param } from "../doc";
+import VariableBlock from "./VariableBlock";
+
+/**
+ * Represents an var block for `while`
+ */
+export default class WhileBlock extends VariableBlock
+{
+
+ /**
+ * @inheritdoc
+ */
+ protected pattern:RegExp = /^\s*while\s*\((\$[a-z0-9_]+)\s*=(?!=)/im;
+
+ /**
+ * @inheritdoc
+ */
+ public parse():Doc
+ {
+ let params = this.match();
+ return this.parseVar(params[1]);
+ }
+}
+
diff --git a/src/doc.ts b/src/doc.ts
index 3338c5a..7473188 100644
--- a/src/doc.ts
+++ b/src/doc.ts
@@ -26,6 +26,13 @@ export class Doc
*/
public params:Array = [];
+ /**
+ * Doc inline
+ *
+ * Currently only used for variable block.
+ */
+ public inline:boolean = false;
+
/**
* Return tag
*
@@ -87,6 +94,9 @@ export class Doc
if (input.message !== undefined) {
this.message = input.message;
}
+ if (input.inline !== undefined) {
+ this.inline = input.inline;
+ }
if (input.params !== undefined && Array.isArray(input.params)) {
input.params.forEach(param => {
this.params.push(new Param(param.type, param.name));
@@ -222,22 +232,26 @@ export class Doc
templateArray.pop();
}
- let templateString:string = templateArray.join("\n");
+ let templateString: string;
+ if (this.inline && templateArray.length === 3) {
+ templateString = "/** " + templateArray[2] + " \$" + templateArray[0] + " \${###} */";
+ } else {
+ templateString = templateArray.join("\n");
+ templateString = templateString.replace(/^$/gm, " *");
+ templateString = templateString.replace(/^(?!(\s\*|\/\*))/gm, " * $1");
+ if (Config.instance.get('autoClosingBrackets') == "never") {
+ templateString = "\n" + templateString + "\n */";
+ } else {
+ templateString = "/**\n" + templateString + "\n */";
+ }
+ }
+
let stop = 0;
templateString = templateString.replace(/###/gm, function():string {
stop++;
return stop + "";
});
- templateString = templateString.replace(/^$/gm, " *");
- templateString = templateString.replace(/^(?!(\s\*|\/\*))/gm, " * $1");
-
- if (Config.instance.get('autoClosingBrackets') == "never") {
- templateString = "\n" + templateString + "\n */";
- } else {
- templateString = "/**\n" + templateString + "\n */";
- }
-
let snippet = new SnippetString(templateString);
return snippet;
diff --git a/src/documenter.ts b/src/documenter.ts
index 492cf3a..b570657 100644
--- a/src/documenter.ts
+++ b/src/documenter.ts
@@ -3,6 +3,9 @@ import FunctionBlock from "./block/function";
import Property from "./block/property";
import Class from "./block/class";
import {Doc, Param} from "./doc";
+import VariableBlock from "./block/VariableBlock";
+import ForeachBlock from "./block/ForeachBlock";
+import WhileBlock from "./block/WhileBlock";
/**
* Check which type of docblock we need and instruct the components to build the
@@ -59,6 +62,21 @@ export default class Documenter
return cla.parse().build();
}
+ let variable = new VariableBlock(this.targetPosition, this.editor);
+ if (variable.test()) {
+ return variable.parse().build();
+ }
+
+ let foreach = new ForeachBlock(this.targetPosition, this.editor);
+ if (foreach.test()) {
+ return foreach.parse().build();
+ }
+
+ let while_ = new WhileBlock(this.targetPosition, this.editor);
+ if (while_.test()) {
+ return while_.parse().build();
+ }
+
return new Doc().build(true);
}
}
diff --git a/src/util/TypeUtil.ts b/src/util/TypeUtil.ts
index dc0f68a..c3ca708 100644
--- a/src/util/TypeUtil.ts
+++ b/src/util/TypeUtil.ts
@@ -145,6 +145,11 @@ export default class TypeUtil {
return 'integer';
}
return 'int';
+ case 'real':
+ case 'double':
+ return 'float';
+ case 'unset':
+ return 'null';
default:
return name;
}
@@ -159,15 +164,13 @@ export default class TypeUtil {
*/
public getTypeFromValue(value:string):string
{
- let result:Array;
-
- // Check for bool
+ // Check for bool `false` `true` `!exp`
if (value.match(/^\s*(false|true)\s*$/i) !== null || value.match(/^\s*\!/i) !== null) {
return this.getFormattedTypeByName('bool');
}
- // Check for int
- if (value.match(/^\s*([\d-]+)\s*$/) !== null) {
+ // Check for int `-1` `1` `1_000_000`
+ if (value.match(/^\s*(\-?\d[\d_]*)\s*$/) !== null) {
return this.getFormattedTypeByName('int');
}
@@ -176,6 +179,11 @@ export default class TypeUtil {
return 'float';
}
+ // Check for float `.1` `1.1` `-1.1` `0.1_000_1`
+ if (value.match(/^\s*(\-?[\d_\.]*)\s*$/) !== null) {
+ return 'float';
+ }
+
// Check for string
if (value.match(/^\s*(["'])/) !== null || value.match(/^\s*<<) !== null) {
return 'string';
@@ -185,6 +193,27 @@ export default class TypeUtil {
if (value.match(/^\s*(array\(|\[)/) !== null) {
return 'array';
}
+
+ // Check for class
+ var match = value.match(/^\s*new\s+([a-z0-9_\\\|]+)/i);
+ if (match) {
+ if (match[1] === 'class') {
+ return 'object';
+ }
+ return match[1];
+ }
+
+ // Check for closure
+ var match = value.match(/^\s*function\s*\(/i) || value.match(/^\s*fn\s*\(/i);
+ if (match) {
+ return '\\Closure';
+ }
+
+ // Check for type casting
+ var match = value.match(/^\s*\(\s*(int|integer|bool|boolean|float|double|real|string|array|object|unset)\s*\)/i);
+ if (match) {
+ return this.getFormattedTypeByName(match[1]);
+ }
return this.getDefaultType();
}
diff --git a/test/completions.test.ts b/test/completions.test.ts
index 88dde74..4e99163 100644
--- a/test/completions.test.ts
+++ b/test/completions.test.ts
@@ -21,6 +21,9 @@ suite("Completion tests", () => {
});
map.forEach(testData => {
+ if (testData.name === undefined) {
+ testData.name = testData.key;
+ }
test("Completion: " + testData.name, () => {
let pos:Position = testPositions[testData.key];
let result:any = completions.provideCompletionItems(
@@ -31,10 +34,12 @@ suite("Completion tests", () => {
let matched:Array = [];
result.forEach(data => {
- matched.push(data.label);
+ matched.push(data.insertText.value);
});
+ let actual = matched.join("\n");
+ let expected = testData.result.join("\n");
- assert.deepEqual(testData.result, matched);
+ assert.deepEqual(actual, expected);
});
});
});
diff --git a/test/fixtures/completions.php b/test/fixtures/completions.php
index 5730f51..38f1a81 100644
--- a/test/fixtures/completions.php
+++ b/test/fixtures/completions.php
@@ -31,5 +31,25 @@ class Blah
{
}
+////=> variable
+ /**
+ $var = null;
+
+////=> foreach
+ /**
+ foreach ([] as $key => $value) {
+
+////=> foreach-with-key
+ /**
+ foreach ([] as $value) {
+
+////=> while
+ /**
+ while ($value = array_shift($arrs)) {
+
+////=> while-no-var
+ /**
+ while ($value == true) {
+
////=> empty
/**
diff --git a/test/fixtures/completions.php.json b/test/fixtures/completions.php.json
index e60dcba..79f1f13 100644
--- a/test/fixtures/completions.php.json
+++ b/test/fixtures/completions.php.json
@@ -3,28 +3,28 @@
"key": "param",
"name": "Param tag",
"result": [
- "@param"
+ "@param ${1:mixed} $${2:name}"
]
},
{
"key": "return",
"name": "Return tag",
"result": [
- "@return"
+ "@return ${1:mixed}"
]
},
{
"key": "package",
"name": "Package tag",
"result": [
- "@package"
+ "@package ${1:category}"
]
},
{
"key": "var",
"name": "Var tag",
"result": [
- "@var"
+ "@var ${1:mixed}"
]
},
{
@@ -37,28 +37,72 @@
"key": "property",
"name": "Property trigger",
"result": [
- "/**"
+ "/**",
+ " * ${1:Undocumented variable}",
+ " *",
+ " * @var ${2:[type]}",
+ " */"
]
},
{
"key": "function",
"name": "Function trigger",
"result": [
- "/**"
+ "/**",
+ " * ${1:Undocumented function}",
+ " *",
+ " * @return ${2:void}",
+ " */"
]
},
{
"key": "class",
"name": "Class trigger",
"result": [
- "/**"
+ "/**",
+ " * ${1:Undocumented class}",
+ " */"
+ ]
+ },
+ {
+ "key": "variable",
+ "result": [
+ "/** @var ${1:[type]} $${2:var} ${3} */"
+ ]
+ },
+ {
+ "key": "foreach",
+ "result": [
+ "/** @var ${1:[type]} $${2:value} ${3} */"
+ ]
+ },
+ {
+ "key": "foreach-with-key",
+ "result": [
+ "/** @var ${1:[type]} $${2:value} ${3} */"
+ ]
+ },
+ {
+ "key": "while",
+ "result": [
+ "/** @var ${1:[type]} $${2:value} ${3} */"
+ ]
+ },
+ {
+ "key": "while-no-var",
+ "result": [
+ "/**",
+ " * ${1}",
+ " */"
]
},
{
"key": "empty",
"name": "Empty trigger",
"result": [
- "/**"
+ "/**",
+ " * ${1}",
+ " */"
]
}
]
diff --git a/test/fixtures/variables.php b/test/fixtures/variables.php
new file mode 100644
index 0000000..3b9b117
--- /dev/null
+++ b/test/fixtures/variables.php
@@ -0,0 +1,91 @@
+ doc
+$var = 1;
+
+////=> float1
+$var = .3;
+
+////=> float2
+$var = 0.3;
+
+////=> float3
+$var = .1_000_1;
+
+////=> int1
+$var = 3;
+
+////=> int2
+$var = 03;
+
+////=> int3
+$var = 1_000_000;
+
+////=> bool1
+$var = true;
+
+////=> bool2
+$var = !0;
+
+////=> bool3
+$var = !'test';
+
+////=> stdclass
+$var = new stdClass;
+
+////=> object
+$var = new class {};
+
+////=> string1
+$var = 'string';
+
+////=> string2
+$var = "string";
+
+////=> string3
+$var = << closure
+$var = function () {};
+
+////=> closure-fn
+$var = fn () => 123;
+
+////=> array1
+$var = [1,2,3];
+
+////=> array2
+$var = [];
+
+////=> array3
+$var = array(
+ 1
+);
+
+////=> array4
+$var = [
+ 1
+];
+
+////=> type-casting-string
+$var = (string)1;
+
+////=> type-casting-float1
+$var = (float)1;
+
+////=> type-casting-float2
+$var = (real)1;
+
+////=> type-casting-float3
+$var = (double)1;
+
+////=> type-casting-null
+$var = (unset)1;
+
+////=> mixed1
+$var = test();
+
+////=> mixed2
+$var = null;
\ No newline at end of file
diff --git a/test/fixtures/variables.php.json b/test/fixtures/variables.php.json
new file mode 100644
index 0000000..2211d0e
--- /dev/null
+++ b/test/fixtures/variables.php.json
@@ -0,0 +1,176 @@
+[
+ {
+ "key": "doc",
+ "config": {
+ "inline": true
+ },
+ "result": {
+ "inline": true,
+ "message": "var",
+ "var": "integer"
+ },
+ "doc": "/** @var ${1:integer} $${2:var} ${3} */"
+ },
+ {
+ "key": "float1",
+ "result": {
+ "var": "float"
+ }
+ },
+ {
+ "key": "float2",
+ "result": {
+ "var": "float"
+ }
+ },
+ {
+ "key": "float3",
+ "result": {
+ "var": "float"
+ }
+ },
+ {
+ "key": "int1",
+ "result": {
+ "var": "integer"
+ }
+ },
+ {
+ "key": "int2",
+ "result": {
+ "var": "integer"
+ }
+ },
+ {
+ "key": "int3",
+ "result": {
+ "var": "integer"
+ }
+ },
+ {
+ "key": "bool1",
+ "result": {
+ "var": "boolean"
+ }
+ },
+ {
+ "key": "bool2",
+ "result": {
+ "var": "boolean"
+ }
+ },
+ {
+ "key": "bool3",
+ "result": {
+ "var": "boolean"
+ }
+ },
+ {
+ "key": "stdclass",
+ "result": {
+ "var": "stdClass"
+ }
+ },
+ {
+ "key": "object",
+ "result": {
+ "var": "object"
+ }
+ },
+ {
+ "key": "string1",
+ "result": {
+ "var": "string"
+ }
+ },
+ {
+ "key": "string2",
+ "result": {
+ "var": "string"
+ }
+ },
+ {
+ "key": "string3",
+ "result": {
+ "var": "string"
+ }
+ },
+ {
+ "key": "closure",
+ "result": {
+ "var": "\\Closure"
+ }
+ },
+ {
+ "key": "closure-fn",
+ "result": {
+ "var": "\\Closure"
+ }
+ },
+ {
+ "key": "array2",
+ "result": {
+ "var": "array"
+ }
+ },
+ {
+ "key": "array2",
+ "result": {
+ "var": "array"
+ }
+ },
+ {
+ "key": "array3",
+ "result": {
+ "var": "array"
+ }
+ },
+ {
+ "key": "array4",
+ "result": {
+ "var": "array"
+ }
+ },
+ {
+ "key": "type-casting-string",
+ "result": {
+ "var": "string"
+ }
+ },
+ {
+ "key": "type-casting-float1",
+ "result": {
+ "var": "float"
+ }
+ },
+ {
+ "key": "type-casting-float2",
+ "result": {
+ "var": "float"
+ }
+ },
+ {
+ "key": "type-casting-float3",
+ "result": {
+ "var": "float"
+ }
+ },
+ {
+ "key": "type-casting-null",
+ "result": {
+ "var": "null"
+ }
+ },
+ {
+ "key": "mixed1",
+ "result": {
+ "var": "[type]"
+ }
+ },
+ {
+ "key": "mixed2",
+ "result": {
+ "var": "[type]"
+ }
+ }
+]
\ No newline at end of file
diff --git a/test/variables.test.ts b/test/variables.test.ts
new file mode 100644
index 0000000..fb29bee
--- /dev/null
+++ b/test/variables.test.ts
@@ -0,0 +1,61 @@
+import * as assert from 'assert';
+import {TextEditor, TextDocument, WorkspaceConfiguration} from 'vscode';
+import Helper from './helpers';
+import Function from '../src/block/function';
+import {Doc, Param} from '../src/doc';
+import Config from '../src/util/config';
+import VariableBlock from '../src/block/VariableBlock';
+
+suite("Variable tests", () => {
+ let editor:TextEditor;
+ let document:TextDocument;
+ let testPositions:any = {};
+
+ let defaults:Config = Helper.getConfig();
+ let map = Helper.getFixtureMap('variables.php.json');
+
+ suiteSetup(function(done) {
+ Helper.loadFixture('variables.php', (edit:TextEditor, doc:TextDocument) => {
+ editor = edit;
+ document = doc;
+ testPositions = Helper.getFixturePositions(document);
+ done();
+ });
+ });
+
+ map.forEach(testData => {
+ if (testData.name === undefined) {
+ testData.name = testData.key;
+ }
+
+ test("Match Test: "+ testData.name, () => {
+ let variable = new VariableBlock(testPositions[testData.key], editor);
+ assert.equal(variable.test(), true, test.name);
+ });
+
+ test("Parse Test: "+ testData.name, () => {
+ let variable = new VariableBlock(testPositions[testData.key], editor);
+ assert.ok(variable.parse(), test.name);
+ });
+
+ test("Type Test: "+ testData.name, () => {
+ Helper.setConfig(testData.config);
+ let variable = new VariableBlock(testPositions[testData.key], editor);
+ let actual:Doc = variable.parse();
+ let expected:Doc = new Doc(testData.key.substring(1));
+ if (testData.result.var === undefined) {
+ expected.var = undefined;
+ }
+ expected.fromObject(actual);
+ expected.fromObject(testData.result);
+ assert.deepEqual(actual, expected);
+
+ if (testData.doc !== undefined) {
+ // if (isArray(testData.doc)) {
+ // testData.doc = testData.doc.join("\n");
+ // }
+ assert.equal(actual.build().value, testData.doc, test.name);
+ }
+ });
+ });
+});