Coverage for apio/utils/jsonc.py: 98%

41 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-07 05:08 +0000

1# This file is part of the Apio project 

2# (C) 2016-present FPGAwars 

3# Project authors: 

4# * Jesús Arroyo (Jesus89) (2016-2019) 

5# * Juan Gonzalez (obijuan) (2019-2024) 

6# * The Apio Team (2024-present) 

7# License: GPLv2 

8 

9"""A simple utility to string '//' comments from a json file. 

10It allows to add comments in apio resource files. 

11 

12Inspired by the state machine in 

13https://github.com/xitop/jsonstrip/blob/main/jsonstrip.py 

14 

15Json5 could do the job but it's very slow, doubling the execution time of 

16'apio system info'. 

17""" 

18 

19from enum import Enum 

20from dataclasses import dataclass 

21 

22 

23class _State(Enum): 

24 """Represents the parsing state.""" 

25 

26 IDLE = 1 

27 IN_STRING = 2 

28 IN_STRING_ESCAPE = 3 

29 IN_FIRST_SLASH = 4 

30 IN_COMMENT = 5 

31 

32 

33class _Action(Enum): 

34 """Actions that are performed on the finite state transitions""" 

35 

36 NO_ACTION = 1 

37 COPY_UP_TO_COMMENT = 2 

38 SKIP_TO_CURRENT_LOCATION = 3 

39 

40 

41@dataclass(frozen=True) 

42class _Transition: 

43 new_state: _State 

44 action: _Action 

45 

46 

47# A key for any other char. 

48ELSE = "ELSE" 

49 

50 

51# -- A dict from a state to state transitions. Each state transition is a 

52# -- dict from an input char to a transition, with ELSE is the transition to 

53# -- all other chars. 

54STATES_TRANSITIONS = { 

55 _State.IDLE: { 

56 "/": _Transition(_State.IN_FIRST_SLASH, _Action.NO_ACTION), 

57 '"': _Transition(_State.IN_STRING, _Action.NO_ACTION), 

58 }, 

59 _State.IN_STRING: { 

60 "\\": _Transition(_State.IN_STRING_ESCAPE, _Action.NO_ACTION), 

61 '"': _Transition(_State.IDLE, _Action.NO_ACTION), 

62 }, 

63 _State.IN_STRING_ESCAPE: { 

64 ELSE: _Transition(_State.IN_STRING, _Action.NO_ACTION), 

65 }, 

66 _State.IN_FIRST_SLASH: { 

67 "/": _Transition(_State.IN_COMMENT, _Action.COPY_UP_TO_COMMENT), 

68 ELSE: _Transition(_State.IDLE, _Action.NO_ACTION), 

69 }, 

70 _State.IN_COMMENT: { 

71 "\r": _Transition(_State.IDLE, _Action.SKIP_TO_CURRENT_LOCATION), 

72 "\n": _Transition(_State.IDLE, _Action.SKIP_TO_CURRENT_LOCATION), 

73 }, 

74} 

75 

76 

77def to_json(text: str) -> str: 

78 """Convert jasonc input to json by removing '//' comments. Line and 

79 number and characters position are preserved to any later json parsing 

80 errors meaningful to the user. 

81 """ 

82 output = [] 

83 

84 # -- Indicates the input position that is already covered by the content 

85 # -- in output. It can be larger than the size of the text in output since 

86 # -- we drop comment text. 

87 output_pos = 0 

88 

89 # -- The current state of the state machine. 

90 state = _State.IDLE 

91 state_transitions = STATES_TRANSITIONS[state] 

92 

93 # -- Iterate and process the input chars. 

94 for input_pos, ch in enumerate(text): 

95 transition: _Transition 

96 

97 if ch in state_transitions: 

98 # -- Found a transition for this char. 

99 transition = state_transitions[ch] 

100 

101 elif ELSE in state_transitions: 

102 # -- Use the default transition if any. 

103 transition = state_transitions[ELSE] 

104 

105 else: 

106 # -- Otherwise, do nothing. All of our actions are in transitions. 

107 continue 

108 

109 # -- We found a transition. Apply it. 

110 

111 # -- Set the state. 

112 state = transition.new_state 

113 state_transitions = STATES_TRANSITIONS[state] 

114 

115 # -- Perform action, if any. 

116 if transition.action is _Action.SKIP_TO_CURRENT_LOCATION: 

117 # -- Move output pos to input pos. This is how we skip over 

118 # -- comments. 

119 output_pos = input_pos 

120 continue 

121 

122 if transition.action is _Action.COPY_UP_TO_COMMENT: 

123 # -- We just entered a comment, copy any pending text from 

124 # -- before the '//'. 

125 end = input_pos - 1 

126 output.append(text[output_pos:end]) 

127 output_pos = input_pos - 1 

128 continue 

129 

130 # -- Here when the transition doesn't have an action. Do nothing. 

131 assert transition.action is _Action.NO_ACTION 

132 

133 # -- Here we reached the end of the input. Copy pending non comment text 

134 # -- if any. 

135 if state != _State.IN_COMMENT: 135 ↛ 139line 135 didn't jump to line 139 because the condition on line 135 was always true

136 output.append(text[output_pos:]) 

137 

138 # -- Concatenates the text pieces into a string. 

139 return "".join(output)