├── .npmrc ├── coverage └── .gitignore ├── .vscode ├── settings.json ├── extensions.json ├── tasks.json └── launch.json ├── test ├── fixtures │ ├── commands.php │ ├── namespace-issue.php │ ├── completions.php │ ├── namespace.php │ ├── classes.php.json │ ├── completions.php.json │ ├── classes.php │ ├── properties.php │ ├── properties.php.json │ ├── functions.php │ ├── doc.json │ └── functions.php.json ├── commands.test.ts ├── runTest.ts ├── doc.test.ts ├── class.test.ts ├── completions.test.ts ├── properties.test.ts ├── index.ts ├── functions.test.ts ├── helpers.ts └── TypeUtil.test.ts ├── images ├── logo.png └── logo.svg ├── .gitignore ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── test.yml │ ├── release.yml │ └── prepare-release.yml └── actions │ └── setup │ └── action.yml ├── .vscodeignore ├── tsconfig.json ├── .travis.yml ├── scripts ├── generateTagTable.ts └── updateChangelog.ts ├── src ├── block │ ├── class.ts │ ├── property.ts │ └── function.ts ├── documenter.ts ├── util │ ├── config.ts │ └── TypeUtil.ts ├── extension.ts ├── completions.ts ├── block.ts ├── tags.ts └── doc.ts ├── LICENSE.md ├── package.json ├── CHANGELOG.md └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | git-tag-version = false 2 | -------------------------------------------------------------------------------- /coverage/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug.node.autoAttach": "on" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/commands.php: -------------------------------------------------------------------------------- 1 | param 4 | * @para 5 | 6 | ////=> return 7 | * @ret 8 | 9 | ////=> package 10 | * @pack 11 | 12 | ////=> var 13 | * @va 14 | 15 | ////=> nothing 16 | * & 17 | 18 | ////=> property 19 | /** 20 | protected $name; 21 | 22 | ////=> function 23 | /** 24 | protected function name() 25 | { 26 | } 27 | 28 | ////=> class 29 | /** 30 | class Blah 31 | { 32 | } 33 | 34 | ////=> empty 35 | /** 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Suite 2 | on: 3 | push: 4 | pull_request: 5 | types: [opened, synchronize] 6 | 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup 17 | uses: ./.github/actions/setup 18 | 19 | - name: Run tests 20 | run: xvfb-run -a npm test 21 | 22 | - name: Coveralls 23 | uses: coverallsapp/github-action@master 24 | with: 25 | github-token: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | 5 | addons: 6 | apt: 7 | packages: 8 | - libsecret-1-dev 9 | 10 | before_install: 11 | - | 12 | if [ $TRAVIS_OS_NAME == "linux" ]; then 13 | export DISPLAY=':99.0' 14 | /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 15 | fi 16 | 17 | install: 18 | - npm install 19 | 20 | after_success: 21 | - cat coverage/lcov.info | coveralls 22 | 23 | cache: 24 | directories: 25 | - .vscode-test 26 | 27 | notifications: 28 | email: 29 | on_success: never 30 | on_failure: always 31 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "watch", 8 | "group": "build", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "label": "npm: watch", 15 | }, 16 | { 17 | "type": "npm", 18 | "script": "build", 19 | "group": { 20 | "kind": "build", 21 | "isDefault": true 22 | }, 23 | "problemMatcher": "$esbuild", 24 | "label": "npm: build", 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /scripts/generateTagTable.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import Tags from "../src/tags"; 3 | import { getMarkdownTable } from 'markdown-table-ts'; 4 | 5 | let tags = new Tags(); 6 | 7 | let formatted: string[][] = []; 8 | tags.list.forEach(tag => { 9 | formatted.push([tag.tag, tag.snippet]); 10 | }); 11 | 12 | let table = getMarkdownTable({ 13 | table: { 14 | head: ['Tag', 'Snippet'], 15 | body: formatted 16 | }, 17 | }); 18 | 19 | console.log(''); 20 | console.log(table); 21 | console.log(''); 22 | 23 | fs.writeFile('./out/TAGS.md', table, 'utf8', function (err) { 24 | if (err) { 25 | throw err; 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /src/block/class.ts: -------------------------------------------------------------------------------- 1 | import { Block } from "../block"; 2 | import { Doc, Param } from "../doc"; 3 | import Config from "../util/config"; 4 | 5 | /** 6 | * Represents a class block 7 | */ 8 | export default class Class extends Block 9 | { 10 | /** 11 | * @inheritdoc 12 | */ 13 | protected pattern:RegExp = /^\s*(abstract|final)?\s*(class|trait|interface)\s+([a-z0-9_]+)\s*/i; 14 | 15 | /** 16 | * @inheritdoc 17 | */ 18 | public parse():Doc 19 | { 20 | let params = this.match(); 21 | let doc = new Doc('Undocumented '+ params[2]); 22 | doc.template = Config.instance.get('classTemplate'); 23 | 24 | return doc; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/commands.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {TextEditor, TextDocument, commands, Selection} from 'vscode'; 3 | import Helper from './helpers'; 4 | 5 | suite("Command tests", () => { 6 | let editor:TextEditor; 7 | let document:TextDocument; 8 | 9 | suiteSetup(function(done) { 10 | Helper.loadFixture('commands.php', (edit:TextEditor, doc:TextDocument) => { 11 | editor = edit; 12 | document = doc; 13 | done(); 14 | }); 15 | }); 16 | 17 | test("Command: trigger", () => { 18 | editor.selection = new Selection(3, 0, 3, 0); 19 | assert.doesNotThrow(async () => { 20 | await commands.executeCommand('php-docblocker.trigger', editor) 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/fixtures/namespace.php: -------------------------------------------------------------------------------- 1 | { 8 | let map = Helper.getFixtureMap('doc.json'); 9 | 10 | map.forEach(testData => { 11 | test("Snippet test: "+ testData.name, () => { 12 | let doc = new Doc(); 13 | let empty = false; 14 | if (testData.config != undefined) { 15 | Helper.setConfig(testData.config); 16 | } else { 17 | Config.instance.setFallback(Helper.getDefaultConfig()); 18 | } 19 | if (testData.input != undefined) { 20 | doc.fromObject(testData.input); 21 | } else { 22 | empty = true; 23 | } 24 | if (Config.instance.get('template')) { 25 | doc.template = Config.instance.get('template'); 26 | } 27 | assert.equal(doc.build(empty).value, testData.expected.join("\n")); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/class.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {TextEditor, TextDocument} from 'vscode'; 3 | import Helper from './helpers'; 4 | import Class from '../src/block/class'; 5 | import {Doc, Param} from '../src/doc'; 6 | 7 | suite("Class tests", () => { 8 | let editor:TextEditor; 9 | let document:TextDocument; 10 | let testPositions:any = {}; 11 | 12 | let map = Helper.getFixtureMap('classes.php.json'); 13 | 14 | suiteSetup(function(done) { 15 | Helper.loadFixture('classes.php', (edit:TextEditor, doc:TextDocument) => { 16 | editor = edit; 17 | document = doc; 18 | testPositions = Helper.getFixturePositions(document); 19 | done(); 20 | }); 21 | }); 22 | 23 | map.forEach(testData => { 24 | test("Match Test: "+ testData.name, () => { 25 | let func = new Class(testPositions[testData.key], editor); 26 | assert.equal(func.test(), true, test.name); 27 | }); 28 | 29 | test("Parse Test: "+ testData.name, () => { 30 | let func = new Class(testPositions[testData.key], editor); 31 | assert.ok(func.parse(), test.name); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | pull_request: 5 | types: [ closed ] 6 | 7 | jobs: 8 | merged: 9 | if: | 10 | github.event.pull_request.merged == true 11 | && github.event.pull_request.user.login == 'php-docblocker' 12 | && contains(github.event.pull_request.labels.*.name, 'release candidate') 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | 19 | - name: Setup 20 | uses: ./.github/actions/setup 21 | 22 | - name: Verify PAT 23 | run: npm run verify-pat -- --pat ${{ secrets.VSCE_PAT }} 24 | 25 | - name: Package 26 | run: npm run package 27 | 28 | - name: Extract version 29 | id: extract_version 30 | uses: Saionaro/extract-package-version@v1.0.6 31 | 32 | - name: Create release 33 | uses: ncipollo/release-action@v1 34 | with: 35 | name: ${{ github.event.pull_request.title }} 36 | artifacts: ./out/package.vsix 37 | body: ${{ github.event.pull_request.body }} 38 | tag: v${{ steps.extract_version.outputs.version }} 39 | token: ${{ secrets.BOT_GITHUB_PAT }} 40 | 41 | - name: Publish 42 | run: npm run publish 43 | env: 44 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 45 | -------------------------------------------------------------------------------- /test/fixtures/classes.php.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": "simple", 4 | "name": "Simple" 5 | }, 6 | { 7 | "key": "interface", 8 | "name": "Interface" 9 | }, 10 | { 11 | "key": "trait", 12 | "name": "Trait" 13 | }, 14 | { 15 | "key": "final", 16 | "name": "Final" 17 | }, 18 | { 19 | "key": "abstract", 20 | "name": "Abstract" 21 | }, 22 | { 23 | "key": "extends", 24 | "name": "Extends" 25 | }, 26 | { 27 | "key": "implements", 28 | "name": "Implements" 29 | }, 30 | { 31 | "key": "extends-implements", 32 | "name": "Extends and Implements" 33 | }, 34 | { 35 | "key": "multiline", 36 | "name": "Multiline implementation" 37 | }, 38 | { 39 | "key": "complex", 40 | "name": "Abstract extension with multline implementation" 41 | }, 42 | { 43 | "key": "extends-namespace", 44 | "name": "Extend a class with a namespace" 45 | }, 46 | { 47 | "key": "implements-namespace", 48 | "name": "Implement a class with a namespace" 49 | }, 50 | { 51 | "key": "extends-implements-namespace", 52 | "name": "Implement and implmentes classes with a namespaces" 53 | } 54 | ] 55 | -------------------------------------------------------------------------------- /test/fixtures/completions.php.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": "param", 4 | "name": "Param tag", 5 | "result": [ 6 | "@param" 7 | ] 8 | }, 9 | { 10 | "key": "return", 11 | "name": "Return tag", 12 | "result": [ 13 | "@return" 14 | ] 15 | }, 16 | { 17 | "key": "package", 18 | "name": "Package tag", 19 | "result": [ 20 | "@package" 21 | ] 22 | }, 23 | { 24 | "key": "var", 25 | "name": "Var tag", 26 | "result": [ 27 | "@var" 28 | ] 29 | }, 30 | { 31 | "key": "nothing", 32 | "name": "Nothing", 33 | "result": [ 34 | ] 35 | }, 36 | { 37 | "key": "property", 38 | "name": "Property trigger", 39 | "result": [ 40 | "/**" 41 | ] 42 | }, 43 | { 44 | "key": "function", 45 | "name": "Function trigger", 46 | "result": [ 47 | "/**" 48 | ] 49 | }, 50 | { 51 | "key": "class", 52 | "name": "Class trigger", 53 | "result": [ 54 | "/**" 55 | ] 56 | }, 57 | { 58 | "key": "empty", 59 | "name": "Empty trigger", 60 | "result": [ 61 | "/**" 62 | ] 63 | } 64 | ] 65 | -------------------------------------------------------------------------------- /test/fixtures/classes.php: -------------------------------------------------------------------------------- 1 | simple 4 | class SimpleClass 5 | { 6 | 7 | } 8 | 9 | 10 | ////=> interface 11 | interface SimpleInterface 12 | { 13 | 14 | } 15 | 16 | ////=> trait 17 | trait SimpleTrait 18 | { 19 | 20 | } 21 | 22 | ////=> final 23 | final class FinalClass 24 | { 25 | 26 | } 27 | 28 | ////=> abstract 29 | abstract class AbstractClass 30 | { 31 | 32 | } 33 | 34 | ////=> extends 35 | class SubClass extends ParentClass 36 | { 37 | 38 | } 39 | 40 | ////=> implements 41 | class ImplementedClass implements ClassInterface 42 | { 43 | 44 | } 45 | 46 | ////=> extends-implements 47 | class ImplementedSubClass extends ImplementedParentClass implements ClassInterface 48 | { 49 | 50 | } 51 | 52 | ////=> multiline 53 | abstract class Multiline implements 54 | ImplementedClass1, 55 | App\Model\ImplementedClass2, 56 | \ImplementedClass3 57 | { 58 | 59 | } 60 | 61 | ////=> complex 62 | abstract class Complex extends MultilineParent implements 63 | ImplementedClass1, 64 | ImplementedClass2 65 | { 66 | 67 | } 68 | 69 | ////=> extends-namespace 70 | class SubClass extends \ParentClass 71 | { 72 | 73 | } 74 | 75 | ////=> implements-namespace 76 | class ImplementedClass implements \ClassInterface 77 | { 78 | 79 | } 80 | 81 | ////=> extends-implements-namespace 82 | class ImplementedSubClass extends App\Model\ImplementedParentClass implements App\Model\ClassInterface 83 | { 84 | 85 | } 86 | -------------------------------------------------------------------------------- /test/completions.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {TextEditor, TextDocument, CancellationTokenSource, CancellationToken, Position, ProviderResult, CompletionItem} from 'vscode'; 3 | import Helper from './helpers'; 4 | import Completions from '../src/completions'; 5 | 6 | suite("Completion tests", () => { 7 | let editor:TextEditor; 8 | let document:TextDocument; 9 | let testPositions:any = {}; 10 | let completions = new Completions(); 11 | 12 | let map = Helper.getFixtureMap('completions.php.json'); 13 | 14 | suiteSetup(function(done) { 15 | Helper.loadFixture('completions.php', (edit:TextEditor, doc:TextDocument) => { 16 | editor = edit; 17 | document = doc; 18 | testPositions = Helper.getFixturePositions(document); 19 | done(); 20 | }); 21 | }); 22 | 23 | map.forEach(testData => { 24 | test("Completion: " + testData.name, () => { 25 | let pos:Position = testPositions[testData.key]; 26 | let result:any = completions.provideCompletionItems( 27 | document, 28 | document.lineAt(pos.line+1).range.end, 29 | new CancellationTokenSource().token 30 | ); 31 | 32 | let matched:Array = []; 33 | result.forEach(data => { 34 | matched.push(data.label); 35 | }); 36 | 37 | assert.deepEqual(testData.result, matched); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /scripts/updateChangelog.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | const version = process.env.npm_package_version 4 | ? process.env.npm_package_version 5 | : process.argv[2]; 6 | 7 | if (!version) { 8 | throw "No version defined"; 9 | } 10 | 11 | let today = (new Date()).toISOString().split('T')[0]; 12 | 13 | fs.readFile('CHANGELOG.md', 'utf8', function (err, data) { 14 | if (err) { 15 | throw err; 16 | } 17 | 18 | if (!fs.existsSync('./out')) { 19 | fs.mkdirSync('./out'); 20 | } 21 | 22 | var body = data.match(/## \[Unreleased\]\s+(.*?)\s*## \[\d+/s); 23 | fs.writeFile('./out/RELEASE.md', body[1], 'utf8', function (err) { 24 | if (err) { 25 | throw err; 26 | } 27 | }); 28 | 29 | let textVersion = version.match(/\d+\.\d+\.\d+/) 30 | ? "v" + version 31 | : version 32 | 33 | fs.writeFile('./out/version.txt', textVersion, 'utf8', function (err) { 34 | if (err) { 35 | throw err; 36 | } 37 | }); 38 | 39 | var result = data.replace(/## \[Unreleased\]/, `## [Unreleased]\n\n## [${version}] - ${today}`); 40 | result = result.replace( 41 | /(\[Unreleased\]\: )(https\:\/\/github.com\/.*?\/compare\/)(v\d+\.\d+\.\d+)(\.{3})(HEAD)/, 42 | "$1$2v" + version + "$4$5\n[" + version + "]: $2$3$4v" + version 43 | ); 44 | 45 | fs.writeFile('CHANGELOG.md', result, 'utf8', function (err) { 46 | if (err) { 47 | throw err; 48 | } 49 | }); 50 | }); -------------------------------------------------------------------------------- /test/properties.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {TextEditor, TextDocument} from 'vscode'; 3 | import Helper from './helpers'; 4 | import Property from '../src/block/property'; 5 | import {Doc, Param} from '../src/doc'; 6 | 7 | suite("Property tests", () => { 8 | let editor:TextEditor; 9 | let document:TextDocument; 10 | let testPositions:any = {}; 11 | 12 | let map = Helper.getFixtureMap('properties.php.json'); 13 | 14 | suiteSetup(function(done) { 15 | Helper.loadFixture('properties.php', (edit:TextEditor, doc:TextDocument) => { 16 | editor = edit; 17 | document = doc; 18 | testPositions = Helper.getFixturePositions(document); 19 | done(); 20 | }); 21 | }); 22 | 23 | map.forEach(testData => { 24 | test("Match Test: "+ testData.name, () => { 25 | let func = new Property(testPositions[testData.key], editor); 26 | assert.equal(func.test(), true, test.name); 27 | }); 28 | 29 | test("Parse Test: "+ testData.name, () => { 30 | let func = new Property(testPositions[testData.key], editor); 31 | assert.ok(func.parse(), test.name); 32 | }); 33 | 34 | test("Type Test: "+ testData.name, () => { 35 | Helper.setConfig(testData.config); 36 | let func = new Property(testPositions[testData.key], editor); 37 | let doc:Doc = func.parse(); 38 | assert.equal(doc.var, testData.var, test.name); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/block/property.ts: -------------------------------------------------------------------------------- 1 | import { Block } from "../block"; 2 | import { Doc, Param } from "../doc"; 3 | import Config from "../util/config"; 4 | import TypeUtil from "../util/TypeUtil"; 5 | 6 | /** 7 | * Represents an property block 8 | */ 9 | export default class Property extends Block 10 | { 11 | 12 | /** 13 | * @inheritdoc 14 | */ 15 | protected pattern:RegExp = /^\s*(static)?\s*(protected|private|public)\s+(static\s*)?(?:readonly\s*)?(\??\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9|_\x7f-\xff\\]+)?\s*(\$[A-Za-z0-9_]+)\s*\=?\s*([^;]*)/m; 16 | 17 | /** 18 | * @inheritdoc 19 | */ 20 | public parse():Doc 21 | { 22 | let params = this.match(); 23 | 24 | let doc = new Doc('Undocumented variable'); 25 | doc.template = Config.instance.get('propertyTemplate'); 26 | 27 | if (params[4]) { 28 | let parts:Array = params[4].match(/(\?)?(.*)/m); 29 | let head:string; 30 | 31 | if (Config.instance.get('qualifyClassNames')) { 32 | head = this.getClassHead(); 33 | } 34 | 35 | let varType = TypeUtil.instance.getResolvedTypeHints(parts[2], head); 36 | varType = TypeUtil.instance.getFormattedTypeByName(varType); 37 | 38 | if (parts[1] === '?') { 39 | varType += '|null'; 40 | } 41 | 42 | doc.var = varType; 43 | } else if (params[6]) { 44 | doc.var = TypeUtil.instance.getTypeFromValue(params[6]); 45 | } else { 46 | doc.var = TypeUtil.instance.getDefaultType(); 47 | } 48 | 49 | return doc; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export async function run(): Promise { 6 | const NYC = require('nyc'); 7 | const nyc = new NYC({ 8 | cwd: path.join(__dirname, '..', '..'), 9 | exclude: ['**/test/**', '.vscode-test/**'], 10 | reporter: ['json', 'lcov'], 11 | extension: ['ts'], 12 | all: true, 13 | instrument: true, 14 | hookRequire: true, 15 | hookRunInContext: true, 16 | hookRunInThisContext: true, 17 | }); 18 | 19 | nyc.reset(); 20 | nyc.wrap(); 21 | 22 | 23 | // Create the mocha test 24 | const mocha = new Mocha({ 25 | ui: 'tdd', 26 | }); 27 | 28 | const testsRoot = path.resolve(__dirname, '..'); 29 | 30 | try { 31 | await new Promise((c, e) => { 32 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 33 | if (err) { 34 | return e(err); 35 | } 36 | 37 | // Add files to the test suite 38 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 39 | 40 | try { 41 | // Run the mocha test 42 | mocha.run(failures => { 43 | if (failures > 0) { 44 | e(new Error(`${failures} tests failed.`)); 45 | } else { 46 | c(failures); 47 | } 48 | }); 49 | } catch (err) { 50 | e(err); 51 | } 52 | }); 53 | }); 54 | } finally { 55 | nyc.writeCoverageFile(); 56 | await nyc.report(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/functions.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {TextEditor, TextDocument, WorkspaceConfiguration} from 'vscode'; 3 | import Helper from './helpers'; 4 | import Function from '../src/block/function'; 5 | import {Doc, Param} from '../src/doc'; 6 | import Config from '../src/util/config'; 7 | 8 | suite("Function tests", () => { 9 | let editor:TextEditor; 10 | let document:TextDocument; 11 | let testPositions:any = {}; 12 | 13 | let defaults:Config = Helper.getConfig(); 14 | let map = Helper.getFixtureMap('functions.php.json'); 15 | 16 | suiteSetup(function(done) { 17 | Helper.loadFixture('functions.php', (edit:TextEditor, doc:TextDocument) => { 18 | editor = edit; 19 | document = doc; 20 | testPositions = Helper.getFixturePositions(document); 21 | done(); 22 | }); 23 | }); 24 | 25 | map.forEach(testData => { 26 | test("Match Test: "+ testData.name, () => { 27 | let func = new Function(testPositions[testData.key], editor); 28 | assert.equal(func.test(), true, test.name); 29 | }); 30 | 31 | test("Parse Test: "+ testData.name, () => { 32 | let func = new Function(testPositions[testData.key], editor); 33 | assert.ok(func.parse(), test.name); 34 | }); 35 | 36 | test("Result Test: "+ testData.name, () => { 37 | Helper.setConfig(testData.config); 38 | let func = new Function(testPositions[testData.key], editor); 39 | let actual:Doc = func.parse(); 40 | let expected:Doc = new Doc('Undocumented function'); 41 | expected.fromObject(testData.result); 42 | expected.template = Helper.getConfig().get('functionTemplate'); 43 | assert.deepEqual(actual, expected); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/documenter.ts: -------------------------------------------------------------------------------- 1 | import {Range, Position, TextEditor, TextEditorEdit, workspace, SnippetString} from "vscode"; 2 | import FunctionBlock from "./block/function"; 3 | import Property from "./block/property"; 4 | import Class from "./block/class"; 5 | import {Doc, Param} from "./doc"; 6 | 7 | /** 8 | * Check which type of docblock we need and instruct the components to build the 9 | * snippet and pass it back 10 | */ 11 | export default class Documenter 12 | { 13 | /** 14 | * The target position of the comment block 15 | * 16 | * @type {Position} 17 | */ 18 | protected targetPosition:Position; 19 | 20 | /** 21 | * We'll need an editor to pass to each editor 22 | * 23 | * @type {TextEditor} 24 | */ 25 | protected editor:TextEditor; 26 | 27 | /** 28 | * Creates an instance of Documenter. 29 | * 30 | * @param {Range} range 31 | * @param {TextEditor} editor 32 | */ 33 | public constructor(range:Range, editor:TextEditor) 34 | { 35 | this.targetPosition = range.start; 36 | this.editor = editor; 37 | } 38 | 39 | /** 40 | * Load and test each type of signature to see if they can trigger and 41 | * if not load an empty block 42 | * 43 | * @returns {SnippetString} 44 | */ 45 | public autoDocument():SnippetString 46 | { 47 | let func = new FunctionBlock(this.targetPosition, this.editor); 48 | if (func.test()) { 49 | return func.parse().build(); 50 | } 51 | 52 | let prop = new Property(this.targetPosition, this.editor); 53 | if (prop.test()) { 54 | return prop.parse().build(); 55 | } 56 | 57 | let cla = new Class(this.targetPosition, this.editor); 58 | if (cla.test()) { 59 | return cla.parse().build(); 60 | } 61 | 62 | return new Doc().build(true); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/prepare-release.yml: -------------------------------------------------------------------------------- 1 | name: Prepare Release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: 'Version' 7 | required: true 8 | default: 'patch' 9 | 10 | jobs: 11 | prepare-release: 12 | name: Prepare Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | 19 | - name: Setup 20 | uses: ./.github/actions/setup 21 | 22 | - name: Verify PAT 23 | run: npm run verify-pat -- --pat ${{ secrets.VSCE_PAT }} 24 | 25 | - name: Update version number 26 | run: | 27 | npm version ${{ github.event.inputs.version }} 28 | echo "npm_package_version=$(<./out/version.txt)" >> $GITHUB_ENV 29 | echo "release_notes<> $GITHUB_ENV 30 | echo "$(<./out/RELEASE.md)" >> $GITHUB_ENV 31 | echo "EOF" >> $GITHUB_ENV 32 | 33 | - name: Stage changes 34 | run: | 35 | git add CHANGELOG.md package.json package-lock.json 36 | git commit -m "Release ${{ env.npm_package_version }}" 37 | 38 | - name: Create Pull Request 39 | id: create-pull-request 40 | uses: peter-evans/create-pull-request@v3 41 | with: 42 | author: php-docblocker 43 | committer: php-docblocker 44 | token: ${{ secrets.BOT_GITHUB_PAT }} 45 | title: ${{ env.npm_package_version }} 46 | branch: releases/${{ env.npm_package_version }} 47 | body: ${{ env.release_notes }} 48 | labels: release candidate 49 | 50 | - name: Enable Pull Request Automerge 51 | if: steps.create-pull-request.outputs.pull-request-operation == 'created' 52 | uses: peter-evans/enable-pull-request-automerge@v1 53 | with: 54 | token: ${{ secrets.BOT_GITHUB_PAT }} 55 | pull-request-number: ${{ steps.create-pull-request.outputs.pull-request-number }} -------------------------------------------------------------------------------- /src/util/config.ts: -------------------------------------------------------------------------------- 1 | import { workspace, TextEditor, Range, Position, TextDocument, WorkspaceConfiguration } from "vscode"; 2 | 3 | /** 4 | * Provides helper function to types 5 | */ 6 | export default class Config { 7 | 8 | /** 9 | * Holds the current instance 10 | * 11 | * @type {Config} 12 | */ 13 | private static _instance: Config; 14 | 15 | /** 16 | * Data to use when we aren't in live mode 17 | * 18 | * @type {Object} 19 | */ 20 | private data:{} 21 | 22 | /** 23 | * Are we in test mode or live 24 | * 25 | * @type {boolean} 26 | */ 27 | private isLive:boolean = true; 28 | 29 | /** 30 | * Returns the instance for this util 31 | * 32 | * @returns {Config} 33 | */ 34 | public static get instance(): Config { 35 | if (this._instance == null) { 36 | this._instance = new this(); 37 | } 38 | return this._instance; 39 | } 40 | 41 | /** 42 | * Set whether this is live mode or not 43 | * 44 | * @param {boolean} bool 45 | */ 46 | public set live(bool:boolean) 47 | { 48 | this.isLive = bool; 49 | } 50 | 51 | /** 52 | * Load in the defaults or the config 53 | */ 54 | public setFallback(config) 55 | { 56 | this.data = config; 57 | } 58 | 59 | /** 60 | * Add overrides 61 | * 62 | * @param overrides 63 | */ 64 | public override(overrides) 65 | { 66 | this.data = {...this.data, ...overrides}; 67 | } 68 | 69 | /** 70 | * Get a settings from the config or the mocked config 71 | * 72 | * @param {string} setting 73 | */ 74 | public get(setting:string) 75 | { 76 | if (this.isLive) { 77 | if (setting === "autoClosingBrackets") { 78 | return workspace.getConfiguration('editor').get(setting); 79 | } 80 | 81 | return workspace.getConfiguration('php-docblocker').get(setting); 82 | } 83 | 84 | return this.data[setting]; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import Completions from "./completions"; 3 | import Documenter from './documenter'; 4 | 5 | /** 6 | * Run a set up when the function is activated 7 | * 8 | * @param {vscode.ExtensionContext} context 9 | */ 10 | export function activate(context: vscode.ExtensionContext) 11 | { 12 | ['php', 'hack'].forEach(lang => { 13 | if (lang == 'hack') { 14 | vscode.languages.setLanguageConfiguration(lang, { 15 | wordPattern: /(-?\d*\.\d\w*)|([^\-\`\~\!\@\#\%\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g, 16 | onEnterRules: [ 17 | { 18 | // e.g. /** | */ 19 | beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, 20 | afterText: /^\s*\*\/$/, 21 | action: { indentAction: vscode.IndentAction.IndentOutdent, appendText: ' * ' } 22 | }, { 23 | // e.g. /** ...| 24 | beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, 25 | action: { indentAction: vscode.IndentAction.None, appendText: ' * ' } 26 | }, { 27 | // e.g. * ...| 28 | beforeText: /^(\t|(\ \ ))*\ \*(\ ([^\*]|\*(?!\/))*)?$/, 29 | action: { indentAction: vscode.IndentAction.None, appendText: '* ' } 30 | }, { 31 | // e.g. */| 32 | beforeText: /^(\t|(\ \ ))*\ \*\/\s*$/, 33 | action: { indentAction: vscode.IndentAction.None, removeText: 1 } 34 | } 35 | ] 36 | }); 37 | } 38 | 39 | vscode.languages.registerCompletionItemProvider(lang, new Completions(), '*', '@'); 40 | }); 41 | 42 | vscode.commands.registerTextEditorCommand('php-docblocker.trigger', (textEditor:vscode.TextEditor) => { 43 | textEditor.selection = new vscode.Selection(textEditor.selection.start, textEditor.selection.start); 44 | let range = new vscode.Range(textEditor.selection.start, textEditor.selection.end); 45 | let documenter = new Documenter(range, textEditor); 46 | let snippet = documenter.autoDocument(); 47 | textEditor.insertSnippet(snippet); 48 | }); 49 | } 50 | 51 | /** 52 | * Shutdown method for the extension 53 | */ 54 | export function deactivate() 55 | { 56 | } 57 | -------------------------------------------------------------------------------- /test/fixtures/properties.php: -------------------------------------------------------------------------------- 1 | public 6 | public $public; 7 | 8 | ////=> protected 9 | protected $protected; 10 | 11 | ////=> static 12 | protected static $static; 13 | 14 | ////=> static-alternate 15 | static protected $staticAlt; 16 | 17 | ////=> default-string 18 | protected $defaultString = 'string'; 19 | 20 | ////=> default-string-alternate 21 | public $defaultStringAlternate = "string"; 22 | 23 | ////=> default-bool 24 | public $defaultBool = false; 25 | 26 | ////=> default-bool-alternate 27 | public $defaultBoolAlternate = TRUE; 28 | 29 | ////=> default-array 30 | public $defaultArray = array(); 31 | 32 | ////=> default-array-alternate 33 | public $defaultArrayAlternate = []; 34 | 35 | ////=> multiline 36 | public $multiline = [ 37 | 'key' => 'value', 38 | 'key2' => 'value2' 39 | ]; 40 | 41 | ////=> multiline-alternate 42 | public $multilineAlternate = array( 43 | 'value', 44 | 'value2' 45 | ); 46 | 47 | ////=> default-float 48 | public $defaultFloat = -124124.50; 49 | 50 | ////=> default-int 51 | public $defaultInt = -214221; 52 | 53 | ////=> default-null 54 | public $defaultNull = null; 55 | 56 | ////=> typed-string 57 | public string $typedString; 58 | 59 | ////=> typed-int 60 | public int $typedInt; 61 | 62 | ////=> typed-interface 63 | public DateTimeInterface $typedInterface; 64 | 65 | ////=> typed-namespace 66 | public \App\Type\Test $typedNamespace; 67 | 68 | ////=> typed-string-default 69 | public string $typedStringDefault = 'test'; 70 | 71 | ////=> typed-int-default 72 | public int $typedIntDefault = 42; 73 | 74 | ////=> typed-string-nullable 75 | public ?string $typedStringNullable; 76 | 77 | ////=> typed-interface-nullable 78 | public ?DateTimeInterface $typedInterfaceNullable; 79 | 80 | ////=> typed-namespace-nullable 81 | public ?\App\Type\Test $typedNamespaceNullable; 82 | 83 | ////=> union-type-simple 84 | public string|array $unionTypeSimple; 85 | 86 | ////=> union-type-namespace 87 | public string|\DateTimeInterface $unionTypeNamespace; 88 | 89 | ////=> union-type-full-namespace 90 | public string|\App\Type\Test $unionTypeFullNamespace; 91 | 92 | ////=> union-type-nullable 93 | public string|array|null $unionTypeNullable; 94 | 95 | ////=> readonly 96 | public readonly string $readonly; 97 | } 98 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | import {TextEditor, TextDocument, WorkspaceConfiguration, workspace, window, Position} from 'vscode'; 2 | import * as fs from 'fs'; 3 | import Config from '../src/util/config'; 4 | 5 | export default class Helper { 6 | static fixturePath = __dirname + '/../../test/fixtures/'; 7 | 8 | public static loadFixture(fixture:string, callback: (editor:TextEditor, document:TextDocument) => any) { 9 | workspace.openTextDocument(Helper.fixturePath + fixture).then(textDocument => { 10 | window.showTextDocument(textDocument).then(textEditor => { 11 | callback.call(this, textEditor, textDocument); 12 | }, error => { 13 | console.log(error); 14 | }); 15 | }, error => { 16 | console.log(error); 17 | }); 18 | } 19 | 20 | public static getFixturePositions(document:TextDocument):any 21 | { 22 | let testPositions:any = {}; 23 | 24 | for (let line = 0; line < document.lineCount; line++) { 25 | let lineText = document.lineAt(line); 26 | if (!lineText.isEmptyOrWhitespace) { 27 | let pos = lineText.text.search(/\/\/\/\/=>/); 28 | if (pos !== -1) { 29 | let name = lineText.text.match(/\/\/\/\/=>\s*([a-z0-9-]+)\s*$/); 30 | if (name !== null) { 31 | testPositions[name[1]] = new Position(line, pos); 32 | } 33 | } 34 | } 35 | } 36 | 37 | return testPositions; 38 | } 39 | 40 | public static getFixtureMap(fixture:string):any { 41 | return JSON.parse(fs.readFileSync(Helper.fixturePath + fixture).toString()); 42 | } 43 | 44 | public static getConfig():Config { 45 | return Config.instance; 46 | } 47 | 48 | public static getDefaultConfig(): object 49 | { 50 | let config = {}; 51 | let packageJson = JSON.parse(fs.readFileSync(__dirname + '/../../package.json').toString()); 52 | let props = packageJson.contributes.configuration.properties; 53 | for (var key in props) { 54 | var item = props[key]; 55 | config[key.replace('php-docblocker.', '')] = item.default; 56 | } 57 | config["autoClosingBrackets"] = "always"; 58 | 59 | return config; 60 | } 61 | 62 | public static setConfig(overrides:any) { 63 | Config.instance.setFallback(Helper.getDefaultConfig()); 64 | Config.instance.live = false; 65 | Config.instance.override(overrides); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/completions.ts: -------------------------------------------------------------------------------- 1 | import {workspace, TextDocument, Position, CancellationToken, ProviderResult, CompletionItem, CompletionItemProvider, Range, SnippetString, CompletionItemKind, window} from "vscode"; 2 | import Documenter from "./documenter"; 3 | import Tags, { Tag } from "./tags"; 4 | import Config from "./util/config"; 5 | 6 | /** 7 | * Completions provider that can be registered to the language 8 | */ 9 | export default class Completions implements CompletionItemProvider 10 | { 11 | /** 12 | * Tags object 13 | * 14 | * @type {Tags} 15 | */ 16 | protected tags: Tags = new Tags(); 17 | 18 | /** 19 | * Implemented function to find and return completions either from 20 | * the tag list or initiate a complex completion 21 | * 22 | * @param {TextDocument} document 23 | * @param {Position} position 24 | * @param {CancellationToken} token 25 | * @returns {ProviderResult} 26 | */ 27 | public provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken):ProviderResult 28 | { 29 | let result = []; 30 | let match; 31 | 32 | if ((match = document.getWordRangeAtPosition(position, /\/\*\*/)) !== undefined) { 33 | let documenter:Documenter = new Documenter(match, window.activeTextEditor); 34 | 35 | let block = new CompletionItem("/**", CompletionItemKind.Snippet); 36 | block.detail = "PHP DocBlocker"; 37 | block.documentation = "Generate a PHP DocBlock from the code snippet below."; 38 | let range = document.getWordRangeAtPosition(position, /\/\*\* \*\//); 39 | block.range = range; 40 | block.insertText = documenter.autoDocument(); 41 | result.push(block); 42 | 43 | return result; 44 | } 45 | 46 | if ((match = document.getWordRangeAtPosition(position, /\@[a-z]*/)) === undefined) { 47 | return result; 48 | } 49 | 50 | let search = document.getText(match); 51 | 52 | let potential = this.getTags().filter((tag) => { 53 | return tag.tag.match(search) !== null; 54 | }); 55 | 56 | potential.forEach(tag => { 57 | let item = new CompletionItem(tag.tag, CompletionItemKind.Snippet); 58 | item.range = match; 59 | item.insertText = new SnippetString(tag.snippet); 60 | 61 | result.push(item); 62 | }); 63 | 64 | return result; 65 | } 66 | 67 | /** 68 | * Get the tag list for completions 69 | * 70 | * @returns {Array<{tag:string, snippet:string}>} 71 | */ 72 | protected getTags(): Tag[] 73 | { 74 | let tags: Tag[] = this.tags.list; 75 | 76 | tags.forEach((tag, index) => { 77 | if (tag.tag == '@author') { 78 | tag.snippet = tag.snippet.replace("{{name}}", Config.instance.get('author').name); 79 | tag.snippet = tag.snippet.replace("{{email}}", Config.instance.get('author').email); 80 | tags[index] = tag; 81 | } 82 | }); 83 | 84 | return tags; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/fixtures/properties.php.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": "public", 4 | "name": "Public", 5 | "var": "[type]" 6 | }, 7 | { 8 | "key": "protected", 9 | "name": "Protected", 10 | "var": "[type]" 11 | }, 12 | { 13 | "key": "static", 14 | "name": "Static", 15 | "var": "[type]" 16 | }, 17 | { 18 | "key": "static-alternate", 19 | "name": "Static Alternate", 20 | "var": "[type]" 21 | }, 22 | { 23 | "key": "default-string", 24 | "name": "Default String", 25 | "var": "string" 26 | }, 27 | { 28 | "key": "default-string-alternate", 29 | "name": "Default String Alternate", 30 | "var": "string" 31 | }, 32 | { 33 | "key": "default-bool", 34 | "name": "Default Bool", 35 | "var": "boolean" 36 | }, 37 | { 38 | "key": "default-bool-alternate", 39 | "name": "Default Bool Alternate", 40 | "var": "boolean" 41 | }, 42 | { 43 | "key": "default-array", 44 | "name": "Default Array", 45 | "var": "array" 46 | }, 47 | { 48 | "key": "default-array-alternate", 49 | "name": "Default Array Alternate", 50 | "var": "array" 51 | }, 52 | { 53 | "key": "multiline", 54 | "name": "Multiline", 55 | "var": "array" 56 | }, 57 | { 58 | "key": "multiline-alternate", 59 | "name": "Multiline Alternate", 60 | "var": "array" 61 | }, 62 | { 63 | "key": "default-float", 64 | "name": "Default Float", 65 | "var": "float" 66 | }, 67 | { 68 | "key": "default-int", 69 | "name": "Default Int", 70 | "var": "integer" 71 | }, 72 | { 73 | "key": "default-null", 74 | "name": "Default Null", 75 | "var": "[type]" 76 | }, 77 | { 78 | "key": "typed-string", 79 | "name": "Typed string", 80 | "var": "string" 81 | }, 82 | { 83 | "key": "typed-int", 84 | "name": "Typed Int", 85 | "var": "integer" 86 | }, 87 | { 88 | "key": "typed-interface", 89 | "name": "Typed Interface", 90 | "var": "DateTimeInterface" 91 | }, 92 | { 93 | "key": "typed-namespace", 94 | "name": "Typed namespace", 95 | "var": "\\App\\Type\\Test" 96 | }, 97 | { 98 | "key": "typed-string-default", 99 | "name": "Typed string with default", 100 | "var": "string" 101 | }, 102 | { 103 | "key": "typed-int-default", 104 | "name": "Typed Int with default", 105 | "var": "integer" 106 | }, 107 | { 108 | "key": "typed-string-nullable", 109 | "name": "Nullable typed string", 110 | "var": "string|null" 111 | }, 112 | { 113 | "key": "typed-interface-nullable", 114 | "name": "Nullable typed inteface", 115 | "var": "DateTimeInterface|null" 116 | }, 117 | { 118 | "key": "typed-namespace-nullable", 119 | "name": "Nullable typed namespace", 120 | "var": "\\App\\Type\\Test|null", 121 | "config": { 122 | "qualifyClassNames": true 123 | } 124 | }, 125 | { 126 | "key": "union-type-simple", 127 | "name": "Simple PHP8 union type", 128 | "var": "string|array" 129 | }, 130 | { 131 | "key": "union-type-namespace", 132 | "name": "PHP8 union type with one namespace", 133 | "var": "string|\\DateTimeInterface" 134 | }, 135 | { 136 | "key": "union-type-full-namespace", 137 | "name": "PHP8 union type with one namespace", 138 | "var": "string|\\App\\Type\\Test" 139 | }, 140 | { 141 | "key": "union-type-nullable", 142 | "name": "PHP8 union type that can be null", 143 | "var": "string|array|null" 144 | }, 145 | { 146 | "key": "readonly", 147 | "name": "PHP8.1 readonly", 148 | "var": "string" 149 | } 150 | ] 151 | -------------------------------------------------------------------------------- /src/block/function.ts: -------------------------------------------------------------------------------- 1 | import { Block } from "../block"; 2 | import { Doc, Param } from "../doc"; 3 | import TypeUtil from "../util/TypeUtil"; 4 | import { Range, Position } from "vscode"; 5 | import Config from "../util/config"; 6 | 7 | /** 8 | * Represents a function code block 9 | * 10 | * This is probably going to be the most complicated of all the 11 | * blocks as function signatures tend to be the most complex and 12 | * varied 13 | */ 14 | export default class FunctionBlock extends Block 15 | { 16 | /** 17 | * @inheritdoc 18 | */ 19 | protected pattern:RegExp = /^\s*((.*)(protected|private|public))?(.*)?\s*function\s+&?([a-z0-9_]+)\s*\(([^{;]*)/im; 20 | 21 | /** 22 | * @inheritdoc 23 | */ 24 | public parse():Doc 25 | { 26 | let params = this.match(); 27 | 28 | let doc = new Doc('Undocumented function'); 29 | doc.template = Config.instance.get('functionTemplate'); 30 | let argString = this.getEnclosed(params[6], "(", ")"); 31 | let head:string; 32 | 33 | 34 | if (argString != "") { 35 | let args = this.getSplitWithoutEnclosed(argString); 36 | 37 | if (Config.instance.get('qualifyClassNames')) { 38 | head = this.getClassHead(); 39 | } 40 | 41 | for (let index = 0; index < args.length; index++) { 42 | let arg:string = args[index]; 43 | let parts:string[] = arg.match(/^\s*(?:(?:public|protected|private)\s+)?(?:readonly\s+)?(\?)?\s*([A-Za-z0-9_\\][A-Za-z0-9_\\|&]+)?\s*\&?((?:[.]{3})?\$[A-Za-z0-9_]+)\s*\=?\s*(.*)\s*/im); 44 | var type:string = TypeUtil.instance.getDefaultType(); 45 | 46 | if (parts[2] != null) { 47 | type = TypeUtil.instance.getResolvedTypeHints(parts[2], head); 48 | } 49 | 50 | if (parts[2] != null && parts[1] === '?') { 51 | type += '|null'; 52 | } else if (parts[2] != null && parts[4] != null && parts[2] != "mixed" && parts[4] == "null") { 53 | type += '|null'; 54 | } else if (parts[4] != null && parts[4] != "" && parts[2] != "mixed") { 55 | type = TypeUtil.instance.getFormattedTypeByName(TypeUtil.instance.getTypeFromValue(parts[4])); 56 | } 57 | 58 | doc.params.push(new Param(type, parts[3])); 59 | } 60 | } 61 | 62 | let returnType:Array = this.signature.match(/.*\)\s*\:\s*(\?)?\s*([a-zA-Z_|0-9\\]+)\s*$/m); 63 | 64 | if (returnType != null) { 65 | returnType[2] = TypeUtil.instance.getResolvedTypeHints(returnType[2], this.getClassHead()); 66 | 67 | doc.return = (returnType[1] === '?') 68 | ? TypeUtil.instance.getFormattedTypeByName(returnType[2])+'|null' 69 | : TypeUtil.instance.getFormattedTypeByName(returnType[2]); 70 | } else { 71 | doc.return = this.getReturnFromName(params[5]); 72 | } 73 | 74 | return doc; 75 | } 76 | 77 | /** 78 | * We can usually assume that these function names will 79 | * be certain return types and we can save ourselves some 80 | * effort by checking these 81 | * 82 | * @param {string} name 83 | * @returns {string} 84 | */ 85 | public getReturnFromName(name:string):string 86 | { 87 | if (/^(is|has|can|should)(?:[A-Z0-9_]|$)/.test(name)) { 88 | return TypeUtil.instance.getFormattedTypeByName('bool'); 89 | } 90 | 91 | switch (name) { 92 | case '__construct': 93 | case '__destruct': 94 | case '__set': 95 | case '__unset': 96 | case '__wakeup': 97 | return null; 98 | case '__isset': 99 | return TypeUtil.instance.getFormattedTypeByName('bool'); 100 | case '__sleep': 101 | case '__debugInfo': 102 | return 'array'; 103 | case '__toString': 104 | return 'string'; 105 | } 106 | 107 | return 'void'; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/util/TypeUtil.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor, Range, Position, TextDocument } from "vscode"; 2 | import Config from "./config"; 3 | 4 | /** 5 | * Provides helper function to types 6 | */ 7 | export default class TypeUtil { 8 | /** 9 | * Holds the current instance 10 | * 11 | * @type {TypeUtil} 12 | */ 13 | private static _instance: TypeUtil; 14 | 15 | /** 16 | * Returns the instance for this util 17 | * 18 | * @returns {TypeUtil} 19 | */ 20 | public static get instance(): TypeUtil { 21 | return this._instance || (this._instance = new this()); 22 | } 23 | 24 | /** 25 | * Resolve a type string that may contain union types 26 | * 27 | * @param {string} types 28 | * @param {string} head 29 | * @returns {string} 30 | */ 31 | public getResolvedTypeHints(types:string, head:string = null): string 32 | { 33 | let union:string[] = types.split(/([|&])/); 34 | for (let index = 0; index < union.length; index += 2) { 35 | if (union[index] === '') { 36 | delete union[index]; 37 | delete union[index+1]; 38 | continue; 39 | } 40 | union[index] = this.getFullyQualifiedType(union[index], head); 41 | union[index] = this.getFormattedTypeByName(union[index]); 42 | } 43 | 44 | return union.join(''); 45 | } 46 | 47 | /** 48 | * Get the full qualified class namespace for a type 49 | * we'll need to access the document 50 | * 51 | * @param {string} type 52 | * @param {string} head 53 | * @returns {string} 54 | */ 55 | public getFullyQualifiedType(type:string, head:string):string 56 | { 57 | if (!head) { 58 | return type; 59 | } 60 | if (!Config.instance.get('qualifyClassNames')) { 61 | return type; 62 | } 63 | 64 | let useEx = /[\s;]?use\s+(?:(const|function)\s*)?([\s\S]*?)\s*;/gmi; 65 | let exec: RegExpExecArray; 66 | while (exec = useEx.exec(head)) { 67 | let isConstOrFunc = exec[1]; 68 | let use = exec[2]; 69 | 70 | if (isConstOrFunc) { 71 | continue; 72 | } 73 | 74 | let clazz = this.getClassesFromUse(use)[type]; 75 | if (clazz === undefined) { 76 | continue; 77 | } 78 | 79 | if (clazz.charAt(0) != '\\') { 80 | clazz = '\\' + clazz; 81 | } 82 | return clazz; 83 | } 84 | 85 | return type; 86 | } 87 | 88 | /** 89 | * Returns the classes from the use 90 | * 91 | * @param use 92 | * @returns 93 | */ 94 | public getClassesFromUse(use: string): { [index: string]: string } { 95 | let namespace: string; 96 | let classes: string[]; 97 | let hasBracket = use.indexOf('{') !== -1; 98 | if (hasBracket) { 99 | let bracketBegin = use.indexOf('{'); 100 | let bracketEnd = (use + '}').indexOf('}'); 101 | namespace = use.substring(0, bracketBegin).trim(); 102 | classes = use.substring(bracketBegin + 1, bracketEnd).split(','); 103 | } else { 104 | namespace = ''; 105 | classes = use.split(','); 106 | } 107 | 108 | var results: { [index: string]: string } = {}; 109 | for (let index = 0; index < classes.length; index++) { 110 | let alias: string; 111 | let clazz = classes[index].trim(); 112 | if (clazz === '') { 113 | continue; 114 | } 115 | 116 | clazz = namespace + clazz; 117 | 118 | [clazz, alias] = clazz.split(/\s+as\s+/gmi, 2); 119 | 120 | if (alias === undefined || alias === '') { 121 | alias = clazz.substring(clazz.lastIndexOf('\\') + 1); 122 | } 123 | 124 | results[alias] = clazz; 125 | } 126 | return results; 127 | } 128 | 129 | /** 130 | * Returns the user configuration based name for the given type 131 | * 132 | * @param {string} name 133 | */ 134 | public getFormattedTypeByName(name:string) { 135 | switch (name) { 136 | case 'bool': 137 | case 'boolean': 138 | if (!Config.instance.get('useShortNames')) { 139 | return 'boolean'; 140 | } 141 | return 'bool'; 142 | case 'int': 143 | case 'integer': 144 | if (!Config.instance.get('useShortNames')) { 145 | return 'integer'; 146 | } 147 | return 'int'; 148 | default: 149 | return name; 150 | } 151 | } 152 | 153 | 154 | /** 155 | * Take the value and parse and try to infer its type 156 | * 157 | * @param {string} value 158 | * @returns {string} 159 | */ 160 | public getTypeFromValue(value:string):string 161 | { 162 | let result:Array; 163 | 164 | // Check for bool 165 | if (value.match(/^\s*(false|true)\s*$/i) !== null || value.match(/^\s*\!/i) !== null) { 166 | return this.getFormattedTypeByName('bool'); 167 | } 168 | 169 | // Check for int 170 | if (value.match(/^\s*([\d-]+)\s*$/) !== null) { 171 | return this.getFormattedTypeByName('int'); 172 | } 173 | 174 | // Check for float 175 | if (value.match(/^\s*([\d.-]+)\s*$/) !== null) { 176 | return 'float'; 177 | } 178 | 179 | // Check for string 180 | if (value.match(/^\s*(["'])/) !== null || value.match(/^\s*<<", 9 | "engines": { 10 | "vscode": "^1.60.0" 11 | }, 12 | "categories": [ 13 | "Programming Languages" 14 | ], 15 | "keywords": [ 16 | "php", 17 | "autocomplete", 18 | "docblock" 19 | ], 20 | "activationEvents": [ 21 | "onLanguage:php", 22 | "onLanguage:hack" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/neild3r/vscode-php-docblocker.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/neild3r/vscode-php-docblocker/issues" 30 | }, 31 | "capabilities": { 32 | "untrustedWorkspaces": { 33 | "supported": true 34 | } 35 | }, 36 | "extensionKind": [ 37 | "workspace", 38 | "ui" 39 | ], 40 | "icon": "images/logo.png", 41 | "browser": "./out/src/extension.js", 42 | "main": "./out/src/extension.js", 43 | "contributes": { 44 | "commands": [ 45 | { 46 | "command": "php-docblocker.trigger", 47 | "title": "Insert PHP Docblock" 48 | } 49 | ], 50 | "configuration": { 51 | "type": "object", 52 | "title": "PHP DocBlocker", 53 | "properties": { 54 | "php-docblocker.gap": { 55 | "type": "boolean", 56 | "default": true, 57 | "description": "If there should be a gap between the description and tags" 58 | }, 59 | "php-docblocker.returnGap": { 60 | "type": "boolean", 61 | "default": false, 62 | "description": "If there should be a gap between params and return" 63 | }, 64 | "php-docblocker.defaultType": { 65 | "type": "string", 66 | "default": "[type]", 67 | "description": "Default type to use if a type wasn't able to be detected" 68 | }, 69 | "php-docblocker.returnVoid": { 70 | "type": "boolean", 71 | "default": true, 72 | "description": "Should we return void if there is no detectable return type?" 73 | }, 74 | "php-docblocker.extra": { 75 | "type": "array", 76 | "default": [], 77 | "description": "Extra tags you wish to include in every DocBlock" 78 | }, 79 | "php-docblocker.useShortNames": { 80 | "type": "boolean", 81 | "default": false, 82 | "description": "Whether you want to use int instead of integer and bool instead of boolean." 83 | }, 84 | "php-docblocker.qualifyClassNames": { 85 | "type": "boolean", 86 | "default": false, 87 | "description": "Fully qualifies any data types used in param and returns by reading the namespaces." 88 | }, 89 | "php-docblocker.alignParams": { 90 | "type": "boolean", 91 | "default": false, 92 | "description": "Vertically aligns param types and names with additional spaces." 93 | }, 94 | "php-docblocker.alignReturn": { 95 | "type": "boolean", 96 | "default": false, 97 | "description": "Vertically aligns return with above param statements." 98 | }, 99 | "php-docblocker.varDescription": { 100 | "type": [ 101 | "string", 102 | "boolean" 103 | ], 104 | "default": false, 105 | "description": "Include a description placeholder for @var tags" 106 | }, 107 | "php-docblocker.paramDescription": { 108 | "type": [ 109 | "string", 110 | "boolean" 111 | ], 112 | "default": false, 113 | "description": "Include a description placeholder for @param tags" 114 | }, 115 | "php-docblocker.returnDescription": { 116 | "type": [ 117 | "string", 118 | "boolean" 119 | ], 120 | "default": false, 121 | "description": "Include a description placeholder for @return tags" 122 | }, 123 | "php-docblocker.functionTemplate": { 124 | "type": "object", 125 | "default": null, 126 | "description": "Specify the default template for functions." 127 | }, 128 | "php-docblocker.propertyTemplate": { 129 | "type": "object", 130 | "default": null, 131 | "description": "Specify the default template for class variables." 132 | }, 133 | "php-docblocker.classTemplate": { 134 | "type": "object", 135 | "default": null, 136 | "description": "Specify the default template for classes." 137 | }, 138 | "php-docblocker.author": { 139 | "type": "object", 140 | "default": { 141 | "name": "Name", 142 | "email": "email@email.com" 143 | }, 144 | "description": "Default author tag" 145 | } 146 | } 147 | } 148 | }, 149 | "scripts": { 150 | "clean": "rm -rf ./coverage/* ./.nyc_output ./out", 151 | "verify-pat": "vsce verify-pat", 152 | "vscode:prepublish": "npm run -S esbuild-base -- --minify", 153 | "lint": "tslint -p ./", 154 | "build": "npm run -S esbuild-base -- --sourcemap", 155 | "watch": "tsc -watch -p ./", 156 | "pretest": "tsc -p ./", 157 | "preversion": "tsc -p ./", 158 | "version": "node ./out/scripts/updateChangelog.js", 159 | "test": "node ./out/test/runTest.js", 160 | "web": "npm run build && vscode-test-web --browserType=webkit --extensionDevelopmentPath=./", 161 | "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/src/extension.js --external:vscode --format=cjs --platform=node", 162 | "package": "vsce package --out ./out/package.vsix", 163 | "publish": "vsce publish --packagePath ./out/package.vsix", 164 | "deploy": "npm run package && npm run publish" 165 | }, 166 | "devDependencies": { 167 | "@types/mocha": "^5.2.6", 168 | "@types/node": "^6.14.13", 169 | "@types/vscode": "^1.60.0", 170 | "@vscode/test-web": "^0.0.7", 171 | "coveralls": "^3.1.1", 172 | "esbuild": "^0.12.29", 173 | "markdown-table-ts": "^1.0.3", 174 | "mocha": "^9.1.1", 175 | "nyc": "^15.1.0", 176 | "typescript": "^3.9.10", 177 | "vsce": "^1.100.0", 178 | "vscode-test": "^1.6.1" 179 | }, 180 | "dependencies": {} 181 | } 182 | -------------------------------------------------------------------------------- /test/TypeUtil.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import TypeUtil from "../src/util/TypeUtil"; 3 | import { window, workspace, TextDocument, Position, TextEditor } from 'vscode'; 4 | import Helper from './helpers'; 5 | import FunctionBlock from '../src/block/function'; 6 | 7 | suite("TypeUtil tests: ", () => { 8 | let editor:TextEditor; 9 | let head:string; 10 | 11 | suiteSetup(function(done) { 12 | workspace.openTextDocument(Helper.fixturePath+'namespace.php').then(doc => { 13 | window.showTextDocument(doc).then(textEditor => { 14 | editor = textEditor; 15 | let block = new FunctionBlock(new Position(0, 0), editor); 16 | head = block.getClassHead(); 17 | done(); 18 | }, error => { 19 | console.log(error); 20 | }) 21 | }, error => { 22 | console.log(error); 23 | }); 24 | }); 25 | 26 | test("Test the classes returned from use is correct", () => { 27 | let type = new TypeUtil; 28 | assert.deepEqual(type.getClassesFromUse('ClassA'), { 'ClassA': 'ClassA' }); 29 | assert.deepEqual(type.getClassesFromUse('ClassB as B'), { 'B': 'ClassB' }); 30 | assert.deepEqual(type.getClassesFromUse('ClassA, namespace\\ClassB as B'), { 'ClassA': 'ClassA', 'B': 'namespace\\ClassB' }); 31 | assert.deepEqual(type.getClassesFromUse('namespace\\{ ClassA, ClassB as B }'), { 'ClassA': 'namespace\\ClassA', 'B': 'namespace\\ClassB' }); 32 | }); 33 | 34 | test("Ensure typehint is not mismatched", () => { 35 | let type = new TypeUtil; 36 | Helper.setConfig({qualifyClassNames: true}); 37 | assert.equal(type.getFullyQualifiedType('Example', head), 'Example'); 38 | }); 39 | 40 | test("Fully qualify typehint from namespace", () => { 41 | let type = new TypeUtil; 42 | Helper.setConfig({qualifyClassNames: true}); 43 | assert.equal(type.getFullyQualifiedType('FilterInterface', head), '\\App\\Test\\Model\\FilterInterface'); 44 | }); 45 | 46 | test("Fully qualify typehint from namespace with prefix", () => { 47 | let type = new TypeUtil; 48 | Helper.setConfig({qualifyClassNames: true}); 49 | assert.equal(type.getFullyQualifiedType('StreamHandler', head), '\\Monolog\\Handler\\StreamHandler'); 50 | }); 51 | 52 | test("Fully qualify typehint from namespace use with alias", () => { 53 | let type = new TypeUtil; 54 | Helper.setConfig({qualifyClassNames: true}); 55 | assert.equal(type.getFullyQualifiedType('BaseExample', head), '\\App\\Test\\Model\\Example'); 56 | }); 57 | 58 | test("Fully qualify typehint from namespace use with _", () => { 59 | let type = new TypeUtil; 60 | Helper.setConfig({qualifyClassNames: true}); 61 | assert.equal(type.getFullyQualifiedType('ExampleInterface', head), '\\App\\Example\\ExampleInterface'); 62 | }); 63 | 64 | test("Fully qualify typehint from namespace use with bracket", () => { 65 | let type = new TypeUtil; 66 | Helper.setConfig({qualifyClassNames: true}); 67 | assert.equal(type.getFullyQualifiedType('ClassA', head), '\\some\\namespace\\ClassA'); 68 | }); 69 | 70 | test("Fully qualify typehint from namespace use with bracket+alias", () => { 71 | let type = new TypeUtil; 72 | Helper.setConfig({qualifyClassNames: true}); 73 | assert.equal(type.getFullyQualifiedType('ClassB_alias', head), '\\some\\namespace\\ClassB'); 74 | }); 75 | 76 | test("Fully qualify typehint from namespace use with comma", () => { 77 | let type = new TypeUtil; 78 | Helper.setConfig({qualifyClassNames: true}); 79 | assert.equal(type.getFullyQualifiedType('ClassD', head), '\\some\\namespace\\ClassD'); 80 | }); 81 | 82 | test("Fully qualify typehint from namespace use with comma+alias", () => { 83 | let type = new TypeUtil; 84 | Helper.setConfig({qualifyClassNames: true}); 85 | assert.equal(type.getFullyQualifiedType('ClassE_alias', head), '\\some\\namespace\\ClassE'); 86 | }); 87 | 88 | test("Fully qualify typehint from namespace use const", () => { 89 | let type = new TypeUtil; 90 | Helper.setConfig({qualifyClassNames: true}); 91 | assert.equal(type.getFullyQualifiedType('myconst', head), 'myconst'); 92 | }); 93 | 94 | test("Fully qualify typehint from namespace use function", () => { 95 | let type = new TypeUtil; 96 | Helper.setConfig({qualifyClassNames: true}); 97 | assert.equal(type.getFullyQualifiedType('myfunction', head), 'myfunction'); 98 | }); 99 | 100 | test("With default settings the integer type formatted is integer", () => { 101 | let type = new TypeUtil; 102 | assert.equal(type.getFormattedTypeByName('int'), 'integer'); 103 | }); 104 | 105 | test("With default settings the boolean type formatted is boolean", () => { 106 | let type = new TypeUtil; 107 | assert.equal(type.getFormattedTypeByName('bool'), 'boolean'); 108 | }); 109 | 110 | test('With special settings the integer type formatted is int', () => { 111 | let type = new TypeUtil; 112 | Helper.setConfig({useShortNames: true}); 113 | assert.equal(type.getFormattedTypeByName('int'), 'int'); 114 | }) 115 | 116 | test('With special settings the boolean type formatted is bool', () => { 117 | let type = new TypeUtil; 118 | Helper.setConfig({useShortNames: true}); 119 | assert.equal(type.getFormattedTypeByName('bool'), 'bool'); 120 | }) 121 | 122 | test("Unknown types won't be touched", () => { 123 | let type = new TypeUtil; 124 | assert.equal(type.getFormattedTypeByName('helloWorld'), 'helloWorld'); 125 | }); 126 | 127 | test("Undefined type uses config", () => { 128 | let type = new TypeUtil; 129 | Helper.setConfig({defaultType: 'mixed'}); 130 | assert.equal(type.getDefaultType(), 'mixed'); 131 | }); 132 | }); 133 | 134 | suite("TypeUtil issue test: ", () => { 135 | let editor:TextEditor; 136 | 137 | suiteSetup(function(done) { 138 | workspace.openTextDocument(Helper.fixturePath+'namespace-issue.php').then(doc => { 139 | window.showTextDocument(doc).then(textEditor => { 140 | editor = textEditor; 141 | done(); 142 | }, error => { 143 | console.log(error); 144 | }) 145 | }, error => { 146 | console.log(error); 147 | }); 148 | }); 149 | 150 | test("Ensure class head does not fail if there isn't one", () => { 151 | let block = new FunctionBlock(new Position(0, 0), editor); 152 | let head = block.getClassHead(); 153 | 154 | assert.equal(head, null); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /test/fixtures/functions.php: -------------------------------------------------------------------------------- 1 | simple 6 | public function test() 7 | { 8 | } 9 | 10 | ////=> simple-static 11 | public static function test2() 12 | { 13 | } 14 | 15 | ////=> constructor-no-vis 16 | function __construct() 17 | { 18 | } 19 | 20 | ////=> abstract-simple 21 | abstract public function getName(); 22 | 23 | ////=> abstract-static 24 | abstract static public function getStaticName(); 25 | 26 | ////=> abstract-static-alt 27 | static abstract protected function getNameStatic(); 28 | 29 | 30 | ////=> final-static 31 | final private static function getFinalPrivateName() 32 | { 33 | } 34 | 35 | ////=> final 36 | final public function getFinalName() 37 | { 38 | } 39 | 40 | ////=> params 41 | public function getParams($var, $var2, $var3) 42 | { 43 | } 44 | 45 | ////=> params-complex 46 | final protected static function getComplexParams(&$var, Hint $var2, $var3 = false) 47 | { 48 | } 49 | 50 | ////=> multiline 51 | public function getMultiline( 52 | $var, 53 | $var2, 54 | $var3 55 | ) { 56 | } 57 | 58 | ////=> multiline-complex 59 | final static public function getComplexMultiline( 60 | TypeHint $var, 61 | &$var2, 62 | $var3 = "default" 63 | ) { 64 | } 65 | 66 | ////=> nullable-return-type 67 | public function nullableReturnType(): ?string 68 | { 69 | } 70 | 71 | ////=> nullable-args 72 | public function nullableArgs(?TypeHint $var, ?\Type2 $var2, ?string $var3) 73 | { 74 | } 75 | 76 | ////=> param-types 77 | public function getParamTypes( 78 | TypeHint $hint, 79 | $boolean = true, 80 | $string = 'single quotes', 81 | $string2 = "double quotes", 82 | $int = 42141513, 83 | $float = 109.50, 84 | $array = [], 85 | $array2 = array() 86 | ) { 87 | } 88 | 89 | ////=> array-params 90 | public function arrayParams($var = array('a', array()), $var2 = array()) 91 | { 92 | } 93 | 94 | ////=> param-namespace 95 | public function paramNamespaced(\TypeHint $hint, $test) 96 | { 97 | } 98 | 99 | ////=> param-namespace-full 100 | public function paramNamespacedFull(App\Model\TypeHint $hint, $test) 101 | { 102 | } 103 | 104 | ////=> param-default-null 105 | public function paramDefaultNull(int $arg = null) 106 | { 107 | } 108 | 109 | ////=> param-mixed-default-null 110 | public function paramMixedDefaultNull(mixed $arg = null) 111 | { 112 | } 113 | 114 | ////=> args 115 | public function dotArgs(...$args) { 116 | } 117 | 118 | ////=> args-typed 119 | public function dotArgsTyped(int ...$args) { 120 | } 121 | 122 | ////=> args-typed-long 123 | public function dotArgsTypedLong(int ...$args) { 124 | } 125 | 126 | ////=> php7-return 127 | public function getPHP7Return(): TypeHint { 128 | } 129 | 130 | ////=> php7-return-snake 131 | public function getPHP7ReturnSnake(): Type_Hint3 { 132 | } 133 | 134 | ////=> php7-return-alt 135 | public function getPHP7ReturnAlt():float 136 | { 137 | } 138 | 139 | ////=> php7-return-param 140 | public function getPHP7ReturnParam(float $param) :int 141 | { 142 | } 143 | 144 | ////=> php7-return-param-long 145 | public function getPHP7ReturnParamLong(float $param) :int 146 | { 147 | } 148 | 149 | ////=> php7-return-multiline 150 | public function getPHP7ReturnMultiline( 151 | float $param, 152 | bool $boolean = false 153 | ) : int { 154 | } 155 | 156 | ////=> php7-return-multiline-long 157 | public function getPHP7ReturnMultilineLong( 158 | float $param, 159 | bool $boolean = false 160 | ) : int { 161 | } 162 | 163 | ////=> php7-return-namespace 164 | public function getPHP7ReturnNamespace():\TypeHint 165 | { 166 | } 167 | 168 | ////=> php7-return-namespace-full 169 | public function getPHP7ReturnNamespaceFull():App\Model\TypeHint 170 | { 171 | } 172 | 173 | ////=> php8-return-union-types 174 | public function getPHP8ReturnUnionTypes():int|bool|\TypeHint|App\Model\TypeHint 175 | { 176 | } 177 | 178 | ////=> php8-return-union-types-with-short-name 179 | public function getPHP8ReturnUnionTypesShortName():int|bool|\TypeHint|App\Model\TypeHint 180 | { 181 | } 182 | 183 | ////=> php8-param-union-types 184 | public function getPHP8ParamUnionTypes(int|bool|\TypeHint|App\Model\TypeHint $arg, string|\Closure ...$args) 185 | { 186 | } 187 | 188 | ////=> php8-constructor-promotion 189 | function __construct($arg1, protected int $arg2, private ?int $arg3 = 1, public string $arg4 = 'var') 190 | { 191 | } 192 | 193 | ////=> trailing-comma 194 | public function trailingComma(int $var, string $var2,) 195 | { 196 | } 197 | 198 | ////=> trailing-comma-multi 199 | public function trailingCommaMulit( 200 | int $var, 201 | Class_Name $var2, 202 | ) { 203 | } 204 | 205 | ////=> php81-intersection-types 206 | function intersection_types(Iterator&\Countable $var) 207 | { 208 | } 209 | 210 | ////=> php81-union-intersection-types-fault-tolerant 211 | function union_intersection_types_fault_tolerant(Iterator&\Countable|Aaa||&|&Bbb &$var) 212 | { 213 | } 214 | 215 | ////=> function-reference 216 | public function &someFunction() 217 | { 218 | } 219 | 220 | ////=> is 221 | public function isSomething() 222 | { 223 | } 224 | 225 | ////=> is-void 226 | public function isotope() 227 | { 228 | } 229 | 230 | ////=> is-only 231 | public function is() 232 | { 233 | } 234 | 235 | ////=> has 236 | public function hasValue() 237 | { 238 | } 239 | 240 | ////=> has-void 241 | public function hashed() 242 | { 243 | } 244 | 245 | ////=> has-only 246 | public function has() 247 | { 248 | } 249 | 250 | ////=> can 251 | public function canValue() 252 | { 253 | } 254 | 255 | ////=> can-void 256 | public function cancel() 257 | { 258 | } 259 | 260 | ////=> can-only 261 | public function can() 262 | { 263 | } 264 | 265 | ////=> should 266 | public function shouldDoSomething() 267 | { 268 | } 269 | 270 | ////=> should-void 271 | public function shouldasdf() 272 | { 273 | } 274 | 275 | ////=> should-only 276 | public function should() 277 | { 278 | } 279 | 280 | ////=> debug-info 281 | public function __debugInfo() 282 | { 283 | } 284 | 285 | ////=> wakeup 286 | public function __wakeup() 287 | { 288 | } 289 | 290 | ////=> sleep 291 | public function __sleep() 292 | { 293 | } 294 | 295 | ////=> isset 296 | public function __isset($name) 297 | { 298 | } 299 | 300 | ////=> unset 301 | public function __unset($name) 302 | { 303 | } 304 | 305 | ////=> set 306 | public function __set($name, $value) 307 | { 308 | } 309 | 310 | ////=> to-string 311 | public function __toString() 312 | { 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/block.ts: -------------------------------------------------------------------------------- 1 | import {Range, Position, TextEditor, workspace, SnippetString} from "vscode"; 2 | import {Param, Doc} from './doc'; 3 | import TypeUtil from "./util/TypeUtil"; 4 | 5 | /** 6 | * Represents a potential code block. 7 | * 8 | * This abstract class serves as a base class that includes lots of 9 | * helpers for dealing with blocks of code and has the basic interface 10 | * for working with the documenter object 11 | */ 12 | export abstract class Block 13 | { 14 | /** 15 | * Regex pattern for the basic signiture match 16 | * 17 | * @type {RegExp} 18 | */ 19 | protected pattern:RegExp; 20 | 21 | /** 22 | * The position of the starting signiture 23 | * 24 | * @type {Position} 25 | */ 26 | protected position:Position; 27 | 28 | /** 29 | * Text editor instance which we'll need to do things like 30 | * get text and ranges and things between ranges 31 | * 32 | * @type {TextEditor} 33 | */ 34 | protected editor:TextEditor; 35 | 36 | /** 37 | * The whole signature string ready for parsing 38 | * 39 | * @type {string} 40 | */ 41 | protected signature:string; 42 | 43 | /** 44 | * Default signature end pattern 45 | * 46 | * @type {RegExp} 47 | */ 48 | protected signatureEnd:RegExp = /[\{;]/; 49 | 50 | /** 51 | * Class heading 52 | * 53 | * @type {string} 54 | */ 55 | protected classHead:string; 56 | 57 | /** 58 | * Creates an instance of Block. 59 | * 60 | * @param {Position} position 61 | * @param {TextEditor} editor 62 | */ 63 | public constructor(position:Position, editor:TextEditor) 64 | { 65 | this.position = position; 66 | this.editor = editor; 67 | this.setSignature(this.getBlock(position, this.signatureEnd)); 68 | } 69 | 70 | /** 71 | * This should be a simple test to determine wether this matches 72 | * our intended block signiture and we can proceed to properly 73 | * match 74 | * 75 | * @returns {boolean} 76 | */ 77 | public test():boolean 78 | { 79 | return this.pattern.test(this.signature); 80 | } 81 | 82 | /** 83 | * Run a match to break the signiture into the constituent parts 84 | * 85 | * @returns {object} 86 | */ 87 | public match():object 88 | { 89 | return this.signature.match(this.pattern); 90 | } 91 | 92 | /** 93 | * Set up the signiture string. 94 | * 95 | * This is usually detected from the position 96 | * 97 | * @param {string} signiture 98 | */ 99 | public setSignature(signiture:string):void 100 | { 101 | this.signature = signiture; 102 | } 103 | 104 | /** 105 | * This matches a block and tries to find everything up to the 106 | * end character which is a regex to determine if it's the right 107 | * character 108 | * 109 | * @param {Position} initial 110 | * @param {RegExp} endChar 111 | * @returns {string} 112 | */ 113 | public getBlock(initial:Position, endChar:RegExp):string 114 | { 115 | let line = initial.line+1; 116 | let part = this.editor.document.lineAt(line).text; 117 | 118 | let initialCharacter = part.search(/[^\s]/); 119 | if (initialCharacter === -1) { 120 | return ""; 121 | } 122 | 123 | let start = new Position(initial.line+1, initialCharacter); 124 | while (!endChar.test(part)) { 125 | line++; 126 | part = this.editor.document.lineAt(line).text; 127 | } 128 | let end = new Position(line, part.search(endChar)); 129 | let block = new Range(start, end); 130 | 131 | return this.editor.document.getText(block); 132 | } 133 | 134 | /** 135 | * Parse a nested block of code 136 | * 137 | * @param {string} context 138 | * @param {string} [opening] 139 | * @param {string} [closing] 140 | * @returns {string} 141 | */ 142 | public getEnclosed(context:string, opening:string, closing:string):string 143 | { 144 | let opened = 0; 145 | let contextArray:Array = context.split(""); 146 | let endPos = 0; 147 | for (let index = 0; index < contextArray.length; index++) { 148 | let char = contextArray[index]; 149 | if (char == closing && opened == 0) { 150 | endPos = index; 151 | break; 152 | } else if (char == closing) { 153 | opened--; 154 | } else if (char == opening) { 155 | opened++; 156 | } 157 | endPos = index; 158 | } 159 | 160 | return context.substr(0, endPos); 161 | } 162 | 163 | /** 164 | * Split a string excluding any braces 165 | * 166 | * Currently doesn't handle string 167 | * 168 | * @param {string} context 169 | * @param {string} [divider=","] 170 | * @returns {Array} 171 | * @memberof Block 172 | */ 173 | public getSplitWithoutEnclosed(context: string, divider: string = ","):Array 174 | { 175 | let result:Array = new Array(); 176 | let contextArray:Array = context.split(""); 177 | 178 | let openers:Array = ['{', '(', '[']; 179 | let closers:Array = ['}', ')', ']']; 180 | let opened = 0; 181 | let startPos = 0; 182 | let endPos = 0; 183 | 184 | for (let index = 0; index < contextArray.length; index++) { 185 | let char = contextArray[index]; 186 | if (char === divider && index === contextArray.length - 1) { 187 | break; 188 | } else if (char === divider && opened === 0) { 189 | endPos = index; 190 | result.push(context.substr(startPos, endPos - startPos)); 191 | startPos = index + 1; 192 | continue; 193 | } else if (openers.indexOf(char) >= 0) { 194 | opened++; 195 | } else if (closers.indexOf(char) >= 0) { 196 | opened--; 197 | } 198 | endPos = index; 199 | } 200 | 201 | let arg:string = context.substr(startPos, endPos - startPos + 1); 202 | 203 | if (!arg.match(/^\s*$/)) { 204 | result.push(context.substr(startPos, endPos - startPos + 1)); 205 | } 206 | 207 | return result; 208 | } 209 | 210 | /** 211 | * Get the header for the class 212 | * 213 | * @returns {string} 214 | */ 215 | public getClassHead(): string 216 | { 217 | if (this.classHead === undefined) { 218 | let limit = this.editor.document.lineCount < 300 ? this.editor.document.lineCount - 1 : 300; 219 | let text = this.editor.document.getText(new Range(new Position(0, 0), new Position(limit, 0))); 220 | let regex = /\s*(abstract|final)?\s*(class|trait|interface)/gm; 221 | let match = regex.exec(text); 222 | 223 | if (match === null) { 224 | this.classHead = null; 225 | } else { 226 | let end = this.editor.document.positionAt(match.index); 227 | let range = new Range(new Position(0, 0), end); 228 | this.classHead = this.editor.document.getText(range); 229 | } 230 | } 231 | 232 | return this.classHead; 233 | } 234 | 235 | /** 236 | * This is where we parse the code block into a Doc 237 | * object which represents our snippet 238 | * 239 | * @returns {Doc} 240 | */ 241 | public abstract parse():Doc; 242 | } 243 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "php-docblocker" extension will be documented in this file. 4 | 5 | ## [Unreleased] 6 | 7 | ## [2.7.0] - 2022-02-11 8 | - Allow configuration of default type 9 | - Fixed fully qualifies class namespace detection can be incorrect with _ [#214](https://github.com/neild3r/vscode-php-docblocker/issues/214) - Thanks @tianyiw2013 10 | - Supported fully qualifies class namespace use with bracket `use some\namespace\{ ClassA, ClassB, ... }` - Thanks @tianyiw2013 11 | - Supported fully qualifies class namespace use with comma `use some\namespace\ClassA, some\namespace\ClassB, ...` - Thanks @tianyiw2013 12 | - Fix issue with coverage reports not firing 13 | - Supported PHP 8.1 Readonly Properties - Thanks @tianyiw2013 14 | - Supported PHP 8.1 Intersection Types - Thanks @tianyiw2013 15 | 16 | ## [2.6.1] - 2021-10-12 17 | - Fix double start delimeter when vscode setting `editor.autoClosingBrackets` is set to `never` 18 | - Improve class head tolerance when there isn't one or it's too long 19 | - Increased class head limit to 300 lines 20 | 21 | ## [2.6.0] - 2021-09-27 22 | 23 | ### Added 24 | - Aligning of @param and @return - Thanks @MGApcDev 25 | - Added option for param description 26 | - Added option for return description 27 | - Added option for var description 28 | 29 | ## [2.5.0] - 2021-09-22 30 | 31 | ### Added 32 | - Psalm and Phan annotations - Thanks @imliam 33 | 34 | ### Updated 35 | - Fixes issue with multi-line functions that have a trailing comma 36 | 37 | ### Operational 38 | - Add auto-merge to release workflow 39 | - Create release using bot token 40 | - Make tests run on push and PR sychronize only 41 | - Convert `updateChangelog.js` script to use typescript 42 | - Add script to generate the table of tags for the README.md 43 | 44 | ## [2.4.0] - 2021-09-05 45 | - Add support for params which have a trailing comma 46 | - Add support for constructor promotion 47 | - Add GitHub workflow to prepare and publish releases 48 | 49 | ## [2.3.0] - 2021-08-23 50 | - Add support for PHP8 union types in function params - Thanks @tianyiw2013 51 | - Add support for PHP8 union types in function return types - Thanks @tianyiw2013 52 | - Add support for PHP8 union types in class properties - Thanks @tianyiw2013 53 | - Switched build checks system to GitHub actions 54 | 55 | ## [2.2.3] - 2021-08-18 56 | - Remove usage of FS module to fix start up errors in vscode web 57 | 58 | ## [2.2.2] - 2021-08-18 59 | - Improve compatability with vscode web 60 | 61 | ## [2.2.1] - 2021-08-17 62 | - Supported workspace trust - Thanks @tianyiw2013 63 | - Add case insensitive matching - Thanks @tianyiw2013 64 | - Improve detection of types from certain default values - Thanks @tianyiw2013 65 | 66 | ## [2.2.0] - 2021-08-11 67 | - Use esbuild to improve startup performance 68 | - Add support for PHP 7.4 typed properties 69 | - Fix issue where if you have a comma in a default value it incorrectly splits the arguments 70 | 71 | ## [2.1.0] - 2019-11-19 72 | - Add should to list of bool return type functions - Thanks @ImClarky 73 | - Fix issue where functions named with a bool return prefix were incorrectly given bool return type - Thanks @ImClarky 74 | - Fix issue with snake case class names in PHP7 return types - Thanks @rmccue 75 | 76 | ## [2.0.1] - 2019-07-08 77 | - Fix issue in templating system where custom order was not being honored 78 | 79 | ## [2.0.0] - 2019-06-30 80 | - Add templating system to handle how DocBlocks are created. While not strictly a breaking change it is a major update so this is why it has been released as 2.0.0 81 | 82 | ## [1.9.0] - 2019-06-30 83 | - Add PHPUnit annotations to docblock completions - Thanks @goodjack 84 | 85 | ## [1.8.0] - 2019-03-13 86 | - Add a command to trigger DocBlocking 87 | - Update configuration description around short names - Thanks @dave470003 88 | 89 | ## [1.7.0] - 2018-12-23 90 | - Add description to snippet to avoid confusion 91 | - Add option to turn of void return type when the return could not be detected 92 | - Remove tabstop on variable names generating DocBlocks - Thanks @markjaquith 93 | 94 | ## [1.6.0] - 2018-03-26 95 | - Add configuration option to full qualify class names in param and returns 96 | - Rework internal config value fetching to allow better testing 97 | 98 | ## [1.5.0] - 2018-03-24 99 | - Add author configuration so the Author tag pulls in your name/email 100 | - Add returnGap option which adds a space between your @params and @return tags 101 | - Add list of completion tags to the README.md 102 | 103 | ## [1.4.0] - 2018-03-23 104 | - Allow using vscode variables in your extra tags see list of variables [here](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables) for the docs 105 | 106 | ## [1.3.3] - 2017-12-22 107 | - Fixed typo in readme. - Thanks Dominik Liebler 108 | - Fixed missing canDo return type - Thanks @jens1o 109 | - Fixed transforming short names with PHP7 typehints into their long form when useShortNames is false 110 | 111 | ## [1.3.2] - 2017-10-07 112 | - Improve compatibility with vscode v1.17 by resolving the issue with the snippet end being before the closing */ 113 | 114 | ## [1.3.1] - 2017-10-06 115 | - Fix compatibility issues with vscode v1.17 now language config has been merged into the core 116 | 117 | ## [1.3.0] - 2017-09-29 118 | - Add basic HHVM/Hack support - Thanks @beefsack 119 | - Change @method snippet to not include a $ - Thanks @ADmad 120 | - Fix issue when using a referenced function 121 | 122 | ## [1.2.0] - 2017-05-26 123 | - Add support for PHP7.1 nullable types 124 | - Add config option to allow enforcing short named return types. Thanks @jens1o 125 | 126 | ## [1.1.0] - 2017-05-20 127 | - Add extra snippets. Thanks @jens1o 128 | 129 | ## [1.0.0] - 2017-04-20 130 | - Add support for ...$args 131 | - Support PHP7 return types 132 | - Infer boolean return type for methods that start is or has 133 | - Add more @ tags from the PHPDoc documentation 134 | - Fix issue with function typehints using namespaces 135 | 136 | ## [0.4.2] - 2017-03-31 137 | - Fix travis not getting the right status code back when tests fail 138 | - Fix issue where completion @ tags weren't matching properly 139 | 140 | ## [0.4.1] - 2017-03-29 141 | - Fix for issue causing built in php intellisense to be broken 142 | - Add code coverage to CI and more unit tests to get full code coverage 143 | 144 | ## [0.4.0] - 2017-03-17 145 | - Add Inferring of types in properties 146 | - Add Inferring of types using function param defaults 147 | - Fix issue where function parameters passed by reference fail to parse 148 | 149 | ## [0.3.3] - 2017-03-16 150 | - Fix matching of multiline class properties 151 | - Fix the falling back to a simple block 152 | - Fix issue with multiline arguments with type hints 153 | - Fix `@param` completion spitting out `@var` 154 | - Fix Interface class not triggering 155 | - Refactor unit tests and add more checks and a more comprehensive realworld test 156 | 157 | ## [0.3.2] - 2017-03-15 158 | - Fixed overflow of function capture meaning it was being applied to properties 159 | 160 | ## [0.3.1] - 2017-03-15 161 | - Fixed regression in functions without params not triggering 162 | 163 | ## [0.3.0] - 2017-03-15 164 | - Added unit tests for function signatures and continuous integration 165 | - Added `@package` to completions 166 | - Fixed issue where functions weren't detected in abstract/interface methods 167 | - Fixed support for multiline function signatures 168 | 169 | ## [0.2.0] - 2017-03-13 170 | - Refactored for potential unit testing 171 | - Switched to use a completion snippet for the main docblock 172 | - Fix bug with function that has no params 173 | 174 | ## [0.1.1] - 2017-03-12 175 | - Updated readme and display name 176 | 177 | ## 0.1.0 - 2017-03-12 178 | - Initial release 179 | 180 | [Unreleased]: https://github.com/neild3r/vscode-php-docblocker/compare/v2.7.0...HEAD 181 | [2.7.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v2.6.1...v2.7.0 182 | [2.6.1]: https://github.com/neild3r/vscode-php-docblocker/compare/v2.6.0...v2.6.1 183 | [2.6.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v2.5.0...v2.6.0 184 | [2.5.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v2.4.0...v2.5.0 185 | [2.4.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v2.3.0...v2.4.0 186 | [2.3.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v2.2.3...v2.3.0 187 | [2.2.3]: https://github.com/neild3r/vscode-php-docblocker/compare/v2.2.2...v2.2.3 188 | [2.2.2]: https://github.com/neild3r/vscode-php-docblocker/compare/v2.2.1...v2.2.2 189 | [2.2.1]: https://github.com/neild3r/vscode-php-docblocker/compare/v2.2.0...v2.2.1 190 | [2.2.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v2.1.0...v2.2.0 191 | [2.1.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v2.0.1...v2.1.0 192 | [2.0.1]: https://github.com/neild3r/vscode-php-docblocker/compare/v2.0.0...v2.0.1 193 | [2.0.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v1.9.0...v2.0.0 194 | [1.9.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v1.8.0...v1.9.0 195 | [1.8.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v1.7.0...v1.8.0 196 | [1.7.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v1.6.0...v1.7.0 197 | [1.6.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v1.5.0...v1.6.0 198 | [1.5.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v1.4.0...v1.5.0 199 | [1.4.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v1.3.3...v1.4.0 200 | [1.3.3]: https://github.com/neild3r/vscode-php-docblocker/compare/v1.3.2...v1.3.3 201 | [1.3.2]: https://github.com/neild3r/vscode-php-docblocker/compare/v1.3.1...v1.3.2 202 | [1.3.1]: https://github.com/neild3r/vscode-php-docblocker/compare/v1.3.0...v1.3.1 203 | [1.3.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v1.2.0...v1.3.0 204 | [1.2.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v1.1.0...v1.2.0 205 | [1.1.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v1.0.0...v1.1.0 206 | [1.0.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v0.4.2...v1.0.0 207 | [0.4.2]: https://github.com/neild3r/vscode-php-docblocker/compare/v0.4.1...v0.4.2 208 | [0.4.1]: https://github.com/neild3r/vscode-php-docblocker/compare/v0.4.0...v0.4.1 209 | [0.4.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v0.3.3...v0.4.0 210 | [0.3.3]: https://github.com/neild3r/vscode-php-docblocker/compare/v0.3.2...v0.3.3 211 | [0.3.2]: https://github.com/neild3r/vscode-php-docblocker/compare/v0.3.1...v0.3.2 212 | [0.3.1]: https://github.com/neild3r/vscode-php-docblocker/compare/v0.3.0...v0.3.1 213 | [0.3.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v0.2.0...v0.3.0 214 | [0.2.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v0.1.1...v0.2.0 215 | [0.2.0]: https://github.com/neild3r/vscode-php-docblocker/compare/v0.1.1...v0.2.0 216 | [0.1.1]: https://github.com/neild3r/vscode-php-docblocker/compare/v0.1.0...v0.1.1 217 | -------------------------------------------------------------------------------- /src/tags.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple interface for a tag 3 | */ 4 | export interface Tag { 5 | tag: string, 6 | snippet: string 7 | } 8 | 9 | /** 10 | * Simple class to contain all the tags 11 | */ 12 | export default class Tags 13 | { 14 | /** 15 | * List of tags 16 | * 17 | * @type {Tag[]} 18 | */ 19 | protected tagList: Tag[] = [ 20 | { 21 | tag: '@api', 22 | snippet: '@api' 23 | }, 24 | { 25 | tag: '@abstract', 26 | snippet: '@abstract' 27 | }, 28 | { 29 | tag: '@after', 30 | snippet: '@after' 31 | }, 32 | { 33 | tag: '@afterClass', 34 | snippet: '@afterClass' 35 | }, 36 | { 37 | tag: '@author', 38 | snippet: '@author ${1:{{name}}} <${2:{{email}}}>' 39 | }, 40 | { 41 | tag: '@backupGlobals', 42 | snippet: '@backupGlobals ${1:switch}' 43 | }, 44 | { 45 | tag: '@backupStaticAttributes', 46 | snippet: '@backupStaticAttributes ${1:switch}' 47 | }, 48 | { 49 | tag: '@before', 50 | snippet: '@before' 51 | }, 52 | { 53 | tag: '@beforeClass', 54 | snippet: '@beforeClass' 55 | }, 56 | { 57 | tag: '@category', 58 | snippet: '@category ${1:description}' 59 | }, 60 | { 61 | tag: '@codeCoverageIgnore', 62 | snippet: '@codeCoverageIgnore' 63 | }, 64 | { 65 | tag: '@codeCoverageIgnoreEnd', 66 | snippet: '@codeCoverageIgnoreEnd' 67 | }, 68 | { 69 | tag: '@codeCoverageIgnoreStart', 70 | snippet: '@codeCoverageIgnoreStart' 71 | }, 72 | { 73 | tag: '@copyright', 74 | snippet: '@copyright ${1:' + (new Date()).getFullYear() + '} ${2:Name}' 75 | }, 76 | { 77 | tag: '@covers', 78 | snippet: '@covers ${1:fqcn}' 79 | }, 80 | { 81 | tag: '@coversDefaultClass', 82 | snippet: '@coversDefaultClass ${1:fqcn}' 83 | }, 84 | { 85 | tag: '@coversNothing', 86 | snippet: '@coversNothing' 87 | }, 88 | { 89 | tag: '@dataProvider', 90 | snippet: '@dataProvider ${1:methodName}' 91 | }, 92 | { 93 | tag: '@depends', 94 | snippet: '@depends ${1:methodName}' 95 | }, 96 | { 97 | tag: '@deprecated', 98 | snippet: '@deprecated ${1:version}' 99 | }, 100 | { 101 | tag: '@doesNotPerformAssertions', 102 | snippet: '@doesNotPerformAssertions' 103 | }, 104 | { 105 | tag: '@example', 106 | snippet: '@example ${1:location} ${2:description}' 107 | }, 108 | { 109 | tag: '@filesource', 110 | snippet: '@filesource' 111 | }, 112 | { 113 | tag: '@final', 114 | snippet: '@final' 115 | }, 116 | { 117 | tag: '@group', 118 | snippet: '@group ${1:group}' 119 | }, 120 | { 121 | tag: '@global', 122 | snippet: '@global' 123 | }, 124 | { 125 | tag: '@ignore', 126 | snippet: '@ignore ${1:description}' 127 | }, 128 | { 129 | tag: '@inheritDoc', 130 | snippet: '@inheritDoc' 131 | }, 132 | { 133 | tag: '@internal', 134 | snippet: '@internal ${1:description}' 135 | }, 136 | { 137 | tag: '@large', 138 | snippet: '@large' 139 | }, 140 | { 141 | tag: '@license', 142 | snippet: '@license ${1:MIT}' 143 | }, 144 | { 145 | tag: '@link', 146 | snippet: '@link ${1:http://url.com}' 147 | }, 148 | { 149 | tag: '@medium', 150 | snippet: '@medium' 151 | }, 152 | { 153 | tag: '@method', 154 | snippet: '@method ${1:mixed} ${2:methodName()}' 155 | }, 156 | { 157 | tag: '@mixin', 158 | snippet: '@mixin ${1:\\MyClass}' 159 | }, 160 | { 161 | tag: '@package', 162 | snippet: '@package ${1:category}' 163 | }, 164 | { 165 | tag: '@param', 166 | snippet: '@param ${1:mixed} \$${2:name}' 167 | }, 168 | { 169 | tag: '@preserveGlobalState', 170 | snippet: '@preserveGlobalState ${1:switch}' 171 | }, 172 | { 173 | tag: '@property', 174 | snippet: '@property ${1:mixed} \$${2:name}' 175 | }, 176 | { 177 | tag: '@property-read', 178 | snippet: '@property-read ${1:mixed} \$${2:name}' 179 | }, 180 | { 181 | tag: '@property-write', 182 | snippet: '@property-write ${1:mixed} \$${2:name}' 183 | }, 184 | { 185 | tag: '@requires', 186 | snippet: '@requires ${1:mixed}' 187 | }, 188 | { 189 | tag: '@return', 190 | snippet: '@return ${1:mixed}' 191 | }, 192 | { 193 | tag: '@runInSeparateProcess', 194 | snippet: '@runInSeparateProcess' 195 | }, 196 | { 197 | tag: '@runTestsInSeparateProcesses', 198 | snippet: '@runTestsInSeparateProcesses' 199 | }, 200 | { 201 | tag: '@see', 202 | snippet: '@see ${1:http://url.com}' 203 | }, 204 | { 205 | tag: '@since', 206 | snippet: '@since ${1:1.0.0}' 207 | }, 208 | { 209 | tag: '@small', 210 | snippet: '@small' 211 | }, 212 | { 213 | tag: '@source', 214 | snippet: '@source ${1:location} ${2:description}' 215 | }, 216 | { 217 | tag: '@static', 218 | snippet: '@static' 219 | }, 220 | { 221 | tag: '@subpackage', 222 | snippet: '@subpackage ${1:category}' 223 | }, 224 | { 225 | tag: '@test', 226 | snippet: '@test' 227 | }, 228 | { 229 | tag: '@testdox', 230 | snippet: '@testdox ${1:description}' 231 | }, 232 | { 233 | tag: '@testWith', 234 | snippet: '@testWith ${1:elements}' 235 | }, 236 | { 237 | tag: '@throws', 238 | snippet: '@throws ${1:Exception}' 239 | }, 240 | { 241 | tag: '@ticket', 242 | snippet: '@ticket ${1:ticket}' 243 | }, 244 | { 245 | tag: '@todo', 246 | snippet: '@todo ${1:Something}' 247 | }, 248 | { 249 | tag: '@uses', 250 | snippet: '@uses ${1:MyClass::function} ${2:Name}' 251 | }, 252 | { 253 | tag: '@var', 254 | snippet: '@var ${1:mixed}' 255 | }, 256 | { 257 | tag: '@version', 258 | snippet: '@version ${1:1.0.0}' 259 | }, 260 | { 261 | tag: '@psalm-var (Psalm)', 262 | snippet: '@psalm-var ${1:mixed}' 263 | }, 264 | { 265 | tag: '@psalm-param (Psalm)', 266 | snippet: '@psalm-param ${1:mixed} \$${2:name}' 267 | }, 268 | { 269 | tag: '@psalm-return (Psalm)', 270 | snippet: '@psalm-return ${1:mixed}' 271 | }, 272 | { 273 | tag: '@psalm-suppress (Psalm)', 274 | snippet: '@psalm-suppress ${1:IssueName}' 275 | }, 276 | { 277 | tag: '@psalm-assert (Psalm)', 278 | snippet: '@psalm-assert ${1:[assertion]} \$${2:var}' 279 | }, 280 | { 281 | tag: '@psalm-assert-if-true (Psalm)', 282 | snippet: '@psalm-assert-if-true ${1:[assertion]} \$${2:var}' 283 | }, 284 | { 285 | tag: '@psalm-assert-if-false (Psalm)', 286 | snippet: '@psalm-assert-if-false ${1:[assertion]} \$${2:var}' 287 | }, 288 | { 289 | tag: '@psalm-ignore-nullable-return (Psalm)', 290 | snippet: '@psalm-ignore-nullable-return' 291 | }, 292 | { 293 | tag: '@psalm-ignore-falsable-return (Psalm)', 294 | snippet: '@psalm-ignore-falsable-return' 295 | }, 296 | { 297 | tag: '@psalm-seal-properties (Psalm)', 298 | snippet: '@psalm-seal-properties' 299 | }, 300 | { 301 | tag: '@psalm-internal (Psalm)', 302 | snippet: '@psalm-internal ${1:Namespace}' 303 | }, 304 | { 305 | tag: '@psalm-readonly (Psalm)', 306 | snippet: '@psalm-readonly' 307 | }, 308 | { 309 | tag: '@psalm-mutation-free (Psalm)', 310 | snippet: '@psalm-mutation-free' 311 | }, 312 | { 313 | tag: '@psalm-external-mutation-free (Psalm)', 314 | snippet: '@psalm-external-mutation-free' 315 | }, 316 | { 317 | tag: '@psalm-immutable (Psalm)', 318 | snippet: '@psalm-immutable' 319 | }, 320 | { 321 | tag: '@psalm-pure (Psalm)', 322 | snippet: '@psalm-pure' 323 | }, 324 | { 325 | tag: '@phan-suppress (Phan)', 326 | snippet: '@phan-suppress ${1:IssueName}' 327 | }, 328 | { 329 | tag: '@suppress (Phan)', 330 | snippet: '@suppress ${1:IssueName}' 331 | }, 332 | { 333 | tag: '@phan-suppress-current-line (Phan)', 334 | snippet: '@phan-suppress-current-line ${1:IssueName, IssueName}' 335 | }, 336 | { 337 | tag: '@phan-suppress-next-line (Phan)', 338 | snippet: '@phan-suppress-next-line ${1:IssueName, IssueName}' 339 | }, 340 | { 341 | tag: '@phan-file-suppress (Phan)', 342 | snippet: '@phan-file-suppress ${1:IssueName}' 343 | }, 344 | { 345 | tag: '@override (Phan)', 346 | snippet: '@override' 347 | }, 348 | { 349 | tag: '@inherits (Phan)', 350 | snippet: '@inherits' 351 | }, 352 | { 353 | tag: '@phan-assert (Phan)', 354 | snippet: '@phan-assert ${1:[assertion]} \$${2:var}' 355 | }, 356 | { 357 | tag: '@phan-assert-true-condition (Phan)', 358 | snippet: '@phan-assert-true-condition ${1:[assertion]} \$${2:var}' 359 | }, 360 | { 361 | tag: '@phan-assert-false-condition (Phan)', 362 | snippet: '@phan-assert-false-condition ${1:[assertion]} \$${2:var}' 363 | }, 364 | { 365 | tag: '@phan-closure-scope (Phan)', 366 | snippet: '@phan-closure-scope' 367 | }, 368 | { 369 | tag: '@phan-read-only (Phan)', 370 | snippet: '@phan-read-only' 371 | }, 372 | { 373 | tag: '@phan-write-only (Phan)', 374 | snippet: '@phan-write-only' 375 | }, 376 | { 377 | tag: '@phan-pure (Phan)', 378 | snippet: '@phan-pure' 379 | }, 380 | { 381 | tag: '@phan-phan-output-reference (Phan)', 382 | snippet: '@param ${1:mixed} \$${2:name} @phan-phan-output-reference' 383 | }, 384 | { 385 | tag: '@phan-phan-ignore-reference (Phan)', 386 | snippet: '@param ${1:mixed} \$${2:name} @phan-phan-ignore-reference' 387 | }, 388 | { 389 | tag: '@phan-var (Phan)', 390 | snippet: '@phan-var ${1:mixed}' 391 | }, 392 | { 393 | tag: '@phan-param (Phan)', 394 | snippet: '@phan-param ${1:mixed} \$${2:name}' 395 | }, 396 | { 397 | tag: '@phan-return (Phan)', 398 | snippet: '@phan-return ${1:mixed}' 399 | }, 400 | { 401 | tag: '@phan-return (Phan)', 402 | snippet: '@phan-return ${1:mixed}' 403 | }, 404 | { 405 | tag: '@phan-method (Phan)', 406 | snippet: '@phan-method ${1:mixed} ${2:methodName()}' 407 | }, 408 | { 409 | tag: '@template (Phan)', 410 | snippet: '@template' 411 | } 412 | ]; 413 | 414 | /** 415 | * Get the tag list for completions 416 | * 417 | * @returns {Tag[]} 418 | */ 419 | get list(): Tag[] 420 | { 421 | return this.tagList; 422 | } 423 | } -------------------------------------------------------------------------------- /src/doc.ts: -------------------------------------------------------------------------------- 1 | import {workspace, SnippetString, WorkspaceConfiguration} from 'vscode'; 2 | import Config from './util/config'; 3 | 4 | interface MaxParamLength { 5 | type: number, 6 | name: number 7 | } 8 | 9 | interface AlignmentSpaces { 10 | prepend: string, 11 | append: string 12 | } 13 | 14 | /** 15 | * Represents a comment block. 16 | * 17 | * This class collects data about the snippet then builds 18 | * it with the appropriate tags 19 | */ 20 | export class Doc 21 | { 22 | /** 23 | * List of param tags 24 | * 25 | * @type {Array} 26 | */ 27 | public params:Array = []; 28 | 29 | /** 30 | * Return tag 31 | * 32 | * @type {string} 33 | */ 34 | public return:string; 35 | 36 | /** 37 | * Var tag 38 | * 39 | * @type {string} 40 | */ 41 | public var:string; 42 | 43 | /** 44 | * The message portion of the block 45 | * 46 | * @type {string} 47 | */ 48 | public message:string; 49 | 50 | /** 51 | * Define the template for the documentor 52 | * 53 | * @type {Object} 54 | */ 55 | protected _template:Object; 56 | 57 | /** 58 | * Creates an instance of Doc. 59 | * 60 | * @param {string} [message=''] 61 | */ 62 | public constructor(message:string = '') 63 | { 64 | this.message = message; 65 | } 66 | 67 | /** 68 | * Define indent character for param alignment. 69 | * 70 | * @type {string} 71 | */ 72 | public indentCharacter:string = ' '; 73 | 74 | /** 75 | * Set class properties from a standard object 76 | * 77 | * @param {*} input 78 | */ 79 | public fromObject(input:any):void 80 | { 81 | if (input.return !== undefined) { 82 | this.return = input.return; 83 | } 84 | if (input.var !== undefined) { 85 | this.var = input.var; 86 | } 87 | if (input.message !== undefined) { 88 | this.message = input.message; 89 | } 90 | if (input.params !== undefined && Array.isArray(input.params)) { 91 | input.params.forEach(param => { 92 | this.params.push(new Param(param.type, param.name)); 93 | }); 94 | } 95 | } 96 | 97 | /** 98 | * Build all the set values into a SnippetString ready for use 99 | * 100 | * @param {boolean} [isEmpty=false] 101 | * @returns {SnippetString} 102 | */ 103 | public build(isEmpty:boolean = false):SnippetString 104 | { 105 | let extra = Config.instance.get('extra'); 106 | let gap = Config.instance.get('gap'); 107 | let returnGap = Config.instance.get('returnGap'); 108 | let alignParams = Config.instance.get('alignParams'); 109 | // Align return is false if align params is not active. 110 | let alignReturn = alignParams ? Config.instance.get('alignReturn') : false; 111 | 112 | let returnString = ""; 113 | let varString = ""; 114 | let paramString = ""; 115 | let extraString = ""; 116 | let messageString = ""; 117 | 118 | if (isEmpty) { 119 | gap = true; 120 | extra = []; 121 | } 122 | 123 | messageString = "\${###" + (this.message != "" ? ':' : '') + this.message + "}"; 124 | 125 | // Loop through params and find max length of type and name. 126 | let maxParamLength = this.getMaxParamLength(this.params, this.return); 127 | 128 | if (this.params.length) { 129 | paramString = ""; 130 | this.params.forEach(param => { 131 | if (paramString != "") { 132 | paramString += "\n"; 133 | } 134 | 135 | let paramType = param.type; 136 | let paramName = param.name.replace('$', '\\$'); 137 | 138 | let alignmentSpaces = this.getParamAlignmentSpaces(maxParamLength, paramName, paramType); 139 | 140 | paramString += 141 | "@param " + 142 | // Add extra space to align '@param' and '@return'. 143 | (alignReturn ? this.indentCharacter : '') + 144 | "\${###:"+paramType+"} " + 145 | alignmentSpaces.prepend + paramName + alignmentSpaces.append; 146 | 147 | let description = Config.instance.get('paramDescription'); 148 | if (description === true) { 149 | paramString += "\${###}" 150 | } else if (typeof description == 'string') { 151 | paramString += "\${###:" + description + "}" 152 | } 153 | }); 154 | } 155 | 156 | if (this.var) { 157 | varString = "@var \${###:" +this.var + "}"; 158 | 159 | let varDescription = Config.instance.get('varDescription'); 160 | if (varDescription === true) { 161 | varString += " \${###}" 162 | } else if (typeof varDescription == 'string') { 163 | varString += " \${###:" + varDescription + "}" 164 | } 165 | } 166 | 167 | if (this.return && (this.return != 'void' || Config.instance.get('returnVoid'))) { 168 | let alignmentSpaces = this.getReturnAlignmentSpaces(maxParamLength); 169 | returnString = "@return \${###:" +this.return + "}" + alignmentSpaces.append; 170 | 171 | let description = Config.instance.get('returnDescription'); 172 | if (description === true) { 173 | returnString += "\${###}" 174 | } else if (typeof description == 'string') { 175 | returnString += "\${###:" + description + "}" 176 | } 177 | } 178 | 179 | if (Array.isArray(extra) && extra.length > 0) { 180 | extraString = extra.join("\n"); 181 | } 182 | 183 | 184 | let templateArray = []; 185 | for (let key in this.template) { 186 | let propConfig = this.template[key]; 187 | let propString:string; 188 | if (key == 'message' && messageString) { 189 | propString = messageString; 190 | if (gap) { 191 | propConfig.gapAfter = true; 192 | } 193 | } else if (key == 'var' && varString) { 194 | propString = varString; 195 | } else if (key == 'return' && returnString) { 196 | propString = returnString; 197 | if (returnGap) { 198 | propConfig.gapBefore = true; 199 | } 200 | } else if (key == 'param' && paramString) { 201 | propString = paramString; 202 | } else if (key == 'extra' && extraString) { 203 | propString = extraString; 204 | } else if (propConfig.content !== undefined) { 205 | propString = propConfig.content; 206 | } 207 | 208 | if (propString && propConfig.gapBefore && templateArray[templateArray.length - 1] != "") { 209 | templateArray.push(""); 210 | } 211 | 212 | if (propString) { 213 | templateArray.push(propString); 214 | } 215 | 216 | if (propString && propConfig.gapAfter) { 217 | templateArray.push(""); 218 | } 219 | } 220 | 221 | if (templateArray[templateArray.length - 1] == "") { 222 | templateArray.pop(); 223 | } 224 | 225 | let templateString:string = templateArray.join("\n"); 226 | let stop = 0; 227 | templateString = templateString.replace(/###/gm, function():string { 228 | stop++; 229 | return stop + ""; 230 | }); 231 | 232 | templateString = templateString.replace(/^$/gm, " *"); 233 | templateString = templateString.replace(/^(?!(\s\*|\/\*))/gm, " * $1"); 234 | 235 | if (Config.instance.get('autoClosingBrackets') == "never") { 236 | templateString = "\n" + templateString + "\n */"; 237 | } else { 238 | templateString = "/**\n" + templateString + "\n */"; 239 | } 240 | 241 | let snippet = new SnippetString(templateString); 242 | 243 | return snippet; 244 | } 245 | 246 | /** 247 | * Set the template for rendering 248 | * 249 | * @param {Object} template 250 | */ 251 | public set template(template:Object) 252 | { 253 | this._template = template; 254 | } 255 | 256 | /** 257 | * Get the template 258 | * 259 | * @type {Object} 260 | */ 261 | public get template():Object 262 | { 263 | if (this._template == null) { 264 | return { 265 | message: {}, 266 | var: {}, 267 | param: {}, 268 | return: {}, 269 | extra: {} 270 | } 271 | } 272 | 273 | return this._template; 274 | } 275 | 276 | /** 277 | * Get the max param type length and param name length. 278 | * 279 | * @param {Array} params 280 | * @param {string} returnStatement 281 | * @returns {MaxParamLength} 282 | */ 283 | private getMaxParamLength(params: Array, returnStatement: string): MaxParamLength 284 | { 285 | let alignParams = Config.instance.get('alignParams'); 286 | let alignReturn = alignParams ? Config.instance.get('alignReturn') : false; 287 | 288 | 289 | let maxParamTypeLength = 0; 290 | let maxParamNameLength = 0; 291 | if (params.length && alignParams) { 292 | params.forEach(param => { 293 | let paramType = param.type; 294 | if (paramType.length > maxParamTypeLength) { 295 | maxParamTypeLength = paramType.length; 296 | } 297 | let paramName = param.name.replace('$', '\\$') 298 | if (paramName.length > maxParamNameLength) { 299 | maxParamNameLength = paramName.length; 300 | } 301 | }); 302 | } 303 | // If align return is active, check if it has a longer type. 304 | if (returnStatement && (returnStatement != 'void' || Config.instance.get('returnVoid')) && alignReturn) { 305 | if (returnStatement.length > maxParamTypeLength) { 306 | maxParamTypeLength = returnStatement.length; 307 | } 308 | } 309 | 310 | return { 311 | type: maxParamTypeLength, 312 | name: maxParamNameLength 313 | } 314 | } 315 | 316 | /** 317 | * Get extra spaces for alignment of params. 318 | * 319 | * @param {MaxParamLength} maxParamLength 320 | * @param {string} paramName 321 | * @param {string} paramType 322 | * @returns {AlignmentSpaces} 323 | */ 324 | private getParamAlignmentSpaces(maxParamLength: MaxParamLength, paramName: string, paramType: string): AlignmentSpaces 325 | { 326 | let alignParams = Config.instance.get('alignParams'); 327 | let paramDescription = Config.instance.get('paramDescription'); 328 | 329 | let prependSpace = ''; 330 | let appendSpace = ''; 331 | if (alignParams) { 332 | // Append additional spaces on param type and param name. 333 | prependSpace = Array(maxParamLength.type - paramType.length).fill(this.indentCharacter).join(''); 334 | // Add 1 to array size, so there is already a space appended for typing comments. 335 | appendSpace = Array(1 + maxParamLength.name - paramName.length).fill(this.indentCharacter).join(''); 336 | } 337 | 338 | return { 339 | append: paramDescription ? (alignParams ? appendSpace : this.indentCharacter) : '', 340 | prepend: prependSpace 341 | }; 342 | } 343 | 344 | /** 345 | * Get extra spaces for alignment of return statement. 346 | * 347 | * @param {MaxParamLength} maxParamLength 348 | * @returns {AlignmentSpaces} 349 | */ 350 | private getReturnAlignmentSpaces(maxParamLength: MaxParamLength): AlignmentSpaces 351 | { 352 | let alignParams = Config.instance.get('alignParams'); 353 | let alignReturn = alignParams ? Config.instance.get('alignReturn') : false; 354 | let returnDescription = Config.instance.get('returnDescription'); 355 | 356 | let appendSpace = ''; 357 | if (alignReturn) { 358 | appendSpace = 359 | Array(1 + maxParamLength.type - this.return.length).fill(this.indentCharacter).join('') + 360 | Array(maxParamLength.name).fill(this.indentCharacter).join(''); 361 | } 362 | 363 | return { 364 | append: returnDescription ? (alignReturn ? appendSpace : this.indentCharacter) : '', 365 | prepend: '' 366 | }; 367 | } 368 | 369 | } 370 | 371 | /** 372 | * A simple paramter object 373 | */ 374 | export class Param 375 | { 376 | /** 377 | * The type of the parameter 378 | * 379 | * @type {string} 380 | */ 381 | public type:string; 382 | 383 | /** 384 | * The parameter name 385 | * 386 | * @type {string} 387 | */ 388 | public name:string; 389 | 390 | /** 391 | * Creates an instance of Param. 392 | * 393 | * @param {string} type 394 | * @param {string} name 395 | */ 396 | public constructor(type:string, name:string) 397 | { 398 | this.type = type; 399 | this.name = name; 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP DocBlocker 2 | 3 | [![Latest Release](https://vsmarketplacebadge.apphb.com/version-short/neilbrayfield.php-docblocker.svg)](https://marketplace.visualstudio.com/items?itemName=neilbrayfield.php-docblocker) [![Installs](https://vsmarketplacebadge.apphb.com/installs/neilbrayfield.php-docblocker.svg)](https://marketplace.visualstudio.com/items?itemName=neilbrayfield.php-docblocker) [![Rating](https://vsmarketplacebadge.apphb.com/rating-short/neilbrayfield.php-docblocker.svg)](https://marketplace.visualstudio.com/items?itemName=neilbrayfield.php-docblocker) [![Test Suite](https://github.com/neild3r/vscode-php-docblocker/actions/workflows/test.yml/badge.svg)](https://github.com/neild3r/vscode-php-docblocker/actions/workflows/test.yml) [![Coverage status](https://coveralls.io/repos/github/neild3r/vscode-php-docblocker/badge.svg)](https://coveralls.io/github/neild3r/vscode-php-docblocker) 4 | 5 | Basic PHP DocBlocking extension. 6 | 7 | We now have a set of unit tests and some full coverage on the parsing of signatures as well as continuous integration. This should ensure the extension remains stable as development progresses. 8 | 9 | ## Features 10 | 11 | * Completion snippet after `/**` above a class, function, class property 12 | * Completion of DocBlock tags such as `@param`, `@return`, `@throws` 13 | * Inferring of param and return types from signatures 14 | * Configuration of template for each type of docblock completion 15 | 16 | ## Requirements 17 | 18 | This extension has no dependencies. 19 | 20 | ## Extension Settings 21 | 22 | This extension contributes the following settings: 23 | 24 | * `php-docblocker.gap`: set to `false` to disable the gap between the description and tags 25 | * `php-docblocker.returnGap`: set to `true` to add a gap between the param and return tags 26 | * `php-docblocker.returnVoid`: set to `false` to turn off the automatic void return type when it can't be determined 27 | * `php-docblocker.defaultType`: default type to use if a type wasn't able to to be determined 28 | * `php-docblocker.extra`: an array of extra tags to add to each DocBlock (These can include tabstops and snippet variables) 29 | * `php-docblocker.useShortNames`: Whether we should use short type names. e.g. bool or boolean 30 | * `php-docblocker.qualifyClassNames`: When adding type hints for class names search namespace use statements and qualify the class 31 | * `php-docblocker.alignParams`: set to `true` to align params vertically and add appropriate spaces after param names 32 | * `php-docblocker.alignReturn`: set to `true` to align return vertically with above params statements, this setting requires align params to also be active 33 | * `php-docblocker.varDescription`: set to `true` to include a description placeholder for `@var` completions. If you specify a string this will be the default placeholder text 34 | * `php-docblocker.paramDescription`: set to `true` to include a description placeholder for `@param` completions. If you specify a string this will be the default placeholder text 35 | * `php-docblocker.returnDescription`: set to `true` to include a description placeholder for `@return` completions. If you specify a string this will be the default placeholder text 36 | * `php-docblocker.author`: An object containing your default author tag settings 37 | * `php-docblocker.functionTemplate`: See below for how to set up docblock templates 38 | * `php-docblocker.propertyTemplate`: See below for how to set up docblock templates 39 | * `php-docblocker.classTemplate`: See below for how to set up docblock templates 40 | 41 | ### Templating 42 | 43 | If you want more control over the order or gap settings on your docblocks or you want different things for properties vs class templates 44 | you can start customising the template configuration objects. These are the config options `functionTemplate`, `propertyTemplate` and 45 | `classTemplate`. 46 | 47 | #### Default set up for function 48 | 49 | The below is the default set up for a function. The order of the keys represents the output order. There are no specific options in each 50 | config option per key to add additional control. 51 | 52 | ```json 53 | { 54 | "message": {}, 55 | "param": {}, 56 | "return": {}, 57 | "extra": {} 58 | } 59 | ``` 60 | 61 | #### Supported template keys 62 | 63 | | Key | Aplies to type | Description | 64 | |-----------------|-----------------|-----------------------------------------------------------------------------------| 65 | | message | All | Space for entering a description of your block | 66 | | extra | All | Adds in your custom tags from the extra config | 67 | | param | Function | Function @param items | 68 | | return | Function | Function @return item | 69 | | var | Property | Property @var item | 70 | | * | All | This is for any key that is unmatched you can use the content option to add a tag | 71 | 72 | #### Supported template config options 73 | 74 | | Option | Aplies to key(s) | Description | 75 | |-----------------|------------------|------------------------------------------------| 76 | | gapBefore | All | Adds a gap before the tag section starts | 77 | | gapAfter | All | Adds a gap after the tag section ends | 78 | | content | * | Adds a gap after the tag section ends | 79 | 80 | #### Configured function template example 81 | 82 | In the example below we have added some gap configuration and removed the return tag for our template as well as 83 | changing the default order. This means we'll never have a @return tag and extra comes before the params. It's also 84 | worth pointing out that the gapAfter in the message is the same as setting the gap config option in the main config 85 | to true. 86 | 87 | ```json 88 | { 89 | "message": { 90 | "gapAfter": true 91 | }, 92 | "extra": {}, 93 | "param": { 94 | "gapBefore": true 95 | }, 96 | } 97 | ``` 98 | 99 | #### Configured function with extra content and placeholders 100 | 101 | The example below won't have a return tag and will add in an author tag with correct placeholders depending on 102 | how many options you have. You can put in placeholders by using `###` in place of the tab stop number and it 103 | will be calculated at generation time. 104 | 105 | ```json 106 | { 107 | "message": { 108 | "gapAfter": true 109 | }, 110 | "param": { 111 | "gapBefore": true 112 | }, 113 | "author": { 114 | "content": "@author ${###:Neil Brayfield} <${###:neil@test.com}>" 115 | } 116 | } 117 | ``` 118 | 119 | ## Supported DocBlock tags 120 | 121 | Please see below for a list of supported tags and their snippets. These tags are available within a DocBlock 122 | and can be triggered by typing @ then another characted (Provided your vscode settings allow). 123 | 124 | | Tag | Snippet | 125 | | ------------------------------------- | -------------------------------------------------------- | 126 | | @api | @api | 127 | | @abstract | @abstract | 128 | | @after | @after | 129 | | @afterClass | @afterClass | 130 | | @author | @author ${1:{{name}}} <${2:{{email}}}> | 131 | | @backupGlobals | @backupGlobals ${1:switch} | 132 | | @backupStaticAttributes | @backupStaticAttributes ${1:switch} | 133 | | @before | @before | 134 | | @beforeClass | @beforeClass | 135 | | @category | @category ${1:description} | 136 | | @codeCoverageIgnore | @codeCoverageIgnore | 137 | | @codeCoverageIgnoreEnd | @codeCoverageIgnoreEnd | 138 | | @codeCoverageIgnoreStart | @codeCoverageIgnoreStart | 139 | | @copyright | @copyright ${1:2021} ${2:Name} | 140 | | @covers | @covers ${1:fqcn} | 141 | | @coversDefaultClass | @coversDefaultClass ${1:fqcn} | 142 | | @coversNothing | @coversNothing | 143 | | @dataProvider | @dataProvider ${1:methodName} | 144 | | @depends | @depends ${1:methodName} | 145 | | @deprecated | @deprecated ${1:version} | 146 | | @doesNotPerformAssertions | @doesNotPerformAssertions | 147 | | @example | @example ${1:location} ${2:description} | 148 | | @filesource | @filesource | 149 | | @final | @final | 150 | | @group | @group ${1:group} | 151 | | @global | @global | 152 | | @ignore | @ignore ${1:description} | 153 | | @inheritDoc | @inheritDoc | 154 | | @internal | @internal ${1:description} | 155 | | @large | @large | 156 | | @license | @license ${1:MIT} | 157 | | @link | @link ${1:http://url.com} | 158 | | @medium | @medium | 159 | | @method | @method ${1:mixed} ${2:methodName()} | 160 | | @mixin | @mixin ${1:\MyClass} | 161 | | @package | @package ${1:category} | 162 | | @param | @param ${1:mixed} $${2:name} | 163 | | @preserveGlobalState | @preserveGlobalState ${1:switch} | 164 | | @property | @property ${1:mixed} $${2:name} | 165 | | @property-read | @property-read ${1:mixed} $${2:name} | 166 | | @property-write | @property-write ${1:mixed} $${2:name} | 167 | | @requires | @requires ${1:mixed} | 168 | | @return | @return ${1:mixed} | 169 | | @runInSeparateProcess | @runInSeparateProcess | 170 | | @runTestsInSeparateProcesses | @runTestsInSeparateProcesses | 171 | | @see | @see ${1:http://url.com} | 172 | | @since | @since ${1:1.0.0} | 173 | | @small | @small | 174 | | @source | @source ${1:location} ${2:description} | 175 | | @static | @static | 176 | | @subpackage | @subpackage ${1:category} | 177 | | @test | @test | 178 | | @testdox | @testdox ${1:description} | 179 | | @testWith | @testWith ${1:elements} | 180 | | @throws | @throws ${1:Exception} | 181 | | @ticket | @ticket ${1:ticket} | 182 | | @todo | @todo ${1:Something} | 183 | | @uses | @uses ${1:MyClass::function} ${2:Name} | 184 | | @var | @var ${1:mixed} | 185 | | @version | @version ${1:1.0.0} | 186 | | @psalm-var (Psalm) | @psalm-var ${1:mixed} | 187 | | @psalm-param (Psalm) | @psalm-param ${1:mixed} $${2:name} | 188 | | @psalm-return (Psalm) | @psalm-return ${1:mixed} | 189 | | @psalm-suppress (Psalm) | @psalm-suppress ${1:IssueName} | 190 | | @psalm-assert (Psalm) | @psalm-assert ${1:[assertion]} $${2:var} | 191 | | @psalm-assert-if-true (Psalm) | @psalm-assert-if-true ${1:[assertion]} $${2:var} | 192 | | @psalm-assert-if-false (Psalm) | @psalm-assert-if-false ${1:[assertion]} $${2:var} | 193 | | @psalm-ignore-nullable-return (Psalm) | @psalm-ignore-nullable-return | 194 | | @psalm-ignore-falsable-return (Psalm) | @psalm-ignore-falsable-return | 195 | | @psalm-seal-properties (Psalm) | @psalm-seal-properties | 196 | | @psalm-internal (Psalm) | @psalm-internal ${1:Namespace} | 197 | | @psalm-readonly (Psalm) | @psalm-readonly | 198 | | @psalm-mutation-free (Psalm) | @psalm-mutation-free | 199 | | @psalm-external-mutation-free (Psalm) | @psalm-external-mutation-free | 200 | | @psalm-immutable (Psalm) | @psalm-immutable | 201 | | @psalm-pure (Psalm) | @psalm-pure | 202 | | @phan-suppress (Phan) | @phan-suppress ${1:IssueName} | 203 | | @suppress (Phan) | @suppress ${1:IssueName} | 204 | | @phan-suppress-current-line (Phan) | @phan-suppress-current-line ${1:IssueName, IssueName} | 205 | | @phan-suppress-next-line (Phan) | @phan-suppress-next-line ${1:IssueName, IssueName} | 206 | | @phan-file-suppress (Phan) | @phan-file-suppress ${1:IssueName} | 207 | | @override (Phan) | @override | 208 | | @inherits (Phan) | @inherits | 209 | | @phan-assert (Phan) | @phan-assert ${1:[assertion]} $${2:var} | 210 | | @phan-assert-true-condition (Phan) | @phan-assert-true-condition ${1:[assertion]} $${2:var} | 211 | | @phan-assert-false-condition (Phan) | @phan-assert-false-condition ${1:[assertion]} $${2:var} | 212 | | @phan-closure-scope (Phan) | @phan-closure-scope | 213 | | @phan-read-only (Phan) | @phan-read-only | 214 | | @phan-write-only (Phan) | @phan-write-only | 215 | | @phan-pure (Phan) | @phan-pure | 216 | | @phan-phan-output-reference (Phan) | @param ${1:mixed} $${2:name} @phan-phan-output-reference | 217 | | @phan-phan-ignore-reference (Phan) | @param ${1:mixed} $${2:name} @phan-phan-ignore-reference | 218 | | @phan-var (Phan) | @phan-var ${1:mixed} | 219 | | @phan-param (Phan) | @phan-param ${1:mixed} $${2:name} | 220 | | @phan-return (Phan) | @phan-return ${1:mixed} | 221 | | @phan-return (Phan) | @phan-return ${1:mixed} | 222 | | @phan-method (Phan) | @phan-method ${1:mixed} ${2:methodName()} | 223 | | @template (Phan) | @template | 224 | 225 | ## Future development 226 | 227 | It probably wouldn't be too much work to expand this to work with multiple languages. If this is something you are interested in, please pop over to github and add your feedback to the issue [neild3r/vscode-php-docblocker#17](https://github.com/neild3r/vscode-php-docblocker/issues/17). 228 | 229 | Please also feel free to suggest new configuration options, I appreciate at this time the extension is mostly set up for my own DocBlock style requirements but more options could be added for other use cases. 230 | -------------------------------------------------------------------------------- /test/fixtures/doc.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Empty", 4 | "config": { 5 | "gap": false 6 | }, 7 | "expected": [ 8 | "/**", 9 | " * ${1}", 10 | " */" 11 | ] 12 | }, 13 | { 14 | "name": "AutoClosingBrackets: never", 15 | "config": { 16 | "gap": false, 17 | "returnVoid": true, 18 | "extra": [], 19 | "autoClosingBrackets": "never" 20 | }, 21 | "input": { 22 | "return": "void" 23 | }, 24 | "expected": [ 25 | "", 26 | " * ${1}", 27 | " * @return ${2:void}", 28 | " */" 29 | ] 30 | }, 31 | { 32 | "name": "Empty Function", 33 | "config": { 34 | "gap": false, 35 | "returnVoid": true, 36 | "extra": [] 37 | }, 38 | "input": { 39 | "return": "void" 40 | }, 41 | "expected": [ 42 | "/**", 43 | " * ${1}", 44 | " * @return ${2:void}", 45 | " */" 46 | ] 47 | }, 48 | { 49 | "name": "Property", 50 | "config": { 51 | "gap": false, 52 | "extra": [] 53 | }, 54 | "input": { 55 | "message": "Undocumented var", 56 | "var": "mixed" 57 | }, 58 | "expected": [ 59 | "/**", 60 | " * ${1:Undocumented var}", 61 | " * @var ${2:mixed}", 62 | " */" 63 | ] 64 | }, 65 | { 66 | "name": "Property with description placeholder", 67 | "config": { 68 | "gap": false, 69 | "extra": [], 70 | "varDescription": true 71 | }, 72 | "input": { 73 | "message": "Undocumented var", 74 | "var": "mixed" 75 | }, 76 | "expected": [ 77 | "/**", 78 | " * ${1:Undocumented var}", 79 | " * @var ${2:mixed} ${3}", 80 | " */" 81 | ] 82 | }, 83 | { 84 | "name": "Property with custom description placeholder", 85 | "config": { 86 | "gap": false, 87 | "extra": [], 88 | "varDescription": "description" 89 | }, 90 | "input": { 91 | "message": "Undocumented var", 92 | "var": "mixed" 93 | }, 94 | "expected": [ 95 | "/**", 96 | " * ${1:Undocumented var}", 97 | " * @var ${2:mixed} ${3:description}", 98 | " */" 99 | ] 100 | }, 101 | { 102 | "name": "Function", 103 | "config": { 104 | "gap": true, 105 | "extra": [] 106 | }, 107 | "input": { 108 | "message": "Undocumented function", 109 | "return": "void", 110 | "params": [ 111 | { 112 | "name": "$name", 113 | "type": "int" 114 | } 115 | ] 116 | }, 117 | "expected": [ 118 | "/**", 119 | " * ${1:Undocumented function}", 120 | " *", 121 | " * @param ${2:int} \\$name", 122 | " * @return ${3:void}", 123 | " */" 124 | ] 125 | }, 126 | { 127 | "name": "Function multiple params", 128 | "config": { 129 | "gap": true, 130 | "extra": [] 131 | }, 132 | "input": { 133 | "message": "Undocumented function", 134 | "return": "void", 135 | "params": [ 136 | { 137 | "name": "$name", 138 | "type": "int" 139 | }, 140 | { 141 | "name": "$message", 142 | "type": "string" 143 | } 144 | ] 145 | }, 146 | "expected": [ 147 | "/**", 148 | " * ${1:Undocumented function}", 149 | " *", 150 | " * @param ${2:int} \\$name", 151 | " * @param ${3:string} \\$message", 152 | " * @return ${4:void}", 153 | " */" 154 | ] 155 | }, 156 | { 157 | "name": "Extra", 158 | "config": { 159 | "gap": true, 160 | "extra": [ 161 | "@author John Smith " 162 | ] 163 | }, 164 | "input": { 165 | "message": "Undocumented function" 166 | }, 167 | "expected": [ 168 | "/**", 169 | " * ${1:Undocumented function}", 170 | " *", 171 | " * @author John Smith ", 172 | " */" 173 | ] 174 | }, 175 | { 176 | "name": "Function params no return", 177 | "config": { 178 | "gap": true, 179 | "extra": [ 180 | "@author John Smith " 181 | ] 182 | }, 183 | "input": { 184 | "message": "Undocumented function", 185 | "params": [ 186 | { 187 | "name": "$name", 188 | "type": "int" 189 | } 190 | ] 191 | }, 192 | "expected": [ 193 | "/**", 194 | " * ${1:Undocumented function}", 195 | " *", 196 | " * @param ${2:int} \\$name", 197 | " * @author John Smith ", 198 | " */" 199 | ] 200 | }, 201 | { 202 | "name": "Function no gap", 203 | "config": { 204 | "gap": false, 205 | "extra": [] 206 | }, 207 | "input": { 208 | "message": "Undocumented function", 209 | "params": [ 210 | { 211 | "name": "$name", 212 | "type": "int" 213 | } 214 | ] 215 | }, 216 | "expected": [ 217 | "/**", 218 | " * ${1:Undocumented function}", 219 | " * @param ${2:int} \\$name", 220 | " */" 221 | ] 222 | }, 223 | { 224 | "name": "Empty extra string", 225 | "config": { 226 | "gap": true, 227 | "extra": [ 228 | "", 229 | "@author John Smith " 230 | ] 231 | }, 232 | "input": { 233 | "message": "Undocumented function", 234 | "params": [ 235 | { 236 | "name": "$name", 237 | "type": "int" 238 | } 239 | ] 240 | }, 241 | "expected": [ 242 | "/**", 243 | " * ${1:Undocumented function}", 244 | " *", 245 | " * @param ${2:int} \\$name", 246 | " *", 247 | " * @author John Smith ", 248 | " */" 249 | ] 250 | }, 251 | { 252 | "name": "Return gap", 253 | "config": { 254 | "gap": true, 255 | "returnGap": true 256 | }, 257 | "input": { 258 | "message": "Undocumented function", 259 | "params": [ 260 | { 261 | "name": "$name", 262 | "type": "int" 263 | } 264 | ], 265 | "return": "void" 266 | }, 267 | "expected": [ 268 | "/**", 269 | " * ${1:Undocumented function}", 270 | " *", 271 | " * @param ${2:int} \\$name", 272 | " *", 273 | " * @return ${3:void}", 274 | " */" 275 | ] 276 | }, 277 | { 278 | "name": "No void", 279 | "config": { 280 | "returnVoid": false 281 | }, 282 | "input": { 283 | "message": "Undocumented function", 284 | "return": "void" 285 | }, 286 | "expected": [ 287 | "/**", 288 | " * ${1:Undocumented function}", 289 | " */" 290 | ] 291 | }, 292 | { 293 | "name": "No void with param & gap", 294 | "config": { 295 | "gap": true, 296 | "returnVoid": false 297 | }, 298 | "input": { 299 | "message": "Undocumented function", 300 | "params": [ 301 | { 302 | "name": "$name", 303 | "type": "int" 304 | } 305 | ], 306 | "return": "void" 307 | }, 308 | "expected": [ 309 | "/**", 310 | " * ${1:Undocumented function}", 311 | " *", 312 | " * @param ${2:int} \\$name", 313 | " */" 314 | ] 315 | }, 316 | { 317 | "name": "Function template", 318 | "config": { 319 | "template": { 320 | "message": { 321 | "gapAfter": true 322 | }, 323 | "author": { 324 | "content": "@author Neil Brayfield " 325 | }, 326 | "param": { 327 | "gapBefore": true 328 | } 329 | } 330 | }, 331 | "input": { 332 | "message": "Undocumented function", 333 | "params": [ 334 | { 335 | "name": "$name", 336 | "type": "int" 337 | } 338 | ], 339 | "return": "void" 340 | }, 341 | "expected": [ 342 | "/**", 343 | " * ${1:Undocumented function}", 344 | " *", 345 | " * @author Neil Brayfield ", 346 | " *", 347 | " * @param ${2:int} \\$name", 348 | " */" 349 | ] 350 | }, 351 | { 352 | "name": "Function template with placeholders", 353 | "config": { 354 | "template": { 355 | "message": { 356 | "gapAfter": true 357 | }, 358 | "author": { 359 | "content": "@author ${###:Neil Brayfield} <${###:neil@test.com}>" 360 | }, 361 | "param": { 362 | "gapBefore": true 363 | } 364 | } 365 | }, 366 | "input": { 367 | "message": "Undocumented function", 368 | "params": [ 369 | { 370 | "name": "$name", 371 | "type": "int" 372 | } 373 | ], 374 | "return": "void" 375 | }, 376 | "expected": [ 377 | "/**", 378 | " * ${1:Undocumented function}", 379 | " *", 380 | " * @author ${2:Neil Brayfield} <${3:neil@test.com}>", 381 | " *", 382 | " * @param ${4:int} \\$name", 383 | " */" 384 | ] 385 | }, 386 | { 387 | "name": "Align params", 388 | "config": { 389 | "gap": true, 390 | "alignParams": true 391 | }, 392 | "input": { 393 | "message": "Undocumented function", 394 | "params": [ 395 | { 396 | "name": "$nameLong", 397 | "type": "int" 398 | }, 399 | { 400 | "name": "$name", 401 | "type": "string" 402 | } 403 | ], 404 | "return": "LongClassName" 405 | }, 406 | "expected": [ 407 | "/**", 408 | " * ${1:Undocumented function}", 409 | " *", 410 | " * @param ${2:int} \\$nameLong", 411 | " * @param ${3:string} \\$name", 412 | " * @return ${4:LongClassName}", 413 | " */" 414 | ] 415 | }, 416 | { 417 | "name": "Align params without return statement", 418 | "config": { 419 | "gap": true, 420 | "alignParams": true, 421 | "returnVoid": false 422 | }, 423 | "input": { 424 | "message": "Undocumented function", 425 | "params": [ 426 | { 427 | "name": "$nameLong", 428 | "type": "int" 429 | }, 430 | { 431 | "name": "$name", 432 | "type": "string" 433 | }, 434 | { 435 | "name": "$nameEvenLonger", 436 | "type": "LongClassName" 437 | }, 438 | { 439 | "name": "$n", 440 | "type": "bool" 441 | } 442 | ], 443 | "return": "void" 444 | }, 445 | "expected": [ 446 | "/**", 447 | " * ${1:Undocumented function}", 448 | " *", 449 | " * @param ${2:int} \\$nameLong", 450 | " * @param ${3:string} \\$name", 451 | " * @param ${4:LongClassName} \\$nameEvenLonger", 452 | " * @param ${5:bool} \\$n", 453 | " */" 454 | ] 455 | }, 456 | { 457 | "name": "Align params and return statement", 458 | "config": { 459 | "gap": true, 460 | "alignParams": true, 461 | "alignReturn": true 462 | }, 463 | "input": { 464 | "message": "Undocumented function", 465 | "params": [ 466 | { 467 | "name": "$nameLong", 468 | "type": "int" 469 | }, 470 | { 471 | "name": "$name", 472 | "type": "string" 473 | } 474 | ], 475 | "return": "LongClassName" 476 | }, 477 | "expected": [ 478 | "/**", 479 | " * ${1:Undocumented function}", 480 | " *", 481 | " * @param ${2:int} \\$nameLong", 482 | " * @param ${3:string} \\$name", 483 | " * @return ${4:LongClassName}", 484 | " */" 485 | ] 486 | }, 487 | { 488 | "name": "Align params and shorter return statement", 489 | "config": { 490 | "gap": true, 491 | "alignParams": true, 492 | "alignReturn": true 493 | }, 494 | "input": { 495 | "message": "Undocumented function", 496 | "params": [ 497 | { 498 | "name": "$nameLong", 499 | "type": "int" 500 | }, 501 | { 502 | "name": "$name", 503 | "type": "string" 504 | } 505 | ], 506 | "return": "int" 507 | }, 508 | "expected": [ 509 | "/**", 510 | " * ${1:Undocumented function}", 511 | " *", 512 | " * @param ${2:int} \\$nameLong", 513 | " * @param ${3:string} \\$name", 514 | " * @return ${4:int}", 515 | " */" 516 | ] 517 | }, 518 | { 519 | "name": "Align params and return statement with description", 520 | "config": { 521 | "gap": true, 522 | "alignParams": true, 523 | "alignReturn": true, 524 | "paramDescription": true 525 | }, 526 | "input": { 527 | "message": "Undocumented function", 528 | "params": [ 529 | { 530 | "name": "$nameLong", 531 | "type": "int" 532 | }, 533 | { 534 | "name": "$name", 535 | "type": "string" 536 | } 537 | ], 538 | "return": "int" 539 | }, 540 | "expected": [ 541 | "/**", 542 | " * ${1:Undocumented function}", 543 | " *", 544 | " * @param ${2:int} \\$nameLong ${3}", 545 | " * @param ${4:string} \\$name ${5}", 546 | " * @return ${6:int}", 547 | " */" 548 | ] 549 | }, 550 | { 551 | "name": "Align params and return statement with description and return description", 552 | "config": { 553 | "gap": true, 554 | "alignParams": true, 555 | "alignReturn": true, 556 | "paramDescription": "description", 557 | "returnDescription": true 558 | }, 559 | "input": { 560 | "message": "Undocumented function", 561 | "params": [ 562 | { 563 | "name": "$nameLong", 564 | "type": "int" 565 | }, 566 | { 567 | "name": "$name", 568 | "type": "string" 569 | } 570 | ], 571 | "return": "int" 572 | }, 573 | "expected": [ 574 | "/**", 575 | " * ${1:Undocumented function}", 576 | " *", 577 | " * @param ${2:int} \\$nameLong ${3:description}", 578 | " * @param ${4:string} \\$name ${5:description}", 579 | " * @return ${6:int} ${7}", 580 | " */" 581 | ] 582 | }, 583 | { 584 | "name": "Align params and return statement with description and return description text", 585 | "config": { 586 | "gap": true, 587 | "alignParams": true, 588 | "alignReturn": true, 589 | "paramDescription": "description", 590 | "returnDescription": "description" 591 | }, 592 | "input": { 593 | "message": "Undocumented function", 594 | "params": [ 595 | { 596 | "name": "$nameLong", 597 | "type": "int" 598 | }, 599 | { 600 | "name": "$name", 601 | "type": "string" 602 | } 603 | ], 604 | "return": "LongClassName" 605 | }, 606 | "expected": [ 607 | "/**", 608 | " * ${1:Undocumented function}", 609 | " *", 610 | " * @param ${2:int} \\$nameLong ${3:description}", 611 | " * @param ${4:string} \\$name ${5:description}", 612 | " * @return ${6:LongClassName} ${7:description}", 613 | " */" 614 | ] 615 | }, 616 | { 617 | "name": "Param and return descriptions", 618 | "config": { 619 | "gap": true, 620 | "paramDescription": "placeholder", 621 | "returnDescription": "placeholder" 622 | }, 623 | "input": { 624 | "message": "Undocumented function", 625 | "params": [ 626 | { 627 | "name": "$nameLong", 628 | "type": "int" 629 | }, 630 | { 631 | "name": "$name", 632 | "type": "string" 633 | } 634 | ], 635 | "return": "int" 636 | }, 637 | "expected": [ 638 | "/**", 639 | " * ${1:Undocumented function}", 640 | " *", 641 | " * @param ${2:int} \\$nameLong ${3:placeholder}", 642 | " * @param ${4:string} \\$name ${5:placeholder}", 643 | " * @return ${6:int} ${7:placeholder}", 644 | " */" 645 | ] 646 | } 647 | ] 648 | -------------------------------------------------------------------------------- /test/fixtures/functions.php.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": "simple", 4 | "name": "Simple public", 5 | "result": { 6 | "return": "void", 7 | "params": [] 8 | } 9 | }, 10 | { 11 | "key": "simple-static", 12 | "name": "Simple public static", 13 | "result": { 14 | "return": "void", 15 | "params": [] 16 | } 17 | }, 18 | { 19 | "key": "constructor-no-vis", 20 | "name": "Constructor with no visibility", 21 | "result": { 22 | "return": null, 23 | "params": [] 24 | } 25 | }, 26 | { 27 | "key": "abstract-simple", 28 | "name": "Abstract method simple", 29 | "result": { 30 | "return": "void", 31 | "params": [] 32 | } 33 | }, 34 | { 35 | "key": "abstract-static", 36 | "name": "Abstract method static", 37 | "result": { 38 | "return": "void", 39 | "params": [] 40 | } 41 | }, 42 | { 43 | "key": "abstract-static-alt", 44 | "name": "Abstract method static alternate", 45 | "result": { 46 | "return": "void", 47 | "params": [] 48 | } 49 | }, 50 | { 51 | "key": "final-static", 52 | "name": "Final static method", 53 | "result": { 54 | "return": "void", 55 | "params": [] 56 | } 57 | }, 58 | { 59 | "key": "final", 60 | "name": "Final method", 61 | "config": { 62 | "qualifyClassNames": true 63 | }, 64 | "result": { 65 | "return": "void", 66 | "params": [] 67 | } 68 | }, 69 | { 70 | "key": "params", 71 | "name": "Basic params", 72 | "config": { 73 | "qualifyClassNames": true 74 | }, 75 | "result": { 76 | "return": "void", 77 | "params": [ 78 | { 79 | "name": "$var", 80 | "type": "[type]" 81 | }, 82 | { 83 | "name": "$var2", 84 | "type": "[type]" 85 | }, 86 | { 87 | "name": "$var3", 88 | "type": "[type]" 89 | } 90 | ] 91 | } 92 | }, 93 | { 94 | "key": "params-complex", 95 | "name": "Complex params", 96 | "result": { 97 | "return": "void", 98 | "params": [ 99 | { 100 | "name": "$var", 101 | "type": "[type]" 102 | }, 103 | { 104 | "name": "$var2", 105 | "type": "Hint" 106 | }, 107 | { 108 | "name": "$var3", 109 | "type": "boolean" 110 | } 111 | ] 112 | } 113 | }, 114 | { 115 | "key": "multiline", 116 | "name": "Basic Multiline", 117 | "result": { 118 | "return": "void", 119 | "params": [ 120 | { 121 | "name": "$var", 122 | "type": "[type]" 123 | }, 124 | { 125 | "name": "$var2", 126 | "type": "[type]" 127 | }, 128 | { 129 | "name": "$var3", 130 | "type": "[type]" 131 | } 132 | ] 133 | } 134 | }, 135 | { 136 | "key": "multiline-complex", 137 | "name": "Complex Multiline", 138 | "result": { 139 | "return": "void", 140 | "params": [ 141 | { 142 | "name": "$var", 143 | "type": "TypeHint" 144 | }, 145 | { 146 | "name": "$var2", 147 | "type": "[type]" 148 | }, 149 | { 150 | "name": "$var3", 151 | "type": "string" 152 | } 153 | ] 154 | } 155 | }, 156 | { 157 | "key": "array-params", 158 | "name": "Complex Array params", 159 | "result": { 160 | "return": "void", 161 | "params": [ 162 | { 163 | "name": "$var", 164 | "type": "array" 165 | }, 166 | { 167 | "name": "$var2", 168 | "type": "array" 169 | } 170 | ] 171 | } 172 | }, 173 | { 174 | "key": "nullable-return-type", 175 | "name": "Nullable return type", 176 | "result": { 177 | "return": "string|null", 178 | "params": [] 179 | } 180 | }, 181 | { 182 | "key": "nullable-args", 183 | "name": "Nullable args", 184 | "result": { 185 | "return": "void", 186 | "params": [ 187 | { 188 | "name": "$var", 189 | "type": "TypeHint|null" 190 | }, 191 | { 192 | "name": "$var2", 193 | "type": "\\Type2|null" 194 | }, 195 | { 196 | "name": "$var3", 197 | "type": "string|null" 198 | } 199 | ] 200 | } 201 | }, 202 | { 203 | "key": "param-types", 204 | "name": "All param types", 205 | "result": { 206 | "return": "void", 207 | "params": [ 208 | { 209 | "name": "$hint", 210 | "type": "TypeHint" 211 | }, 212 | { 213 | "name": "$boolean", 214 | "type": "boolean" 215 | }, 216 | { 217 | "name": "$string", 218 | "type": "string" 219 | }, 220 | { 221 | "name": "$string2", 222 | "type": "string" 223 | }, 224 | { 225 | "name": "$int", 226 | "type": "integer" 227 | }, 228 | { 229 | "name": "$float", 230 | "type": "float" 231 | }, 232 | { 233 | "name": "$array", 234 | "type": "array" 235 | }, 236 | { 237 | "name": "$array2", 238 | "type": "array" 239 | } 240 | ] 241 | } 242 | }, 243 | { 244 | "key": "param-namespace", 245 | "name": "Param with namespaced typehint", 246 | "result": { 247 | "return": "void", 248 | "params": [ 249 | { 250 | "name": "$hint", 251 | "type": "\\TypeHint" 252 | }, 253 | 254 | { 255 | "name": "$test", 256 | "type": "[type]" 257 | } 258 | ] 259 | } 260 | }, 261 | { 262 | "key": "param-namespace-full", 263 | "name": "Param with fully namespaced typehint", 264 | "result": { 265 | "return": "void", 266 | "params": [ 267 | { 268 | "name": "$hint", 269 | "type": "App\\Model\\TypeHint" 270 | }, 271 | 272 | { 273 | "name": "$test", 274 | "type": "[type]" 275 | } 276 | ] 277 | } 278 | }, 279 | { 280 | "key": "param-default-null", 281 | "name": "Param with default null", 282 | "result": { 283 | "return": "void", 284 | "params": [ 285 | { 286 | "name": "$arg", 287 | "type": "integer|null" 288 | } 289 | ] 290 | } 291 | }, 292 | { 293 | "key": "param-mixed-default-null", 294 | "name": "Param with mixed and default null", 295 | "result": { 296 | "return": "void", 297 | "params": [ 298 | { 299 | "name": "$arg", 300 | "type": "mixed" 301 | } 302 | ] 303 | } 304 | }, 305 | { 306 | "key": "args", 307 | "name": "Argument with ...$args", 308 | "result": { 309 | "return": "void", 310 | "params": [ 311 | { 312 | "name": "...$args", 313 | "type": "[type]" 314 | } 315 | ] 316 | } 317 | }, 318 | { 319 | "key": "args-typed", 320 | "name": "Arguments with ...$args that are typed", 321 | "config": { 322 | "useShortNames": true 323 | }, 324 | "result": { 325 | "return": "void", 326 | "params": [ 327 | { 328 | "name": "...$args", 329 | "type": "int" 330 | } 331 | ] 332 | } 333 | }, 334 | { 335 | "key": "args-typed-long", 336 | "name": "Arguments with ...$args that are typed long form", 337 | "result": { 338 | "return": "void", 339 | "params": [ 340 | { 341 | "name": "...$args", 342 | "type": "integer" 343 | } 344 | ] 345 | } 346 | }, 347 | { 348 | "key": "php7-return", 349 | "name": "PHP7 return type", 350 | "result": { 351 | "return": "TypeHint", 352 | "params": [] 353 | } 354 | }, 355 | { 356 | "key": "php7-return-snake", 357 | "name": "PHP7 return snake case", 358 | "result": { 359 | "return": "Type_Hint3", 360 | "params": [] 361 | } 362 | }, 363 | { 364 | "key": "php7-return-alt", 365 | "name": "PHP7 return type alternate", 366 | "config": { 367 | "qualifyClassNames": true 368 | }, 369 | "result": { 370 | "return": "float", 371 | "params": [] 372 | } 373 | }, 374 | { 375 | "key": "php7-return-namespace", 376 | "name": "PHP7 return namespace type", 377 | "result": { 378 | "return": "\\TypeHint", 379 | "params": [] 380 | } 381 | }, 382 | { 383 | "key": "php7-return-namespace-full", 384 | "name": "PHP7 return namespace type", 385 | "result": { 386 | "return": "App\\Model\\TypeHint", 387 | "params": [] 388 | } 389 | }, 390 | { 391 | "key": "php8-return-union-types", 392 | "name": "PHP8 return union types", 393 | "result": { 394 | "return": "integer|boolean|\\TypeHint|App\\Model\\TypeHint", 395 | "params": [] 396 | } 397 | }, 398 | { 399 | "key": "php8-return-union-types-with-short-name", 400 | "name": "PHP8 return union types with short-name", 401 | "config": { 402 | "useShortNames": true 403 | }, 404 | "result": { 405 | "return": "int|bool|\\TypeHint|App\\Model\\TypeHint", 406 | "params": [] 407 | } 408 | }, 409 | { 410 | "key": "php8-param-union-types", 411 | "name": "PHP8 param union types", 412 | "config": { 413 | "useShortNames": true 414 | }, 415 | "result": { 416 | "return": "void", 417 | "params": [ 418 | { 419 | "name": "$arg", 420 | "type": "int|bool|\\TypeHint|App\\Model\\TypeHint" 421 | }, 422 | { 423 | "name": "...$args", 424 | "type": "string|\\Closure" 425 | } 426 | ] 427 | } 428 | }, 429 | { 430 | "key": "php8-constructor-promotion", 431 | "name": "Constructor with promotion", 432 | "result": { 433 | "return": null, 434 | "params": [ 435 | { 436 | "name": "$arg1", 437 | "type": "[type]" 438 | }, 439 | { 440 | "name": "$arg2", 441 | "type": "integer" 442 | }, 443 | { 444 | "name": "$arg3", 445 | "type": "integer|null" 446 | }, 447 | { 448 | "name": "$arg4", 449 | "type": "string" 450 | } 451 | ] 452 | } 453 | }, 454 | { 455 | "key": "trailing-comma", 456 | "name": "Trailing comma after params", 457 | "result": { 458 | "return": "void", 459 | "params": [ 460 | { 461 | "name": "$var", 462 | "type": "integer" 463 | }, 464 | { 465 | "name": "$var2", 466 | "type": "string" 467 | } 468 | ] 469 | } 470 | }, 471 | { 472 | "key": "trailing-comma-multi", 473 | "name": "Trailing comma after params multi-line", 474 | "result": { 475 | "return": "void", 476 | "params": [ 477 | { 478 | "name": "$var", 479 | "type": "integer" 480 | }, 481 | { 482 | "name": "$var2", 483 | "type": "Class_Name" 484 | } 485 | ] 486 | } 487 | }, 488 | { 489 | "key": "php81-intersection-types", 490 | "name": "PHP8.1 intersection types", 491 | "result": { 492 | "return": "void", 493 | "params": [ 494 | { 495 | "name": "$var", 496 | "type": "Iterator&\\Countable" 497 | } 498 | ] 499 | } 500 | }, 501 | { 502 | "key": "php81-union-intersection-types-fault-tolerant", 503 | "name": "PHP8.1 unionin types AND tersection types with fault-tolerant", 504 | "result": { 505 | "return": "void", 506 | "params": [ 507 | { 508 | "name": "$var", 509 | "type": "Iterator&\\Countable|Aaa|Bbb" 510 | } 511 | ] 512 | } 513 | }, 514 | { 515 | "key": "function-reference", 516 | "name": "Function with reference ampersand", 517 | "result": { 518 | "return": "void", 519 | "params": [] 520 | } 521 | }, 522 | { 523 | "key": "php7-return-param", 524 | "name": "PHP7 return type with param", 525 | "config": { 526 | "useShortNames": true 527 | }, 528 | "result": { 529 | "return": "int", 530 | "params": [ 531 | { 532 | "name": "$param", 533 | "type": "float" 534 | } 535 | ] 536 | } 537 | }, 538 | { 539 | "key": "php7-return-param-long", 540 | "name": "PHP7 return type with param long form", 541 | "config": { 542 | "qualifyClassNames": true 543 | }, 544 | "result": { 545 | "return": "integer", 546 | "params": [ 547 | { 548 | "name": "$param", 549 | "type": "float" 550 | } 551 | ] 552 | } 553 | }, 554 | { 555 | "key": "php7-return-multiline", 556 | "name": "PHP7 return type multi-line", 557 | "config": { 558 | "useShortNames": true 559 | }, 560 | "result": { 561 | "return": "int", 562 | "params": [ 563 | { 564 | "name": "$param", 565 | "type": "float" 566 | }, 567 | { 568 | "name": "$boolean", 569 | "type": "bool" 570 | } 571 | ] 572 | } 573 | }, 574 | { 575 | "key": "php7-return-multiline-long", 576 | "name": "PHP7 return type multi-line long from", 577 | "result": { 578 | "return": "integer", 579 | "params": [ 580 | { 581 | "name": "$param", 582 | "type": "float" 583 | }, 584 | { 585 | "name": "$boolean", 586 | "type": "boolean" 587 | } 588 | ] 589 | } 590 | }, 591 | { 592 | "key": "is", 593 | "name": "Check is prefix", 594 | "result": { 595 | "return": "boolean", 596 | "params": [] 597 | } 598 | }, 599 | { 600 | "key": "is-void", 601 | "name": "Check is prefix - void", 602 | "result": { 603 | "return": "void", 604 | "params": [] 605 | } 606 | }, 607 | { 608 | "key": "is-only", 609 | "name": "Check is prefix - only", 610 | "result": { 611 | "return": "boolean", 612 | "params": [] 613 | } 614 | }, 615 | { 616 | "key": "has", 617 | "name": "Check has prefix", 618 | "result": { 619 | "return": "boolean", 620 | "params": [] 621 | } 622 | }, 623 | { 624 | "key": "has-void", 625 | "name": "Check has prefix - void", 626 | "result": { 627 | "return": "void", 628 | "params": [] 629 | } 630 | }, 631 | { 632 | "key": "has-only", 633 | "name": "Check has prefix - only", 634 | "result": { 635 | "return": "boolean", 636 | "params": [] 637 | } 638 | }, 639 | { 640 | "key": "can", 641 | "name": "Check can prefix", 642 | "result": { 643 | "return": "boolean", 644 | "params": [] 645 | } 646 | }, 647 | { 648 | "key": "can-void", 649 | "name": "Check can prefix - void", 650 | "result": { 651 | "return": "void", 652 | "params": [] 653 | } 654 | }, 655 | { 656 | "key": "can-only", 657 | "name": "Check can prefix - only", 658 | "result": { 659 | "return": "boolean", 660 | "params": [] 661 | } 662 | }, 663 | { 664 | "key": "should", 665 | "name": "Check should prefix", 666 | "result": { 667 | "return": "boolean", 668 | "params": [] 669 | } 670 | }, 671 | { 672 | "key": "should-void", 673 | "name": "Check should prefix - void", 674 | "result": { 675 | "return": "void", 676 | "params": [] 677 | } 678 | }, 679 | { 680 | "key": "should-only", 681 | "name": "Check should prefix - only", 682 | "result": { 683 | "return": "boolean", 684 | "params": [] 685 | } 686 | }, 687 | { 688 | "key": "debug-info", 689 | "name": "Debug info return type", 690 | "result": { 691 | "return": "array", 692 | "params": [] 693 | } 694 | }, 695 | { 696 | "key": "sleep", 697 | "name": "Magic sleep return type", 698 | "result": { 699 | "return": "array", 700 | "params": [] 701 | } 702 | }, 703 | { 704 | "key": "wakeup", 705 | "name": "Magic wakeup return type", 706 | "result": { 707 | "return": null, 708 | "params": [] 709 | } 710 | }, 711 | { 712 | "key": "isset", 713 | "name": "isset return type", 714 | "result": { 715 | "return": "boolean", 716 | "params": [ 717 | { 718 | "name": "$name", 719 | "type": "[type]" 720 | } 721 | ] 722 | } 723 | }, 724 | { 725 | "key": "to-string", 726 | "name": "toString return type", 727 | "result": { 728 | "return": "string", 729 | "params": [] 730 | } 731 | }, 732 | { 733 | "key": "set", 734 | "name": "Magic set", 735 | "result": { 736 | "return": null, 737 | "params": [ 738 | { 739 | "name": "$name", 740 | "type": "[type]" 741 | }, 742 | { 743 | "name": "$value", 744 | "type": "[type]" 745 | } 746 | ] 747 | } 748 | }, 749 | { 750 | "key": "unset", 751 | "name": "unset return type", 752 | "result": { 753 | "return": null, 754 | "params": [ 755 | { 756 | "name": "$name", 757 | "type": "[type]" 758 | } 759 | ] 760 | } 761 | } 762 | ] 763 | --------------------------------------------------------------------------------