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

41 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-06 10:20 +0000

1# -*- coding: utf-8 -*- 

2# -- This file is part of the Apio project 

3# -- (C) 2016-2024 FPGAwars 

4# -- Author Jesús Arroyo 

5# -- License GPLv2 

6# -- Derived from: 

7# ---- Platformio project 

8# ---- (C) 2014-2016 Ivan Kravets <me@ikravets.com> 

9# ---- License Apache v2 

10 

11 

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

13It allows to add comments in apio resource files. 

14 

15Inspired by the state machine in 

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

17 

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

19'apio system info'. 

20""" 

21 

22from enum import Enum 

23from dataclasses import dataclass 

24 

25 

26class _State(Enum): 

27 """Represents the parsing state.""" 

28 

29 IDLE = 1 

30 IN_STRING = 2 

31 IN_STRING_ESCAPE = 3 

32 IN_FIRST_SLASH = 4 

33 IN_COMMENT = 5 

34 

35 

36class _Action(Enum): 

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

38 

39 NO_ACTION = 1 

40 COPY_UP_TO_COMMENT = 2 

41 SKIP_TO_CURRENT_LOCATION = 3 

42 

43 

44@dataclass(frozen=True) 

45class _Transition: 

46 new_state: _State 

47 action: _Action 

48 

49 

50# A key for any other char. 

51ELSE = "ELSE" 

52 

53 

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

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

56# -- all other chars. 

57STATES_TRANSITIONS = { 

58 _State.IDLE: { 

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

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

61 }, 

62 _State.IN_STRING: { 

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

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

65 }, 

66 _State.IN_STRING_ESCAPE: { 

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

68 }, 

69 _State.IN_FIRST_SLASH: { 

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

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

72 }, 

73 _State.IN_COMMENT: { 

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

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

76 }, 

77} 

78 

79 

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

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

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

83 errors meaningful to the user. 

84 """ 

85 output = [] 

86 

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

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

89 # -- we drop comment text. 

90 output_pos = 0 

91 

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

93 state = _State.IDLE 

94 state_transitions = STATES_TRANSITIONS[state] 

95 

96 # -- Iterate and process the input chars. 

97 for input_pos, ch in enumerate(text): 

98 

99 if ch in state_transitions: 

100 # -- Found a transition for this char. 

101 transition: _Transition = state_transitions[ch] 

102 

103 elif ELSE in state_transitions: 

104 # -- Use the default transition if any. 

105 transition: _Transition = state_transitions[ELSE] 

106 

107 else: 

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

109 continue 

110 

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

112 

113 # -- Set the state. 

114 state = transition.new_state 

115 state_transitions = STATES_TRANSITIONS[state] 

116 

117 # -- Perform action, if any. 

118 if transition.action is _Action.SKIP_TO_CURRENT_LOCATION: 

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

120 # -- comments. 

121 output_pos = input_pos 

122 continue 

123 

124 if transition.action is _Action.COPY_UP_TO_COMMENT: 

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

126 # -- before the '//'. 

127 end = input_pos - 1 

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

129 output_pos = input_pos - 1 

130 continue 

131 

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

133 assert transition.action is _Action.NO_ACTION 

134 

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

136 # -- if any. 

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

138 output.append(text[output_pos:]) 

139 

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

141 return "".join(output)