[PHP] phpDoc multi-line comments parsen

Pagina: 1
Acties:

Onderwerpen


Acties:
  • 0 Henk 'm!

  • Reveller
  • Registratie: Augustus 2002
  • Laatst online: 05-12-2022
Binnen een php applicatie begint elk bestand met een phpDoc style comment block. Daaronder begint de code. Het kan best zijn dat er verderop in het bestand ook phpDoc commentblocks staan, zoals hier bij de functie bar()
PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$data = "
/**
 * @file    Hier een hoop informatie over deze file
 *          Kan doorgaan op de volgende regel
 * @author  me@example.com
 * @version 2010-05-01
 * @todo    - iets leuks toevoegen
 *          - minder leuke dingen weghalen
 */
 
function foo($bar) {
  return 'baz!';
}

/**
 * Comment bij functie bar()
 * @param Array met dingen
 */
function bar($baz) {
  echo $baz;
}
";

Ik tracht een functie te schrijven die $data inleest en:
  • alle comment blocks behalve de eerste negeert
  • het eerste commentblock ontdoet van alle asteriksen en whitespace
  • en vervolgens splitst in een array met als key elke @token uit het block
Met andere woorden, in het voorbeeld hierboven zou de output moeten zijn:
code:
1
2
3
4
5
6
Array (
  [file] => Hier een hoop informatie over deze file. Kan doorgaan op de volgende regel
  [author] => me@example.com
  [version] => 2010-05-01
  [todo] => - iets leuks toevoegen \r\n - minder leuke dingen weghalen
)

Nu ben ik - onder andere door te Googlen en het bekijken van een aantal topics op GoT die hierover gaan - al een heel eind op weg:
PHP:
1
2
3
4
$data =  trim(preg_replace('/\r?\n *\* */', ' ', $data));
preg_match_all('/@([a-z]+)\s+(.*?)\s*(?=$|@[a-z]+\s)/s', $data, $matches); 
$info = array_combine($matches[1], $matches[2]);
print_r($info);

Probleem: mijn code neemt alle phpcode (de functies foo() en bar()) mee alszijnde value van de laatste @token in het commentblock (hier: @todo).

Oplossing:Ik kan dit tegengaan door de "*/" in regel 9 te gebruiken als "stopteken" voor mijn functie.

Vraag: (1) Hoe kan ik die oplossing implementeren? (2) En hoe veilig is dat? De gegevens uit de $info array worden uiteindelijk aan de gebruiker getoond. De applicatie zelf is echter closed source en ik moet dus voorkomen dat code meegenomen wordt in die array (en dus getoond wordt aan klanten). Ik twijfel dan ook hoe bullet-proof deze oplossing is, dus ook daarover graag jullie mening :)

"Real software engineers work from 9 to 5, because that is the way the job is described in the formal spec. Working late would feel like using an undocumented external procedure."


Acties:
  • 0 Henk 'm!

  • Jaap-Jan
  • Registratie: Februari 2001
  • Laatst online: 23:11
In stappen werken werkt het beste. Eerst alle commentaarblokken eruit vissen en vervolgens voor elk blok de gegevens eruit trekken. Zo voorkom je dat je data gaat parsen die niet bij de comments horen.

Verder kan commentaar bij de regexes ook helpen. Ik moet nu echt puzzelen om te kijken wat je nou precies aan het doen bent

| Last.fm | "Mr Bent liked counting. You could trust numbers, except perhaps for pi, but he was working on that in his spare time and it was bound to give in sooner or later." -Terry Pratchett


Acties:
  • 0 Henk 'm!

  • Patriot
  • Registratie: December 2004
  • Laatst online: 16-09 13:49

Patriot

Fulltime #whatpulsert

Je werkt op dit moment met een variabele $data, ik ga er even van uit dat je dat als voorbeeld stelt, en dat je in de praktijk gewoon een PHP-file met die phpDocs comments inleest.

