1#!/usr/bin/env bash
2
3# Copyright (C) 2020 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17set -e
18
19clang_format=clang-format
20
21# future considerations:
22# - could we make this work with git-clang-format instead?
23# - should we have our own formatter?
24
25function _aidl-format() (
26    # Find .aidl-format file to use. The file is located in one of the parent
27    # directories of the source file
28    function find-aidl-format-style() {
29        local path="$1"
30        while [[ "$path" != / ]];
31        do
32            if find "$path" -maxdepth 1 -mindepth 1 -name .aidl-format | grep "."; then
33                return
34            fi
35            path="$(readlink -f "$path"/..)"
36        done
37    }
38
39    # Do a "reversible" conversion of the input file so that it is more friendly
40    # to clang-format. For example 'oneway interface Foo{}' is not recognized as
41    # an interface. Convert it to 'interface __aidl_oneway__ Foo{}'.
42    function prepare() {
43      # oneway interface Foo {} is not correctly recognized as an interface by
44      # clang-format. Change it to interface __aidl_oneway__ Foo {}.
45      sed -i -E 's/oneway[[:space:]]+interface/interface\ __aidl_oneway__/g' "$1"
46
47      # When a declaration becomes too long, clang-format splits the declaration
48      # into multiple lines. In doing so, annotations that are at the front of
49      # the declaration are always split. i.e.
50      #
51      # @utf8InCpp @nullable void foo(int looooooo....ong, int looo....ong);
52      #
53      # becomes
54      #
55      # @utf8InCpp
56      # @nullable
57      # void foo(int loooooo...ong,
58      #     int looo.....ong);
59      #
60      # This isn't desirable for utf8InCpp and nullable annotations which are
61      # semantically tagged to the type, not the member (field/method). We want
62      # to have the annotations in the same line as the type that they actually
63      # annotate. i.e.
64      #
65      # @utf8InCpp @nullable void foo(int looo....ong,
66      #     int looo.....ong);
67      #
68      # To do so, the annotations are temporarily replaced with tokens that are
69      # not annotations.
70      sed -i -E 's/@utf8InCpp/__aidl_utf8inCpp__/g' "$1"
71      sed -i -E 's/@nullable/__aidl_nullable__/g' "$1"
72    }
73
74    function apply-clang-format() {
75      local input="$1"
76      local style="$2"
77      local temp="$(mktemp)"
78      local styletext="$([ -f "$style" ] && cat "$style" | tr '\n' ',' 2> /dev/null)"
79      cat "$input" | $clang_format \
80        --style='{BasedOnStyle: Google,
81        ColumnLimit: 100,
82        IndentWidth: 4,
83        ContinuationIndentWidth: 8, '"${styletext}"'}' \
84        --assume-filename=${input%.*}.java \
85        > "$temp"
86      mv "$temp" "$input"
87    }
88
89    # clang-format is good, but doesn't perfectly fit to our needs. Fix the
90    # minor mismatches manually.
91    function fixup() {
92      # Revert the changes done during the prepare call. Notice that the
93      # original tokens (@utf8InCpp, etc.) are shorter than the temporary tokens
94      # (__aidl_utf8InCpp, etc.). This can make the output text length shorter
95      # than the specified column limit. We can try to reduce the undesirable
96      # effect by keeping the tokens to have similar lengths, but that seems to
97      # be an overkill at this moment. We can revisit this when this becomes a
98      # real problem.
99      sed -i -E 's/interface\ __aidl_oneway__/oneway\ interface/g' "$1"
100      sed -i -E 's/__aidl_utf8inCpp__/@utf8InCpp/g' "$1"
101      sed -i -E 's/__aidl_nullable__/@nullable/g' "$1"
102
103      # clang-format adds space around "=" in annotation parameters. e.g.
104      # @Anno(a = 100). The following awk script removes the spaces back.
105      # @Anno(a = 1, b = 2) @Anno(c = 3, d = 4) int foo = 3; becomes
106      # @Anno(a=1, b=2) @Anno(c=3, d=4) int foo = 3;
107      # [^@,=] ensures that the match doesn't cross the characters, otherwise
108      # "a = 1, b = 2" would match only once and will become "a = 1, b=2".
109      awk -i inplace \
110        '/@[^@]+\(.*=.*\)/ { # matches a line having @anno(param = val) \
111              print(gensub(/([^@,=]+) = ([^@,=]+|"[^"]*")/, "\\1=\\2", "g", $0)); \
112              done=1;\
113        } \
114        {if (!done) {print($0);} done=0;}' "$1"
115    }
116
117    function format-one() {
118      local mode="$1"
119      local input="$2"
120      local style="$3"
121      local output="$(mktemp)"
122
123      cp "$input" "$output"
124      prepare "$output"
125      apply-clang-format "$output" "$style"
126      fixup "$output"
127
128      if [ $mode = "diff" ]; then
129        diff "$input" "$output" || (
130          echo "You can try to fix this by running:"
131          echo "$0 -w <file>"
132          echo ""
133        )
134        rm "$output"
135      elif [ $mode = "write" ]; then
136        if diff -q "$output" "$input" >/dev/null; then
137            rm "$output"
138        else
139            mv "$output" "$input"
140        fi
141      elif [ $mode = "print" ]; then
142        cat "$output"
143        rm "$output"
144      fi
145    }
146
147    function show-help-and-exit() {
148      echo "Usage: $0 [options] [path...]"
149      echo "  -d: display diff instead of the formatted result"
150      echo "  -w: rewrite the result back to the source file, instead of stdout"
151      echo "  -h: show this help message"
152      echo "  --clang-format-path <PATH>: set the path to the clang-format to <PATH>"
153      echo "  [path...]: source files. if none, input is read from stdin"
154      exit 1
155    }
156
157    local mode=print
158    while [ $# -gt 0 ]; do
159      case "$1" in
160        -d) mode=diff; shift;;
161        -w) mode=write; shift;;
162        -h) show-help-and-exit;;
163	--clang-format-path) clang_format="$2"; shift 2;;
164	*) break;;
165      esac
166    done
167
168    if [ $# -lt 1 ]; then
169      if [ $mode = "write" ]; then
170        echo "-w not supported when input is stdin"
171        exit 1
172      fi
173      local input="$(mktemp)"
174      cat /dev/stdin > "$input"
175      local style="$(pwd)/.aidl-format"
176      format-one $mode "$input" "$style"
177      rm "$input"
178    else
179      for file in "$@"
180      do
181        if [ ! -f "$file" ]; then
182          echo "$file": no such file
183          exit 1
184        fi
185        local style="$(find-aidl-format-style $(dirname "$filename"))"
186        format-one $mode "$file" "$style"
187      done
188    fi
189)
190
191
192_aidl-format "$@"
193