Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
22 / 22 |
|
100.00% |
3 / 3 |
CRAP | |
100.00% |
1 / 1 |
TagCollector | |
100.00% |
22 / 22 |
|
100.00% |
3 / 3 |
10 | |
100.00% |
1 / 1 |
submitTag | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
startParse | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getTagsSeen | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 |
1 | <?php |
2 | namespace MediaWiki\Extension\Hashtags; |
3 | |
4 | use LogicException; |
5 | use MediaWiki\CommentFormatter\CommentParserFactory; |
6 | |
7 | /** |
8 | * @class |
9 | * |
10 | * This is a bit of a hack. |
11 | * |
12 | * We want to be able to get all the tags seen in an edit summary. |
13 | * It is unclear how best to implement this. We have the following |
14 | * restrictions: |
15 | * |
16 | * * We want our HashtagCommentParser to essentially decorate the |
17 | * core class by wrapping and forwarding methods |
18 | * * It should be possible for other extensions to decorate in |
19 | * a similar way |
20 | * * We have no idea if the service class we get is an instance |
21 | * of our class, it could be an instance from a different extension |
22 | * extending it in the same way (Maybe an alternative would be to |
23 | * use class_alis to dynamically specify that we extend whatever |
24 | * class we get from Wikimedia Services, and then we have instaneof |
25 | * and a class hierarchy that makes sense. However that kind of requires |
26 | * us to know about the constructor of the unknown class) |
27 | * ** Hence we cannot add any methods, because we don't know if the |
28 | * class we get will have them. |
29 | * * We do not want to manually construct the base class at any time |
30 | * since constructors are very unstable in MW. This means we cannot |
31 | * just make our own instance since we always want it to wrap an |
32 | * existing base instance that someone else made. |
33 | * * We want to make sure that the tags we return are actually the |
34 | * ones we would actually detect (including in the event that a |
35 | * different decorator modifies the input). |
36 | * |
37 | * So in short, we want to add a method to HashtagCommentParser to |
38 | * extract some specific data during parsing, but we never know if |
39 | * the instance we have will actually expose that method. |
40 | * |
41 | * One solution might be to simply wrap an additional time. If things |
42 | * are indempotent, then that will probably work, we just decorate it |
43 | * multiple times. This seems hacky though. |
44 | * |
45 | * The solution we go with here is to have another class which acts sort |
46 | * of like a log collector. When HashtagCommentParser is running, it sends |
47 | * the list of tags to our collector class. If the particular instanace has |
48 | * been pre-registered, then the collector saves this data, and returns it |
49 | * on a later method call. A bit hacky, and a bit icky in terms of data flow |
50 | * control, but it seems like the best of bad options. |
51 | */ |
52 | class TagCollector { |
53 | |
54 | private array $tags = []; |
55 | private bool $lock = false; |
56 | private ?HashtagCommentParser $mostRecentParser = null; |
57 | |
58 | /** |
59 | * This is very much not reenterant. Should only be called by |
60 | * HashtagCommentParser. |
61 | * |
62 | * @param HashtagCommentParser $parser To verify we are collecting tags from |
63 | * the instance we think we are. |
64 | * @param string $tag Name of tag, including prefix. |
65 | * @private |
66 | */ |
67 | public function submitTag( HashtagCommentParser $parser, string $tag ) { |
68 | if ( !$this->lock ) { |
69 | // We aren't collecting tags |
70 | return; |
71 | } |
72 | if ( $parser !== $this->mostRecentParser ) { |
73 | throw new LogicException( "tag parsing loop" ); |
74 | } |
75 | $this->tags[] = $tag; |
76 | } |
77 | |
78 | /** |
79 | * Only called by HashtagCommentParser. Used to make sure that |
80 | * our code is actually executing at all. |
81 | * |
82 | * @param HashtagCommentParser $parser The parser running our code. May |
83 | * not be the original isntance we call preprocess on, since that may |
84 | * be decorated. |
85 | * @private |
86 | */ |
87 | public function startParse( HashtagCommentParser $parser ) { |
88 | if ( !$this->lock ) { |
89 | // Not collecting tags, so no-op |
90 | return; |
91 | } |
92 | if ( $this->mostRecentParser !== null ) { |
93 | throw new LogicException( "Edit tag parsing already started" ); |
94 | } |
95 | $this->mostRecentParser = $parser; |
96 | } |
97 | |
98 | /** |
99 | * Primary entrypoint. Call this to get tags from an edit summary |
100 | * |
101 | * @param CommentParserFactory $parserFactory (May or may not be HashtagCommentParserFactory |
102 | * but output must wrap a HashtagCommentParser at the very least). |
103 | * @param string $summary The edit summary to look at |
104 | * @return array List of tags |
105 | */ |
106 | public function getTagsSeen( CommentParserFactory $parserFactory, string $summary ) { |
107 | if ( $this->lock || $this->mostRecentParser !== null ) { |
108 | throw new LogicException( __METHOD__ . " is not reenterant" ); |
109 | } |
110 | // It is important we always create a fresh parser and do not reuse. |
111 | $parser = $parserFactory->create(); |
112 | $this->lock = true; |
113 | // Remember that $parser is not neccessarily the same as $this->mostRecentParser |
114 | $parser->preprocess( $summary ); |
115 | |
116 | if ( $this->mostRecentParser === null ) { |
117 | throw new LogicException( "CommentParser never called startParse" ); |
118 | } |
119 | $tags = array_unique( $this->tags ); |
120 | $this->tags = []; |
121 | $this->lock = false; |
122 | $this->mostRecentParser = null; |
123 | return $tags; |
124 | } |
125 | |
126 | } |