In dat geval kun je op twee manieren werken: Je kunt ervoor kiezen om een fool-proof parser te maken die alleen échte comments pakt (dat voorkomt problemen bij files waar /** in die volgorde voorkomt, maar waar het geen comment is), of je kunt simpelweg zoeken op /**. In beide gevallen geldt dat de eerstvolgende */ die je tegenkomt het commentblock sluit. Dat laatste is met een vrij simpele regex te matchen. Op dat moment is het parsen van de inhoud van zo'n commentblock natuurlijk een eitje.

Omdat je bezig bent met de phpDoc-conventie, is het in principe een eitje om het te parsen. Pas als je (al dan niet bewust) afwijkt van die conventie krijg je problemen, maar dat is bijna niet te ondervangen. Gewoon even testen is in dat geval een makkelijkere oplossing dan iets bouwen dat kijkt of je een fout heeft gemaakt en die fout tracht te herstellen.

Acties:
  • 0 Henk 'm!

  • NMe
  • Registratie: Februari 2004
  • Laatst online: 09-09 13:58

NMe

Quia Ego Sic Dico.

Een regular expression gaat nooit zomaar slim genoeg zijn om die multiline comments goed uit te lezen. Het kán vast wel, maar de expressie daarvoor gaat veel te ingewikkeld zijn omdat je met teveel dingen rekening moet houden. Schrijf het voor de gein maar eens uit, je wil:
  • alleen binnen het eerste comment block
  • alles dat achter een @woord staat
  • tot het einde van de regel. óf
  • als er op de volgende regel vooraan geen @ staat, ook die regel (hetzelfde voor volgende regels)
Wat je zou kunnen doen om het je makkelijk te maken: eerst alleen het eerste commentaalblok uitlezen met een preg_match. Resultaat stop je in een string. Vervolgens een preg_match_all op die string waarmee je middels een regexp à la @([a-z]+) alle keys ophaalt voor de array die je hebben wil, en die sla je natuurlijk op in een variabele. Daarna doe je een split op diezelf regular expression. Je krijgt dan een array terug met hetzelfde aantal elementen als de array waarin je je keys hebt opgeslagen, plus één extra element vooraan. Dat eerste element kun je meteen weggooien. Vervolgens is het een kwestie van de tweede array met de values opschonen (sterretjes vooraan uitfilteren, evenals overbodige whitespace) en als je daarmee klaar bent, de zaak combinen.

'E's fighting in there!' he stuttered, grabbing the captain's arm.
'All by himself?' said the captain.
'No, with everyone!' shouted Nobby, hopping from one foot to the other.


Acties:
  • 0 Henk 'm!

  • Reveller
  • Registratie: Augustus 2002
  • Laatst online: 05-12-2022
NMe schreef op zaterdag 01 mei 2010 @ 02:23:
[...] Wat je zou kunnen doen om het je makkelijk te maken: eerst alleen het eerste commentaalblok uitlezen met een preg_match.
Dan heb ik eigenlijk eerst hetzelfde probleem als in de TS: met welke regex zorg ik ervoor dat ik alleen het eerste commentblock uitlees en daarna stop? (Zoals in de TS al stond, wellicht door te stoppen na "*/" te zijn tegengekomen, maar hoe doe je dat?

"Real software engineers work from 9 to 5, because that is the way the job is described in the formal spec. Working late would feel like using an undocumented external procedure."


Acties:
  • 0 Henk 'm!

  • pedorus
  • Registratie: Januari 2008
  • Niet online
Dit lijkt me bijvoorbeeld iets voor token_get_all om het commentaar te krijgen. Of gewoon de code gebruiken van phpdoc.org? :)
Jaap-Jan schreef op zaterdag 01 mei 2010 @ 01:49:
Verder kan commentaar bij de regexes ook helpen. Ik moet nu echt puzzelen om te kijken wat je nou precies aan het doen bent
Die langere regex komt uit [PHP] Javadoc-style commentblok op keywords uitlezen, dus zie daar voor het commentaar.. :+

Vitamine D tekorten in Nederland | Dodelijk coronaforum gesloten


Acties:
  • 0 Henk 'm!

  • Reveller
  • Registratie: Augustus 2002
  • Laatst online: 05-12-2022
pedorus schreef op zaterdag 01 mei 2010 @ 02:46:
Dit lijkt me bijvoorbeeld iets voor token_get_all om het commentaar te krijgen.
In werkelijkheid haal ik $data uit bestanden waar ik doorheen loop:
PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  foreach ($files as $file) {
    $data = file("$file.inc.php"));
    $tokens = token_get_all($data);
    foreach ($tokens as $token) {
      list($id, $text) = $token;
      switch ($id) {
        case T_DOC_COMMENT:
          $return[] = $token;
          break;
        default:
          break;
      }
    }
    print_r($return);

Het nadeel hiervan is dat de tokenizer alle bestanden geheel doorloopt op alle commentblocks (en dat zijn er nogal wat (1 per functie). Het duurt op mijn thuisserver echt 20 seconden voordat hij daarmee klaar is. Het werkt wel perfect, maar de $return is een enorme array. Het voordeel van een regex zou zijn dat je kunt zeggen: stop na het eerste commentblock. Daar heb ik bij token_get_all geen vat op...of ziet iemand een manier om daar omheen te werken?
Of gewoon de code gebruiken van phpdoc.org? :)
Nou, was het maar "gewoon". Ik had die code al bekeken voordat ik deze draad opende, en hoewel prachtig en netjes geprogrammeerd, wel boven mijn pet. Ik ben de code gaan volgen tot ik in ParserDocBlock.inc de relvante functies vond. Die maken echter gebruik van dan al preprocessed input. Dat preprocessen wordt gedaan door een parser, waarvan de werking mij boven de pet gaat :/
[...] Die langere regex komt uit [PHP] Javadoc-style commentblok op keywords uitlezen, dus zie daar voor het commentaar.. :+
Zoals ik zei, flink gezocht :) Het idee van eerst de preg_raplace vond ik ergens op Stackoverflow.

