KiCad PCB EDA Suite
Loading...
Searching...
No Matches
drc_rules_downgrade.h
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20#ifndef DRC_RULES_DOWNGRADE_H
21#define DRC_RULES_DOWNGRADE_H
22
23#include <algorithm>
24#include <cctype>
25#include <string>
26#include <vector>
27
28#include <wx/intl.h>
29
30#include <dsnlexer.h>
31#include <ki_exception.h>
32#include <downgrade_target.h>
34
35// The old rules parsers are all-or-nothing. One unknown keyword anywhere makes the target drop
36// every custom rule and refuse to run DRC. So the export filters the file: rules the target
37// cannot parse are commented out and reported, surviving rules are kept byte-identical.
38
42{
43 for( const DRC_RULE_VOCABULARY& vocabulary : DRC_RULE_VOCABULARIES )
44 {
45 if( aTarget.m_id == vocabulary.m_targetId )
46 return &vocabulary;
47 }
48
49 return nullptr;
50}
51
52
54inline constexpr const char* DRC_RULE_SEMANTIC_BREAKS[] = {
55 // Releases before 10.0.7 test a zone item's fill rather than its outline.
56 "intersectsarea",
57};
58
59
61inline bool DrcVocabularyHas( const char* const* aWords, size_t aCount, const std::string& aWord )
62{
63 return std::binary_search( aWords, aWords + aCount, aWord );
64}
65
66
68{
69 wxString m_text;
70 std::vector<wxString> m_dropped;
71 wxString m_error;
72};
73
74
76{
77 int m_type;
78 std::string m_text;
79 size_t m_offset;
80};
81
82
85{
86 size_t m_start = 0;
87 size_t m_end = 0;
88 size_t m_first = 0;
89 size_t m_last = 0;
90 std::string m_head;
91 wxString m_name;
92};
93
94
97inline bool TokenizeDrcRules( const std::string& aText, std::vector<DRC_RULE_TOKEN>& aTokens, wxString& aError )
98{
99 std::vector<size_t> lineStarts{ 0 };
100
101 for( size_t i = 0; i < aText.size(); i++ )
102 {
103 if( aText[i] == '\n' )
104 lineStarts.push_back( i + 1 );
105 }
106
107 DSNLEXER lexer( aText, wxT( "drc_rules_downgrade" ) );
108 lexer.SetCommentsAreTokens( true );
109
110 try
111 {
112 for( int type = lexer.NextTok(); type != DSN_EOF; type = lexer.NextTok() )
113 {
114 size_t offset = lineStarts[lexer.CurLineNumber() - 1] + lexer.CurOffset() - 1;
115
116 // The lexer places a string at its first character, after the opening quote.
117 if( type == DSN_STRING )
118 offset--;
119
120 aTokens.push_back( { type, lexer.CurText(), offset } );
121 }
122 }
123 catch( const IO_ERROR& error )
124 {
125 aError = error.Problem();
126 return false;
127 }
128
129 return true;
130}
131
132
135inline bool SplitDrcRuleBlocks( const std::vector<DRC_RULE_TOKEN>& aTokens, std::vector<DRC_RULE_BLOCK>& aBlocks,
136 wxString& aError )
137{
138 int depth = 0;
139
140 for( size_t i = 0; i < aTokens.size(); i++ )
141 {
142 const DRC_RULE_TOKEN& token = aTokens[i];
143
144 if( token.m_type == DSN_SYMBOL && token.m_text[0] == '#' )
145 {
146 aError = _( "Design rule comments must start on a separate line." );
147 return false;
148 }
149
150 if( token.m_type == DSN_COMMENT )
151 continue;
152
153 if( depth == 0 )
154 {
155 if( token.m_type != DSN_LEFT )
156 {
157 aError = _( "Design rules contain text outside a rule." );
158 return false;
159 }
160
161 DRC_RULE_BLOCK block;
162 block.m_start = token.m_offset;
163 block.m_first = i;
164
165 auto nextCode = [&aTokens]( size_t aFrom )
166 {
167 while( aFrom < aTokens.size() && aTokens[aFrom].m_type == DSN_COMMENT )
168 aFrom++;
169
170 return aFrom;
171 };
172
173 size_t head = nextCode( i + 1 );
174
175 if( head < aTokens.size() && aTokens[head].m_type == DSN_SYMBOL )
176 block.m_head = aTokens[head].m_text;
177
178 size_t name = nextCode( head + 1 );
179
180 if( block.m_head == "rule" && name < aTokens.size() && aTokens[name].m_type != DSN_LEFT
181 && aTokens[name].m_type != DSN_RIGHT )
182 {
183 block.m_name = wxString::FromUTF8( aTokens[name].m_text.c_str() );
184 }
185
186 aBlocks.push_back( block );
187 }
188
189 if( token.m_type == DSN_LEFT )
190 {
191 depth++;
192 }
193 else if( token.m_type == DSN_RIGHT )
194 {
195 depth--;
196
197 if( depth == 0 )
198 {
199 aBlocks.back().m_end = token.m_offset + 1;
200 aBlocks.back().m_last = i;
201 }
202 }
203 }
204
205 if( depth != 0 )
206 {
207 aError = _( "Unbalanced parentheses in design rules." );
208 return false;
209 }
210
211 return true;
212}
213
214
216inline bool DrcRuleUnsupportedByTarget( const std::vector<DRC_RULE_TOKEN>& aTokens, const DRC_RULE_BLOCK& aBlock,
217 const DRC_RULE_VOCABULARY& aVocabulary )
218{
219 std::vector<std::string> nodes;
220 std::vector<std::string> words;
221 std::string expressions;
222 std::string values;
223 bool headPending = false;
224 bool kindPending = false;
225 bool namePending = false;
226
227 // String literals inside an expression are not names, so only their quotes are kept as gaps.
228 auto addExpression = [&expressions]( const std::string& aExpression )
229 {
230 char quote = 0;
231
232 for( size_t j = 0; j < aExpression.size(); j++ )
233 {
234 char c = aExpression[j];
235
236 if( quote )
237 {
238 if( c == '\\' && j + 1 < aExpression.size() )
239 j++;
240 else if( c == quote )
241 quote = 0;
242 }
243 else if( c == '\'' || c == '"' )
244 {
245 quote = c;
246 expressions += ' ';
247 }
248 else
249 {
250 expressions += c;
251 }
252 }
253
254 expressions += '\n';
255 };
256
257 for( size_t i = aBlock.m_first; i <= aBlock.m_last; i++ )
258 {
259 const DRC_RULE_TOKEN& token = aTokens[i];
260
261 if( token.m_type == DSN_COMMENT )
262 continue;
263
264 if( token.m_type == DSN_LEFT )
265 {
266 nodes.emplace_back();
267 headPending = true;
268 continue;
269 }
270
271 if( token.m_type == DSN_RIGHT )
272 {
273 // A node with no head is not grammar for any reader.
274 if( headPending )
275 return true;
276
277 nodes.pop_back();
278 continue;
279 }
280
281 if( token.m_text.find( "${" ) != std::string::npos )
282 {
283 // A text variable is syntax, and a target without it fails to read the rule at all.
284 if( !aVocabulary.m_textVariables )
285 return true;
286
287 std::string folded = token.m_text;
288
289 for( char& c : folded )
290 c = static_cast<char>( std::tolower( static_cast<unsigned char>( c ) ) );
291
292 // Releases before 10.0.6 read a component class selector as a reference designator.
293 if( folded.find( "${class:" ) != std::string::npos )
294 return true;
295 }
296
297 if( headPending )
298 {
299 headPending = false;
300
301 if( token.m_type != DSN_SYMBOL )
302 return true;
303
304 nodes.back() = token.m_text;
305 words.push_back( token.m_text );
306 kindPending = token.m_text == "constraint";
307 namePending = token.m_text == "rule";
308 continue;
309 }
310
311 const std::string& node = nodes.back();
312
313 if( namePending )
314 {
315 namePending = false;
316 }
317 else if( kindPending )
318 {
319 // The constraint kind is grammar, and three kinds change how what follows is read.
320 kindPending = false;
321 words.push_back( token.m_text );
322
323 if( token.m_text == "assertion" || token.m_text == "disallow" || token.m_text == "zone_connection" )
324 nodes.back() = token.m_text;
325 }
326 else if( node == "condition" || node == "assertion" )
327 {
328 addExpression( token.m_text );
329 }
330 else if( node == "min" || node == "max" || node == "opt" )
331 {
332 values += token.m_text + ' ';
333 }
334 else if( node == "layer" )
335 {
336 // A layer node names a board layer, which is not grammar.
337 }
338 else if( node == "disallow" || node == "zone_connection" )
339 {
340 // The readers accept these items quoted, so they are keywords in disguise.
341 words.push_back( token.m_text );
342 }
343 else if( token.m_type != DSN_STRING && token.m_type != DSN_NUMBER )
344 {
345 words.push_back( token.m_text );
346 }
347 }
348
349 for( const std::string& word : words )
350 {
351 // A dotted name is a layer or similar, never grammar.
352 if( word.find( '.' ) != std::string::npos )
353 continue;
354
355 if( !DrcVocabularyHas( aVocabulary.m_tokens, aVocabulary.m_tokenCount, word ) )
356 return true;
357 }
358
359 auto isWord = []( char c )
360 {
361 return std::isalnum( static_cast<unsigned char>( c ) ) || c == '_';
362 };
363
364 std::string folded = expressions;
365
366 for( char& c : folded )
367 c = static_cast<char>( std::tolower( static_cast<unsigned char>( c ) ) );
368
369 // Inside an expression a word followed by a parenthesis is a function call, and a word after
370 // a dot is a property. Both must exist in the target.
371 for( size_t i = 0; i < folded.size(); i++ )
372 {
373 if( !std::isalpha( static_cast<unsigned char>( folded[i] ) ) && folded[i] != '_' )
374 continue;
375
376 if( i > 0 && isWord( folded[i - 1] ) )
377 continue;
378
379 size_t end = i;
380
381 while( end < folded.size() && isWord( folded[end] ) )
382 end++;
383
384 const std::string word = folded.substr( i, end - i );
385
386 size_t next = end;
387
388 while( next < folded.size() && std::isspace( static_cast<unsigned char>( folded[next] ) ) )
389 next++;
390
391 size_t prev = i;
392
393 while( prev > 0 && std::isspace( static_cast<unsigned char>( folded[prev - 1] ) ) )
394 prev--;
395
396 if( next < folded.size() && folded[next] == '(' )
397 {
398 for( const char* broken : DRC_RULE_SEMANTIC_BREAKS )
399 {
400 if( word == broken )
401 return true;
402 }
403
404 if( !DrcVocabularyHas( aVocabulary.m_functions, aVocabulary.m_functionCount, word ) )
405 return true;
406 }
407 else if( prev > 0 && folded[prev - 1] == '.' )
408 {
409 // Parent navigation is new, but the old standalone Parent string property is supported.
410 if( word == "parent" && next < folded.size() && folded[next] == '.' )
411 return true;
412
413 if( !DrcVocabularyHas( aVocabulary.m_properties, aVocabulary.m_propertyCount, word ) )
414 return true;
415 }
416
417 i = end;
418 }
419
420 // A unit is the letters after a number, glued to it or not. It must be one the target's
421 // value resolver knows.
422 const std::string numeric = values + '\n' + expressions;
423
424 for( size_t i = 0; i < numeric.size(); i++ )
425 {
426 if( !std::isalpha( static_cast<unsigned char>( numeric[i] ) ) )
427 continue;
428
429 if( i > 0 && ( std::isalpha( static_cast<unsigned char>( numeric[i - 1] ) ) || numeric[i - 1] == '_' ) )
430 continue;
431
432 size_t before = i;
433
434 while( before > 0 && std::isspace( static_cast<unsigned char>( numeric[before - 1] ) ) )
435 before--;
436
437 size_t end = i;
438
439 while( end < numeric.size() && isWord( numeric[end] ) )
440 end++;
441
442 if( before > 0 && std::isdigit( static_cast<unsigned char>( numeric[before - 1] ) )
443 && !DrcVocabularyHas( aVocabulary.m_units, aVocabulary.m_unitCount, numeric.substr( i, end - i ) ) )
444 {
445 return true;
446 }
447
448 i = end;
449 }
450
451 return false;
452}
453
454
456inline std::string CommentOutDrcBlock( const std::string& aBlock )
457{
458 std::string out = "# ";
459
460 for( char c : aBlock )
461 {
462 out += c;
463
464 if( c == '\n' )
465 out += "# ";
466 }
467
468 return out;
469}
470
471
474inline DRC_RULES_FILTER_RESULT FilterDrcRulesForTarget( const wxString& aRulesText, const DOWNGRADE_TARGET& aTarget )
475{
476 std::string text = aRulesText.ToStdString( wxConvUTF8 );
477
479
480 const DRC_RULE_VOCABULARY* vocabulary = FindDrcRuleVocabulary( aTarget );
481
482 if( !vocabulary )
483 {
484 result.m_error = wxString::Format( _( "No design rule vocabulary is recorded for %s." ), aTarget.m_name );
485 return result;
486 }
487
488 std::vector<DRC_RULE_TOKEN> tokens;
489 std::vector<DRC_RULE_BLOCK> blocks;
490
491 if( !TokenizeDrcRules( text, tokens, result.m_error ) || !SplitDrcRuleBlocks( tokens, blocks, result.m_error ) )
492 return result;
493
494 const wxString header = wxT( "(version 1)" );
495 const wxString eol = text.find( "\r\n" ) != std::string::npos ? wxT( "\r\n" ) : wxT( "\n" );
496
497 size_t consumed = 0;
498 bool headerWritten = false;
499 bool commentedOut = false;
500
501 auto endsOnBlankLine = []( const wxString& aText )
502 {
503 int lineStart = aText.Find( '\n', true );
504 return aText.Mid( lineStart == wxNOT_FOUND ? 0 : lineStart + 1 ).Trim().IsEmpty();
505 };
506
507 for( const DRC_RULE_BLOCK& block : blocks )
508 {
509 // A comment travels with the block after it, so a dropped rule takes its comment along.
510 wxString prefix = wxString::FromUTF8( text.substr( consumed, block.m_start - consumed ).c_str() );
511 std::string body = text.substr( block.m_start, block.m_end - block.m_start );
512 consumed = block.m_end;
513
514 // The version header is rewritten in place, so a comment above it stays above it.
515 if( block.m_head == "version" )
516 {
517 result.m_text += prefix;
518
519 if( !headerWritten )
520 result.m_text += header;
521
522 headerWritten = true;
523 continue;
524 }
525
526 if( !headerWritten )
527 {
528 result.m_text += header + eol;
529 headerWritten = true;
530 }
531
532 bool unsupported = block.m_head != "rule" || DrcRuleUnsupportedByTarget( tokens, block, *vocabulary );
533
534 result.m_text += prefix;
535
536 // A comment runs to the end of its line, so a commented block and whatever follows it
537 // each start on a line of their own.
538 if( ( unsupported || commentedOut ) && !endsOnBlankLine( result.m_text ) )
539 result.m_text += eol;
540
541 if( unsupported )
542 {
543 result.m_dropped.push_back( block.m_name );
544 result.m_text += wxT( "# Not supported by " ) + aTarget.m_name + wxT( ":" ) + eol;
545 result.m_text += wxString::FromUTF8( CommentOutDrcBlock( body ).c_str() );
546 }
547 else
548 {
549 result.m_text += wxString::FromUTF8( body.c_str() );
550 }
551
552 commentedOut = unsupported;
553 }
554
555 if( !headerWritten )
556 result.m_text += header + eol;
557
558 // Comments after the last block, which by now include every commented rule that was last.
559 result.m_text += wxString::FromUTF8( text.substr( consumed ).c_str() );
560
561 if( !result.m_text.EndsWith( wxT( "\n" ) ) )
562 result.m_text += eol;
563
564 return result;
565}
566
567#endif // DRC_RULES_DOWNGRADE_H
const char * name
Implement a lexical analyzer for the SPECCTRA DSN file format.
Definition dsnlexer.h:75
const char * CurText() const
Return a pointer to the current token's text.
Definition dsnlexer.h:415
int NextTok()
Return the next token found in the input file or DSN_EOF when reaching the end of file.
Definition dsnlexer.cpp:544
bool SetCommentsAreTokens(bool val)
Change the handling of comments.
Definition dsnlexer.h:304
int CurOffset() const
Return the byte offset within the current line, using a 1 based index.
Definition dsnlexer.h:473
int CurLineNumber() const
Return the current line number within my LINE_READER.
Definition dsnlexer.h:445
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
virtual const wxString Problem() const
what was the problem?
std::string CommentOutDrcBlock(const std::string &aBlock)
Prefix every line, so the target reads the whole block as comments.
bool DrcVocabularyHas(const char *const *aWords, size_t aCount, const std::string &aWord)
The generated arrays are sorted, so membership is a binary search.
constexpr const char * DRC_RULE_SEMANTIC_BREAKS[]
A function a release parses but answers differently, which the vocabulary cannot say.
bool DrcRuleUnsupportedByTarget(const std::vector< DRC_RULE_TOKEN > &aTokens, const DRC_RULE_BLOCK &aBlock, const DRC_RULE_VOCABULARY &aVocabulary)
True if the rule uses something the target's parser or evaluator cannot handle.
bool SplitDrcRuleBlocks(const std::vector< DRC_RULE_TOKEN > &aTokens, std::vector< DRC_RULE_BLOCK > &aBlocks, wxString &aError)
Group the tokens into top-level blocks.
const DRC_RULE_VOCABULARY * FindDrcRuleVocabulary(const DOWNGRADE_TARGET &aTarget)
The vocabulary a target release could parse, generated from its release tags by tools/generate_drc_ru...
bool TokenizeDrcRules(const std::string &aText, std::vector< DRC_RULE_TOKEN > &aTokens, wxString &aError)
Tokenize with the lexer the rule readers use, so comments, strings and symbols end exactly where they...
DRC_RULES_FILTER_RESULT FilterDrcRulesForTarget(const wxString &aRulesText, const DOWNGRADE_TARGET &aTarget)
Filter a .kicad_dru file for the target.
constexpr DRC_RULE_VOCABULARY DRC_RULE_VOCABULARIES[]
@ DSN_LEFT
Definition dsnlexer.h:63
@ DSN_RIGHT
Definition dsnlexer.h:62
@ DSN_NUMBER
Definition dsnlexer.h:61
@ DSN_SYMBOL
Definition dsnlexer.h:60
@ DSN_COMMENT
Definition dsnlexer.h:56
@ DSN_STRING
Definition dsnlexer.h:64
@ DSN_EOF
Definition dsnlexer.h:65
#define _(s)
CITER next(CITER it)
Definition ptree.cpp:120
An older KiCad release we can export a project back to.
wxString m_id
Stable machine key for CLI and jobsets, e.g. "10.0".
wxString m_name
User-facing name, e.g. "KiCad 10.0".
wxString m_text
The filtered rules file text.
wxString m_error
Invalid source syntax, do not write m_text.
std::vector< wxString > m_dropped
Names of the rules the target cannot parse.
A top-level block of the rules file, as byte span and token range.
wxString m_name
The name after a "rule" head, for the report.
size_t m_start
Byte offset of the opening parenthesis.
size_t m_last
Index of the closing parenthesis token.
size_t m_first
Index of the opening parenthesis token.
size_t m_end
Byte offset just past the closing parenthesis.
std::string m_head
The word after the opening parenthesis, "rule" or "version".
std::string m_text
The token as the lexer reports it, strings without their quotes.
size_t m_offset
Byte offset of the token's first character in the source.
int m_type
A DSNLEXER token type.
Everything a rules file could name in one release.
VECTOR2I end
wxString result
Test unit parsing edge cases and error handling.