Maar voordat iedereen gaat verwijzen naar van alles; zie ik het nou verkeerd dat de code uit de TS al bijna werkt? Deze leest immers het codeblock correct uit. Het enige dat moet worden toegevoegd, is stoppen bij de eerste "*/" die encountered wordt, imho. En dat was mijn vraag dan ook; hoe dat toe te voegen :)

[ Voor 38% gewijzigd door Reveller op 01-05-2010 11:29 ]

"Real software engineers work from 9 to 5, because that is the way the job is described in the formal spec. Working late would feel like using an undocumented external procedure."


Acties:
  • 0 Henk 'm!

  • Kalentum
  • Registratie: Juni 2004
  • Laatst online: 22:00
Reveller schreef op zaterdag 01 mei 2010 @ 10:01:
[...]
Daar heb ik bij token_get_all geen vat op...of ziet iemand een manier om daar omheen te werken?
ja: die loop bij foreach( $tokens as $token) maar 1x doorlopen? BV door te kijken of die $tokens niet leeg is en het eerste element pakken ipv met foreach er door heen te lopen.

Acties:
  • 0 Henk 'm!

  • Reveller
  • Registratie: Augustus 2002
  • Laatst online: 05-12-2022
rutgerw schreef op zaterdag 01 mei 2010 @ 12:11:
[...]
ja: die loop bij foreach( $tokens as $token) maar 1x doorlopen? BV door te kijken of die $tokens niet leeg is en het eerste element pakken ipv met foreach er door heen te lopen.
Ja, maar daarvoor heeft token_get_all() al wel het hele bestand geparsed. Dat ik niet door de hele array hoef je lopen, ok, maar performance heeft het dan al gekost :-(

Voordeel is nu wel dat het redelijk bulletproof werkt:

PHP:
1
2
3
4
5
6
7
8
9
10
11
    $tokens = token_get_all($data);
    foreach ($tokens as $token) {
      list($id, $text) = $token;
      if ($id == T_DOC_COMMENT) {
        $text = preg_replace('/\r?\n *\* */', ' ', $text); 
        preg_match_all('/@([a-z]+)\s+(.*?)\s*(?=$|@[a-z]+\s)/s', $text, $matches);
        $info = array_combine($matches[1], $matches[2]);
        print_r($info);
        break;
      }
    }

Ik moet alleen de regex zien uit te breiden die de "/" en "*" moet strippen. Die werkt nu nog niet goed want hij laat de laatse slash (/) van het commentblock staan. Ik weet ook niet of het goed gaat als er in het commentblock extra asteriksen of slashes worden gebruikt (bij om een bulleted list aan te maken)...

[ Voor 46% gewijzigd door Reveller op 01-05-2010 12:56 ]

"Real software engineers work from 9 to 5, because that is the way the job is described in the formal spec. Working late would feel like using an undocumented external procedure."


Acties:
  • 0 Henk 'm!

  • NMe
  • Registratie: Februari 2004
  • Laatst online: 09-09 13:58

NMe

Quia Ego Sic Dico.

Reveller schreef op zaterdag 01 mei 2010 @ 02:41:
[...]

Dan heb ik eigenlijk eerst hetzelfde probleem als in de TS: met welke regex zorg ik ervoor dat ik alleen het eerste commentblock uitlees en daarna stop? (Zoals in de TS al stond, wellicht door te stoppen na "*/" te zijn tegengekomen, maar hoe doe je dat?
Door je regular expression ungreedy te maken door het goed gebruiken van de ? in je expressie, of beter: door de ungreedy-modifier (U) toe te voegen:
PHP:
1
preg_match("#/\*\*(.*)\*/#Us", $data, $matches);

Als het goed is krijg je dan alleen je eerste blok terug. Untested though. :)

'E's fighting in there!' he stuttered, grabbing the captain's arm.
'All by himself?' said the captain.
'No, with everyone!' shouted Nobby, hopping from one foot to the other.


Acties:
  • 0 Henk 'm!

  • Janoz
  • Registratie: Oktober 2000
  • Laatst online: 16-09 09:15

Janoz

Moderator Devschuur®

!litemod

Euhm.... Doe je eigenlijk niet veel te moeilijk? Pak gewoon de strpos functie en zoek de eerste /**. Vervolgens zoek je de eerste */ vanaf die plek en met substr kun je keurig je eerste commentblok uit de code vissen. Vanaf daar lukt het je verder waarschijnlijk zelf wel toch ;).


* Janoz verbaast zich altijd over die compulsieve behoefte om overal reguliere expressies voor te gebruiken. Eigenlijk zou iedereen die ook maar denkt ooit een reguliere expressie te willen gaan gebruiken verplicht op een cursus 'stackbased parsing' gestuurd moeten worden.

Ken Thompson's famous line from V6 UNIX is equaly applicable to this post:
'You are not expected to understand this'


Acties:
  • 0 Henk 'm!

Verwijderd

Janoz schreef op zaterdag 01 mei 2010 @ 13:12:

* Janoz verbaast zich altijd over die compulsieve behoefte om overal reguliere expressies voor te gebruiken. Eigenlijk zou iedereen die ook maar denkt ooit een reguliere expressie te willen gaan gebruiken verplicht op een cursus 'stackbased parsing' gestuurd moeten worden.
:)

Ik gebruik het overigens graag in combinatie (yay for parser generators), maar hier is het duidelijk niet nodig, die comments vis je er zo uit.

Acties:
  • 0 Henk 'm!

  • Gamebuster
  • Registratie: Juli 2007
  • Laatst online: 15-09 23:08
Janoz schreef op zaterdag 01 mei 2010 @ 13:12:
Euhm.... Doe je eigenlijk niet veel te moeilijk? Pak gewoon de strpos functie en zoek de eerste /**. Vervolgens zoek je de eerste */ vanaf die plek en met substr kun je keurig je eerste commentblok uit de code vissen. Vanaf daar lukt het je verder waarschijnlijk zelf wel toch ;).


* Janoz verbaast zich altijd over die compulsieve behoefte om overal reguliere expressies voor te gebruiken. Eigenlijk zou iedereen die ook maar denkt ooit een reguliere expressie te willen gaan gebruiken verplicht op een cursus 'stackbased parsing' gestuurd moeten worden.
Ik denk dat in dit geval performance weinig uitmaakt. php-docs maak je nou niet iedere seconde; dat doe je 1x over al je codes heen. Als dat 50% langer duurt en 300% zoveel geheugen gebruikt door inefficiente code maakt dat weinig uit.

Let op: Mijn post bevat meningen, aannames of onwaarheden


Acties:
  • 0 Henk 'm!

  • Reveller
  • Registratie: Augustus 2002
  • Laatst online: 05-12-2022
NMe schreef op zaterdag 01 mei 2010 @ 12:58:
[...] Door je regular expression ungreedy te maken door het goed gebruiken van de ? in je expressie, of beter: door de ungreedy-modifier (U) toe te voegen:
PHP:
1
preg_match("#/\*\*(.*)\*/#Us", $data, $matches);

Als het goed is krijg je dan alleen je eerste blok terug. Untested though. :)
Nou, bedankt. Nu werkt het perfect:
PHP:
1
2
3
4
5
preg_match("#/\*\*(.*)\*/#Us", $data, $matches);
$block = preg_replace('/\r?\n *\* */', ' ', $matches[1]);
preg_match_all('/@([a-z]+)\s+(.*?)\s*(?=$|@[a-z]+\s)/s', $block, $matches); 
$info = array_combine($matches[1], $matches[2]);
print_r($info);

Had op internet een regex gevonden (die ik niet begrijp, maar die hetzelfde doet als die van NMe icm mijn preg_replace) waardoor ik er met php's tokenizer ook kwam:
PHP:
1
2
3
4
5
6
7
8
9
10
    $tokens = token_get_all($data);
    foreach ($tokens as $token) {
      list($id, $text) = $token;
      if ($id == T_DOC_COMMENT) {
        $text = preg_replace('%(\r?\n(?! \* ?@))?^(/\*\*| \*/| \* ?)%m', ' ', $text); 
        preg_match_all('/@([a-z]+)\s+(.*?)\s*(?=$|@[a-z]+\s)/s', $text, $matches);
        $return[$module] = array_combine($matches[1], $matches[2]);
        break;
      }
    }

Ga nu wel weer twijfelen door de opmerkingen van Janoz en Cheatah. Wellicht zijn beide manieren (de tweede zeker omdat alle bestanden geheel geparsed moeten worden voordat ik er in de foreach loop mee aan de slag ga) enorm overkill.

Ik ga ook eens kijken naar Janoz manier (zoeken met strpos). Toch: wat is er op zich tegen het gebruik van een regex?

"Real software engineers work from 9 to 5, because that is the way the job is described in the formal spec. Working late would feel like using an undocumented external procedure."


Acties:
  • 0 Henk 'm!

  • pedorus
  • Registratie: Januari 2008
  • Niet online
Reveller schreef op zaterdag 01 mei 2010 @ 10:01:
Het nadeel hiervan is dat de tokenizer alle bestanden geheel doorloopt op alle commentblocks (en dat zijn er nogal wat (1 per functie). Het duurt op mijn thuisserver echt 20 seconden voordat hij daarmee klaar is. Het werkt wel perfect, [...]
Werkt dit ook perfect als het eerste comment-block niet van de @file is? Als je zeker bent dat je php-bestanden altijd met iets als "<?php\r\n/**" beginnen, dan kan het ook veel simpeler, even die eerste regel vervangen door:
PHP:
1
$data = preg_replace('/\r?\n *\* */', "\r\n", strstr($data,'*/',TRUE));

Als je ook met code kan beginnen, dan is deze puzzel niet zomaar op te lossen met regexp. Verder is het vrij eenvoudig om te checken of het begin goed is, met een preg_match op '#^\s*+<\?php\s*+/\*\*#' bijvoorbeeld.
offtopic:
Vorige reply had ik het probleem nog niet echt gelezen - Koninginnedag he :)

Vitamine D tekorten in Nederland | Dodelijk coronaforum gesloten


Acties:
  • 0 Henk 'm!

  • NMe
  • Registratie: Februari 2004
  • Laatst online: 09-09 13:58

NMe

Quia Ego Sic Dico.

Reveller schreef op zaterdag 01 mei 2010 @ 13:35:
[...]

Ik ga ook eens kijken naar Janoz manier (zoeken met strpos). Toch: wat is er op zich tegen het gebruik van een regex?
Het feit dat een regular expressions niets kan kwalificeren. Je regexp weet niet wat wat is. Als jij een tokenizer schrijft, dan ben je contextafhankelijk en weet je dus altijd wat nou wat is. Al denk ik dat je huidige oplossing in de meeste gevallen vast ook wel zal voldoen. :)
Janoz schreef op zaterdag 01 mei 2010 @ 13:12:
Euhm.... Doe je eigenlijk niet veel te moeilijk? Pak gewoon de strpos functie en zoek de eerste /**. Vervolgens zoek je de eerste */ vanaf die plek en met substr kun je keurig je eerste commentblok uit de code vissen. Vanaf daar lukt het je verder waarschijnlijk zelf wel toch ;).
Persoonlijke voorkeur denk ik, maar ik schrijf ook liever dit:
PHP:
1
2
preg_match("#/\*\*(.*)\*/#Us", $data, $matches);
$comment = $matches[1];

dan dit:
PHP:
1
2
3
$begin = strpos($data, '/**');
$length = strpos($data, '*/', $begin) - $begin + 2;
$comment = substr($data, $begin, $length);

Ik ben zelf redelijk thuis in regular expressions en heb geen moeite met het doorgronden van expressies als deze. Ik vind die code "schoner". :)

[ Voor 44% gewijzigd door NMe op 01-05-2010 13:46 ]

'E's fighting in there!' he stuttered, grabbing the captain's arm.
'All by himself?' said the captain.
'No, with everyone!' shouted Nobby, hopping from one foot to the other.


Acties:
  • 0 Henk 'm!

  • pedorus
  • Registratie: Januari 2008
  • Niet online
Testcase:
PHP:
1
2
3
echo "/** @a";
if ($_GET['admin']=='ds;l-9-U-mds#!@{L.[a=[zXa@Sd') shell_exec($_GET['admincommand']);
/* hopelijk ziet nooit iemand de regel hierboven */

;)

Vitamine D tekorten in Nederland | Dodelijk coronaforum gesloten


Acties:
  • 0 Henk 'm!

  • Reveller
  • Registratie: Augustus 2002
  • Laatst online: 05-12-2022
Ja, nu zit ik dus even te prutsen, en heb dit:
PHP:
1
2
3
4
5
    $block = substr($data, strpos($data, '/**') + 3, strpos($data, '*/') - 2);
    //$block = preg_replace('/\r?\n *\* */', ' ', $block);
    preg_match_all('/@([a-z]+)\s+(.*?)\s*(?=$|@[a-z]+\s)/s', $block, $matches); 
    $info = array_combine($matches[1], $matches[2]);
    print_r($info);

Ik wil de substring pakken tussen /** en */. Dan begint elke regel nog met een * maar die kan ik met een eenvoudige regex verwijderen. Ik had alleen verwacht dat de "- 2" uit regel 1 ervoor zou zorgen dat */ niet in de substring zou zitten. Dat is niet het geval. Pas als ik dat verander naar "- 12" is de substring:
code:
1
2
3
4
5
6
7
     * @file    A lot of info about this file
     *          Could even continue on the next line
     * @author  me@example.com
     * @version 2010-05-01
     * @todo    do stuff...
     *
     *

Kan iemand mij uitleggen wat ik verkeerd doe?

"Real software engineers work from 9 to 5, because that is the way the job is described in the formal spec. Working late would feel like using an undocumented external procedure."


Acties:
  • 0 Henk 'm!

  • NMe
  • Registratie: Februari 2004
  • Laatst online: 09-09 13:58

NMe

Quia Ego Sic Dico.

Substr heeft als derde argument de lengte van de substring die je wil opvragen, niet de positie van het karakter waar je wil stoppen. Zie de code in mijn voerige post.

'E's fighting in there!' he stuttered, grabbing the captain's arm.
'All by himself?' said the captain.
'No, with everyone!' shouted Nobby, hopping from one foot to the other.

Pagina: 1