From 55ff7ac20c8164350d0a9b86ab375d0ec9f4c94e Mon Sep 17 00:00:00 2001 From: Peter Shen Date: Mon, 23 Aug 2021 10:21:22 +0800 Subject: [PATCH 01/43] #18 Update action description --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index e4527c8f..07b27881 100644 --- a/action.yml +++ b/action.yml @@ -1,5 +1,5 @@ name: C/C++ Lint Action -description: Automatically checks push & pull request changes with clang-format & clang-tidy, then posts a comment with faulty results. +description: Improved automatically checks push & pull request changes with clang-format & clang-tidy, then posts a comment with faulty results. author: shenxianpeng branding: icon: 'check-circle' From b5a6a324dbd1646bc4953fcf98a1a4b9320a0444 Mon Sep 17 00:00:00 2001 From: Peter Shen Date: Mon, 23 Aug 2021 10:25:03 +0800 Subject: [PATCH 02/43] #18 update action description --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index 07b27881..e4527c8f 100644 --- a/action.yml +++ b/action.yml @@ -1,5 +1,5 @@ name: C/C++ Lint Action -description: Improved automatically checks push & pull request changes with clang-format & clang-tidy, then posts a comment with faulty results. +description: Automatically checks push & pull request changes with clang-format & clang-tidy, then posts a comment with faulty results. author: shenxianpeng branding: icon: 'check-circle' From e0e93bf84007b13f769301601ed5787cf76331ad Mon Sep 17 00:00:00 2001 From: Peter Shen Date: Tue, 24 Aug 2021 20:03:35 -0600 Subject: [PATCH 03/43] #20 add imgs & update readme and workflow file (#21) * feat: add images & update readme and workflow file * Create LICENSE * Add badges --- .../workflows/{test.yml => cpp-linter.yml} | 7 ++- LICENSE | 21 +++++++ README.md | 52 +++++++++++------- demo/image/icon.png | Bin 0 -> 4348 bytes demo/image/logo.png | Bin 0 -> 4806 bytes demo/{ => image}/result.png | Bin 6 files changed, 57 insertions(+), 23 deletions(-) rename .github/workflows/{test.yml => cpp-linter.yml} (79%) create mode 100644 LICENSE create mode 100644 demo/image/icon.png create mode 100644 demo/image/logo.png rename demo/{ => image}/result.png (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/cpp-linter.yml similarity index 79% rename from .github/workflows/test.yml rename to .github/workflows/cpp-linter.yml index f0dc8922..19f21fb9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/cpp-linter.yml @@ -1,4 +1,4 @@ -name: Test action +name: cpp-linter on: push: @@ -6,11 +6,12 @@ on: types: [opened] jobs: - test: + cpp-linter: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: shenxianpeng/cpp-linter-action@master + - name: C/C++ Lint Action + uses: shenxianpeng/cpp-linter-action@master id: linter env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..b049769e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 shenxianpeng + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 0a7c0dbf..30ade98f 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,22 @@ -# C/C++ Lint Action +

+ icon +

-Github Actions for linting the C/C++ code. Integrated clang-tidy, clang-format checks. +# C/C++ Lint Action | clang-format & clang-tidy -## Integration with GitHub Actions +![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/shenxianpeng/cpp-linter-action) +[![cpp-linter](https://github.com/shenxianpeng/cpp-linter-action/actions/workflows/cpp-linter.yml/badge.svg)](https://github.com/shenxianpeng/cpp-linter-action/actions/workflows/cpp-linter.yml) +![GitHub](https://img.shields.io/github/license/shenxianpeng/cpp-linter-action) -Just create a `yml` file under your GitHub repository. For example `.github/workflows/cpp-linter.yml` +Github Actions for linting C/C++ code. Integrated clang-tidy, clang-format check. -!!! Requires `secrets.GITHUB_TOKEN` set to an environment variable named `GITHUB_TOKEN`. +## Usage -```yml +Create a new GitHub Actions workflow in your project, e.g. at `.github/workflows/cpp-linter.yml` + +The conetent of the file should be in the following format. + +```yaml name: cpp-linter # Triggers the workflow on push or pull request events @@ -28,30 +36,34 @@ jobs: with: style: 'file' ``` -## Optional Inputs -| Input name | default value | Description | +`GITHUB_TOKEN` - Provided by Github (see [Authenticating with the GITHUB_TOKEN](https://docs.github.com/en/actions/reference/authentication-in-a-workflow)) + +### Optional Inputs + +| Name | Default | Description | |------------|---------------|-------------| -| style | 'llvm' | The style rules to use. Set this to 'file' to have clang-format use the closest relative .clang-format file. | -| extensions | 'c,h,C,H,cpp,hpp,cc,hh,c++,h++,cxx,hxx' | The file extensions to run the action against. This is a comma-separated string. | -| tidy-checks | 'boost-\*,bugprone-\*,performance-\*,
readability-\*,portability-\*,
modernize-\*,clang-analyzer-\*,
cppcoreguidelines-\*' | A string of regex-like patterns specifying what checks clang-tidy will use.| -| repo-root | '.' | The relative path to the repository root directory. This path is relative to path designated by the runner's GITHUB_WORKSPACE environment variable. | -| version | '10' | The desired version of the clang tools to use. Accepted options are strings which can be 6.0, 7, 8, 9, 10, 11, 12. | +| `style` | 'llvm' | The style rules to use. Set this to 'file' to have clang-format use the closest relative .clang-format file. | +| `extensions` | 'c,h,C,H,cpp,hpp,cc,hh,c++,h++,cxx,hxx' | The file extensions to run the action against. This is a comma-separated string. | +| `tidy-checks` | 'boost-\*,bugprone-\*,performance-\*,
readability-\*,portability-\*,
modernize-\*,clang-analyzer-\*,
cppcoreguidelines-\*' | A string of regex-like patterns specifying what checks clang-tidy will use.| +| `repo-root` | '.' | The relative path to the repository root directory. This path is relative to path designated by the runner's GITHUB_WORKSPACE environment variable. | +| `version` | '10' | The desired version of the clang tools to use. Accepted options are strings which can be 6.0, 7, 8, 9, 10, 11, 12. | ### Outputs This action creates 1 output variable named `checks-failed`. Even if the linting checks fail for source files this action will still pass, but users' CI workflows can use this action's output to exit the workflow early if that is desired. -## Results of GitHub Actions +## Example + +![github-actions bot](./demo/image/result.png) -![github-actions bot](./demo/result.png) +Example comment is [here](https://github.com/shenxianpeng/cpp-linter-action/pull/5#commitcomment-55252014). -Behind the scenes, this is because this repository has added `test.yml` under `.github/workflows/`. When an unformatted C/C++ source file was committed and create a Pull Request will automatically recognize and add warning comments. +## Have question or feedback? -Please feel free to commit code to the `test` branch of this repository or create a Pull Request to see how the process works. +To provide feedback (requesting a feature or reporting a bug) please post to [issues](https://github.com/shenxianpeng/cpp-linter-action/issues). -For example, this test PR [#6](https://github.com/shenxianpeng/cpp-linter-action/pull/5) and github-actions bot comments [link](https://github.com/shenxianpeng/cpp-linter-action/pull/5#commitcomment-55252014). -## Contribution +## License -If you have any suggestions or contributions, welcome to PR [here](https://github.com/shenxianpeng/cpp-linter-action). +The scripts and documentation in this project are released under the [MIT License](LICENSE) diff --git a/demo/image/icon.png b/demo/image/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e6cc0e81442efeef562016560c7a4fc1b387e190 GIT binary patch literal 4348 zcmcgw_g524utfnSbVCnKAOQja0|e4e^Uq~j+@kMvL? zMUke05dsPbNY{t&e|YbQnK^gP&N*|=-krU>@wZG+O!OdnDk>@_eY6hdA`bsEx+@p{ zl~sM=MYt4zK_RK?C-^ol0F9fLu@)6o(@TakC)x`P@IzY%P*E`x{@JA=-)d(nD%Mne z9WC=<`yT~>6!ux3AujeCs=z4JPA}N~krdn&%Y&Q$ahIlIMrQ`E$0onr1Wb%4z2h#x z{;=19WM%0ANah)iRxBS2Id&zCM}NgRGICzixvvVhrz?%(`$_&g$CCf!IJ7b>$TcYW zZv(}l1r_1jv2q%*bl&06Q!j#LMbZiVf4L2|r9kT3#kur6%mj6omxFKwr2Msqhwpr) zjEmsQax3iG`m(U(Y-kq@iT>eq4sF*?-#i;OP`Xherq==ckONC#!>Ax0D~btSvOf^T zS}NA&iwI%OHP{5-xWW@PhVtmtZB#w_4i*f>g=UStogUJePbtTqE=N3mvC-#so+V9{W zX?lePrWNQ2-)12=U}*%+*}1tJnW%LTer1_p7JAuVa8()8m07YnlLvjtf;X zPzD!vWxx5}StA9hbeJPsarS;{NsX$?TrLBAYgTTe_U(gbI&AOr+z}lb#C)tpdCuDu zO4Lt~oGVfNSEjB=vpc!CG9x@Ti>_mfmvwOMdRI+D!wPfQA?Laf?r<6*$7SYKygeep zo?loZQXkM$NLTBcQ8;b94Y<5MAx-Y0o@O%;ib~HzwBcq=ib9G z(gET>Vsqx-E8Xp`5%FIO);6ZmUGLQ3%u~6K0*JLSo45qUT-%;w{DgX?)jqa+3~S|< z%??@_sM<*J8grYTj`PX5!)$ba)T5W=*p~Ibw9tBJc~P0%-gI?Ut%_R~udb!Ug6xSK zxHq>>DhnSH*GDGn5oxCirX-*u**ArMazjPMj0=AI8$-hi(rDD1EpfLyfhL~(;Gq)u zqQO91Wb~0(af_%Ct}&0D7X&80M%U_#H}Jdo{CXOTssn*Q57i$#RoF}p)Vz09yT;x= z@fTPyP5*vH=nhq)d+2H%2lbU%!ju)X-qEojrfku+*gL!=$l8v!%3NO_aJHr_c=NGI z0$Q;qu<^fSLYx=J>NobU7Q%mmp|$QhWu)i3;V*-GG>lQBrM4F*Xl2kqNp2RrlfLQQ zth^wqf318@9}K*P8(U1AHp({KOX1Tluf{mm<8z*l4tZ=&iQtFKtt(^iFRhutJM&Vh z+Sgte-q2048Co=Z){a~`BittJGZ5dopFc$Lpwg)unOmBed7Z=6b)ph8B0g9@cqmT% z;~aAwunB@j%_!d=Oq}N!wCNbw*NVnHF5bl$^+eIEEG@G2oiJu=%*sD%eF}NE+Ei3G zy@7t{EPahwGumap_Z^scWTdT4=*H;HlN2K;PVmz_9`YpQ!_z`j>aJhPw(zt`|E4Th4M@=?r`p(0Kx`vPEKy%Rgs@#tAnx&4UT^hAHRgu+=h?Y+Wl_yL zSLBb6Kd>E~y@7el_-y`fJvdSV_LfK4@ZK13%tu9ThN%}J^iviZlTvufU*#2Fte>{} z7HE^z`^9t{kb7KTjzESc3urr@X^J?f^05e&!?v&DPT4=6u9Si_ ziivm#hEJ&I(yGIzF^w@g`Eb`dD_uITy{g|BUp(s4^L)R5(I;j@!@v{^>(J~UeNkwQ zva7l)^Mtz#n3o6p_C^K+=-qeq6p-Qx1)yG5NJ&X8psnVo-WM)T82zZ>R&QkOae6W; zAfgLw&Wwcr@v=b%M?W2{0h-;!{U?_*)|9(JpgYeJG)$f6cCVCAE7F&unzpr(4UPtf z$^qlCg-wK9UqR+QwV3WcpSjL8u6w4$k4?AsW`2WFwNxWtJ_S^u9f%OWnZAH2vFewb zq9*x@R)2JBc@V#oHsVusOJYN8A8dNSSHx_`*Ol{jj-hETQZt?=NzSAxBO7>yCW_h3 zIz=mPH~8$u{)uL3CwGz_YNSU$^Pb+;l`a`>N;ehpvHyh1$NIfQdEZZefI!?ZG3R+o zFu!Ncmwo?I!mhkhGDxIxsRt>mN~uW@__8f1>@Pa{wfvJNb)Gd+7PKr0`%l^EJ72ti z$vwTh_2(wlc@K-0z8@vnSfJ%Ay;oAHTw^%A!zLuds`CzRS*xBR=@~(aQ?rupCD^s{ znrS6oGQlI`fq`InqFH@ec^uxceFeJv<(qOumhc_F{i8|@OKr+^SLuf5;0$vH_nE5J zD~I%BZTe`7T$KVS+}@s=y5yieCUbSzKNXlAyTyh7y*7ctzX)-*tV@8|U?wK}Q&Ng$ zV*8J@+#DvA0y=V3nFbk71|fIWEuXAC?r>>06DuVOpZ~OS zj5u}>2#qqAMYHY+Xue97!@3-%G~#iGzpZJ}K)!ej#G>yZ5&N;RG0YA~mgzKuBt`Ig_$T}LFM zuc*)kG>G5N8uUgIKcT_R>Fin(Omf<9=Ni>J?(`$?$K;B;O1-i>&Fr3VgPf<{+7^>Z zpVe2aK}>x{xhGM|#vm}nc+G3yx=N``zZY|hB%74Ci`B6ng`p!AosVJVMX73~bJaZk z%~C*4&?ae(*%e;wxeTOR2~Wox6v-3>%?za5@O$((-!K%fLELNlIB!#SQf->fg$wa( z5{s~w^OJFDpNL>kyCZJoD>@@bSZWI$WzAxFGMkuj&HW8S+m!>-7hskGn?Sxg9_n&q zHqlO%mUn@Jb7tsH;mIxsrCoee^93Vl8{>Qb1W+)J#U8}JvvYgAHCr@2ZfBK4>i*S6 zD+Y|;y0V%VFFAAaYuyc~Cjr3yk*$#V#|!{)&TVixeUZA#RuW;xb*rv_k(GEAJnY-n zfq8c-lt~9%%Qm=Q8^uF^f+rl&nyCHWq2;lx7)Khh*Ve>azFvG{?V@7<8-j@3-O0Zo$I@|UWcwU*J8=fNHn z*sw@T(OBDCJhO`E02lwvxYBY5rnQN$N?%y%;^M86NZP0>gW!vY_e8d9T_?2_Ly1|X zhLbj%aKoJ{th1B3UR-?*i~DuhuHQtr$}huzAjXb z(OjMLN~#&LYf5b_ea3uhH?ZRr*v4)ttCz^#)Mepd4*>#9n9MzJsg>@X#V;3L8(Q5) z!~B9``&NEm3`;-HHu7+^oPm1L&kHSZh@p(uF+DNqLTascef2QX=n@ywB~V&z5qnot zGao(`8OUJY*NP&dkKt)d9f3&;J^1j#hQ7M*#X{;-#Is|56NE0!CelvlNvgS~z{HvBByer(4bCxohwT|b0AC7)mvJz$)}NJ z!$~|NpFd4XM!Ypd8ZcA7mb7;4nvr5VQ-q(>SeGO4Mbep#rS~A|3&Q$;-axMTnQ!ET z^J{3NXYt)-DtJXJc$tl6hTwm}e^$zYNDFluE2gGci2qfjywm()8EL%`X!D4;$9Eyz z7dnd%yvT#blx z-PjM8UB!UzdTd*&H+R=>y9|f4wHs92?0D%ZtWON1YkC!+y6{sHJj4Vvs0AQ?W-bph z7fW-06~pMvM*65QF1(p(8Sn1@B^mOq)e|Dkz{Gk zoPq%v3{8!mUo8hp#1{k7sA4?~lWWRlzthB>u6?{EUY{+DmVX|@yx_DV!M&-z$C}E1 z=k`AtHh$u@z@ZsY*U&I|8aP+p47c0LoJ+fgTla>mGbkQ25WwBZ9@;1RE1iK)URT(x46>-7_2r|=8r z-n!wcj!#SU7Ik4x^pj=ZpFi*p1$n02GrSB8025XDL#JD$Ljd1!DC#uv4lvtvD*dHO zT-t*S_0goERi%~Ng*Oy`@I-IJ71WAMPbZMWiJyJ48idLf;0L;Jbdjm44ms|TrBEe7 zhTz=jR>ZiJDEr(5dCfpP?Det0u>tZCF%!xyx?QLK!l=KZxVXxm+G&Qm>FltC*J&{V z8LfD37TxR+ot(KX3RZwmC=_?zxa3B{;QX(o?L2xS5IqSvLNvsmLJ@4Cd_4`BE(sD( zCPRm1s5x9(0DxqkIvQ%z=p6bG#KvPGsFB!N&=cltV+c4R1sq1WMRv9QSn|sSqMxWT z{s`affn`s=U8cvB^=HHmC$(NTGO8u0{olpjQ9GN_L8Zx+cB%rVy}@MmE$F<87kNv; zsetIdWH|amTBAmb`0X~B2!O8{!^zE$%Cf9Bcg`Pck^>)hl2D^;avWknG9&lzp!FmK zHJ2_r?n7~czw>7HaTs8O&2{H`qAIOuP)XF?L0xMyf?}s$rBxe)2Fub7-lPVnPn{oy zeB&fsov}?(lI1Tj$|CC)V)rHCr_zcL6`B#*<$af(5E~e$3a>^{Qqd&Vq;HbVAN)x$ n;R6L5^ndnXA4%`%_)C4D9Rd0LB?ou$V?m{_Yob$+bd35RDI@X% literal 0 HcmV?d00001 diff --git a/demo/image/logo.png b/demo/image/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3a6aaed0e97ba878883be0333094db910a2b7e83 GIT binary patch literal 4806 zcmbt&HMowB%AtcPy|VDd8gBDg3CVQ@T_7 z+4sZy4?NF@x#ylaXU>@qbFR6riPY6rAt7WS1ONaeYN|^57(a+HJpydZ%Y>->ig8#T z`YLY#m7`2Mm;lEXqy+*1YG6cn5L`_B)K%5k0{|fH`hSMi=TiC+0H6e`DS-@p%@4Ak z8om2K(?>-BHo^sY#~bRAQ!52gEJPPf|A^91DVf^+rdjSaIqu>)iJX#l?$YcVl{5s? zB7ZcmvF-8@5Y!I7q<(r!DIQck%6@gaq;dLvjKSb{+Y$6KD@lxf;4vp)v_)do+uPfn z0H-G}^ihM22fzUg#%4d|;35CtfBfn;rlx0u1UtX2XeG?lp=!-^0tuVEj@^`5NGX;A z%^xA#gPS*;_N#iuv)e~$POol6x30RmR)1{08se|2yo1$jA(cq_y%;`vw|fZZmV}#j z&X4XTe+aMkomls{5FF|E8gE3)cvIw-M45I1R_X_cK35z(l^3tCXPcQAy96GdO(pyF zzaS>Q5vYobi~ITWXH%2pKM5dEhld)8M4A{I`}z9f;NTb;8Liw7yeQx~7%D1vTGTnP zm3AzjzkU7E_yxV)Imnz1u{Tq-+zM?*p%@~bue$#dwHYQLC7m80?_d`X?1$Z|+%WI@ zac4+7d1MxL|K&&(ir^|1buRlp_#Nz{|&J=5;k}^wAPEUtJ@pgB2L-5HqPkig^>xDGdcp?f53Ticp7qB8E zvo1&Do(#-%;g+EzOqSZM9?=T=E+m#414M%YswT8AP1^${xVX6LZ2w%IoYXWlyw~rH z#NU9&$CI@@hP9-qrB8vSiWKv1w|Sv0<-()1T41x1bbfysTo&DR(ye!HR) z?bg~k;oQIRE!E3Fg_|$qTyGj^bh3y0@1HdPc3x=0QtymQQ%mA%lP_-k% zTS&0EC2J0BXz`%6-Qm0J|ng#`(4--JAf z);&-^K0o{%3A`)QaflLeP+zeoudhd*`Eq64Xon0ldB!ZqUzqbSO(1d6tFERb0#nI` z)m?ALx{`KYDPuC?&7&F4hcVfKN+#x*0f)uLDmSF0OkbaEU7VlC&uci-79VJ!I1I;m zJ(3@!10$n5hUT%~^JVVoY$=7&BECx-r7Z7zg$wu`U&q#PUcR8HZyafc7S*(rSNcpf zN^jIt_fW(-32vQPRc>%k{uEy&e_~CN-Q&IfL|`%5;;Pf-z0*Jj{$mGe#MxqZz0l9< zuHM6L4WB4iSwN`&Bh6bP??8$l%m>H3mJwaTt5@pJD346s+}un}nY<;l!iZ?q?TK<$ z%Bo94elRL5_~!fuo*?%RSjnymf5Ow#>6w{h_m^B7CD$8$V2U}mQ#1e)98H|BYX9^m zm!cGjtp+_38omu|QU*LdHhI4!AINvhUf%HSKLo0)Y?Xbygxz9o4BTl_!Yh=*=6CGJ zS01i)2cN^vkuVU9yp!>|CHGm`rMc ze_-KyL&TeIf!9BZI47~TYQXj5lSnu&qg5;ak+NRmOMx4)%>cSqw5{%o&kD4f3zE-M z=ThE!KHj`Xp$u{GKAXgsblEz$UM_8E?DwwQwQ;gnm6q~2My76RX=&APj*>I7W|vWT z)dwA9#bt&K^sa<9?UoD zFdZgL?sj(BH%ik_{SlI#(5l2*zJa2Acg_V8h8HzT32z_i-KqGfK~3gU?#U~ZqG$=~hrf}$8xLOWI3 zyDJ`4rJPQ;-Y>^E62&X|c=sm})KBk5Otq^%_c7=dt|-^*-d(r8*`De_p-__s{)%HA z9avx{(&2DM>(NOcpaw@C`NQkYeMr@(FFBvXtL=OWJX>(v@#_}vq0ELP8`IO%98cWV zdt+;?`?RyApWP-Z(7aCbPx9C7KXZ7iX>2E&;@@WZZO)Dn*XUQr9b7m43cf`-`z+!S z{;}Rurz}ZxC;fz#o?W;l;qPbk<{b$2V-914pT+B$6KY#PNTIOXR>&gZz+#fy>7t2i2LN%F6SN~NDWHPA6Q-@@64G}YC zfIerS8C(g>334T>Sr>=O%O$gD;<7EU1c7CW!M(k`=v|xSpyFaKjqq>!#9(4Ju%kDL zZsPtH91h2<2>VZ;!l1&k1N_l7xN=MA_u1So=@IaN9(n`m97kzN@Nxf}n}mp6;++W& zIWfY=;qOW_66ro}$z72zM-XeSdbgQEP3b35Hd)-K4nt`twqBojmL9juPPa^8*XVJd zjB8b!nF}Ko19#zvBp1ZS9W)=byNfxh);Y%6i-_^UWCakGJ@uiyDV?T`|x0nvN=i-$zVB)YfmztpC1Et}|!oLx^W4%vhQDtt~N*i7Gz?6~n0-3v>+QmeRE zKy=VNVYQAhJ3RASNaY!}kDp!0KSU>BWrQ2o*+5k8ek&`V--sx4r%q?-AI-6H=~{xi zf*j?ORw?nVWawZEya^V|ucoE75GZ@h$W6cPBbQ;)PL~Y0XYl{xF`l4c+)F=u-{B|i zjn>`d`QdY!x=eY|1YhabDm#U`?7n`vx_c3x{ zM9~|0S)6l)N?GJmG#0!UjuG=rb88imlPfDh48*jh5R&a`&z6k@>y&sPcOH7iP7|fc zJES4(*aBwku@IGT<7wvxX`s~5CkUjYqCF~l$LhNk&$@z94B|8M9W;YC*$$Xbf|@+vfo4hvzm6Qwjwv z2doCken^q6rDXWcYrX!Ul$ksbWq{Fe-C3=pYkGc3@Q1pz_HENuAuJhpNCPPei2(Se zd9OD*cz?<$VBJr|8y!Da60H6!uN8YXX*j#6p}3EJ)~b9@<(;(7)>!mFZT~D*!2Bv- zASXN!dy_#nJ~tO8C~oN)UagftMa*r|Dx^RfSQ}uLOa>oTGuQGM+Ds*I-KK&#p=(pTZ>SRwG=!+~>+b*&_H88qs zy-1UfN&ay0E;KwmM=GR|>+b%((P=*1(U`Pk2_=wZGMVg)A!Zi^nteAP593@7wppLR;Pi%2*6SD!HIY~!=Sw>sMuDLB2d&)x+m+#48CGkHu%>eB!nt~MB3PQi_Xg(M?bP80cxFDx_29v6U4(ql-;7B zGdg;{Nl|*x9Z7}W!7?*^n4x;l!qRqL?xrEp^zU(WoOYQlwW6Y;TIg;t$nR_u5*-_# zFhM&X7XtbQ6)uPq_s!Oys~s1g+ZLfB9!O94JOBH;)cZmS$y*HpqwinaNLh^4!E9m7 z)|*4l_FZ3um-W*&jQR-b@NB!KTNIL|nl3hr0`NC}y$;;Bx8r|dLCBK2=OVnj z99SN{r$&u9;)-^ZwFSraYD?&vEC4%ZwQ2m_ag;d53zVbz_+0Iv1jj|omHEig zFInGX%Rf1?US7V|O3KQ(ltr;5Ejce))7Y#A-97dO`7`%GZ%tksifV=E;iZ@>?w97` zgq-}8I(%LG^=Q(o6MObFXiXL+z+HzZ`dx-7ZZT6d?n#rk3vEq@)f7Ht)@&KQ_}ujH z`-6O^(3?MwF{`>AM39fda@&Zq3h`w;?wqL2o6N8#{4F>5Cs?p7&%mHdJGSezgpX+a z(&(Q`!FvleL|#4$J^{%?K0Ruy8mP{>bbr2qi|G~Q4Z#CJIyySoWcD9928V`V%rZ3e z^pQ|u5D27^GNz$Y722Zq!I=o9t>!AVz>264Wm}$v{!rD7`_|alIH|HUKJIqDJr2FS z1UYx*x9Wnyw-+-O7zkFk=VxOB1f%_w7343fsdMXGnH`mXUd$zongh0PyL64Oc)+YHUv~L{*`{jFL^>dCCrfXu7k)EEOl9G~^mX?$>G2z|R+$^)9 zTy}uf3BU+lth~hP>gptJ9sQOZY7CwHB4@!vEz&kMLa14R(Q20$2UDGNkajj5bAtHM z``fGh!ca}F3$vJ|mHmBUvy$B0W&Wz41TJ#GR9f8Ubrlr? Date: Wed, 25 Aug 2021 10:20:51 +0800 Subject: [PATCH 04/43] #21 Fixed license not specified --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 30ade98f..f3732568 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/shenxianpeng/cpp-linter-action) [![cpp-linter](https://github.com/shenxianpeng/cpp-linter-action/actions/workflows/cpp-linter.yml/badge.svg)](https://github.com/shenxianpeng/cpp-linter-action/actions/workflows/cpp-linter.yml) -![GitHub](https://img.shields.io/github/license/shenxianpeng/cpp-linter-action) +![GitHub](https://img.shields.io/github/license/shenxianpeng/cpp-linter-action?label=license) Github Actions for linting C/C++ code. Integrated clang-tidy, clang-format check. From d5764b0d2d2fe07a219309a19dfb34a349bc6b26 Mon Sep 17 00:00:00 2001 From: Peter Shen Date: Mon, 11 Oct 2021 06:32:56 -0600 Subject: [PATCH 05/43] Ready to merge master-py to master (#27) * Parse better with python (#22) * upload new demo image * Revert "upload new demo image" This reverts commit 1aff6c70c69acc366cdddd69358a173a502fe31b. * update readme & upload new demo pic * avoids duplicated checks on commits to open PR * fix workflow from the last commit * Revert "fix workflow from the last commit" This reverts commit 778e10dc42714dfb46a2ef1065162ac4969ab549. * create python scripts * output event payload & pip3 ver * echo pip3 list * switch action to this branch * echo pip3 list * install pip and list modules * typo * pip3 install requests * switching to only python * should've read the docker docs * switching back to ENTRYPOINT * typo * don't upgrade if files exist outside /use dir * use python as an entrypoint * add project.toml * bad syntax * try again * hard_code version in setup.py * try pip install * only upgrade pip * test on source files * headers are dicts * test on source files * fix posting comment using requsts * fix passing verbosity to CLI args * action uses current branch for now * parsing everything; no diff action yet * show all debug in logs * use bot's id not mine * double trigger action * auto-verbose logging on repeated runs * show me then run number * support sync events in PRs * switch to mkdocs * fix bad indent in yml * install pkg before documenting * typo * compatible w/ windows; add diff-only input option * diff comments working on PR from clang-tidy advice * rolling back diff comments; update docs * use bot id * disable diff-only in demo * Update setup.py * try mkdocs gh-deploy * use a gh action to publish docs * oops. ignoe release only condition for now * change doc's favicon * [no ci] publish docs only on release event * rename docs CI workflow * prototype badge * [no ci] augment doc build instructions * update readme & demo picture * [no ci] pub docs on release * update docs * disable mkdocs CI (switching to rtfd CI) * fix some review suggestions * update badge in readme * Use lazy % formatting in logging functions * fix more code-inspector notices * slight refactor and switch to pylint * fix pylint workflow * run pylint on PR synch events too * Tell code-inspector to ignore python srcs * Tell code inspector to ignore mkdocs.yml * ran black * self review changes * Update .github/workflows/run-pylint.yml * add gitpod badge to root README.md * try to fix verify_files_are_present() * remove
to auto adjust and easy copying * fixed typo * solution to #24 * using check=True causes #24 * log non-zero exit codes as warnings * warn (in log) about no git checkout * Update action name Update the name to make it easier to search in the marketplace when users search with clang-format or clang-tidy. * Revert "Update action name" This reverts commit 4a41a5e84291fa5c3599dfcd6df1814cad4ea010. * Upadate README.md * Check all files (#26) * initial attempt (#25) * change test action to my branch * adjust workflow triggers * try log grouping with logger.critical() * change logger's format * don't upgrade pip; test some new features * test fake submodule; show me args.ignore on boot * considering alternate fmt for ignore option * remove fake submodule (it worked); * action inputs can't take a sequence; delimit by \n * specifying `--ignore` better * fix exit early when no files found * list_source_files() is malfunctioning??? * Revert "list_source_files() is malfunctioning???" This reverts commit 59522e0e031ebd6c376e9d73df868e4815dc4ff4. * is ignore option causing malfunction? * show me what paths are being skipped * show me which paths are crawled * show me which files are considered as src files * show me comparison of ignored paths * change done debug statements * fix debugging statement in is_file_ignored() * skip comparing empty strings in ignored paths * maybe a bug about ignoring root dir * try getting python latest from src * python src build needs deps. revert to apt build * try using tojson() * bad yml fmt * try a different json approach * make yml array an explicit str * try forcing it as a py list * abandon json idea * try new log_cmder and !ignore prefix * need to separate a single str into multi args * use pipe char as delimiters * slightly different approach to passing ignore val * might need to switch to 1 line of input * how to handle spaces in a path among multiple * no need to escape quotes * fix debug prompts; and workflow-triggered paths * fix cpp-linter-test action's triggered paths * check action stil works without using new features * try log cmds to annotate * try abbotations again * use a long unlikely string as default ignore * try annotating with line and columns * try \n with minimal parameters * does file need changes to show annotation? * use html
and line's columns * trigger annotations * adjust annotation's output * try CRLF * simply can't use mult-line annotation msg * almost ready for PR * use proper casing in chosen style name * adjust last commit for GNU style as well * remove artifact * we don't need the `re` module anymore * new thread-comments option; update docs & README * avoid duplicate clang-tidy comments * [no ci] proofread README * Fix indent in root README for mkdocs * Use sub-headings instead of list points in README * reviewed errors in docs * switch test workflow to upstream action * Update the new example yml and remove old demo link Co-authored-by: Brendan <2bndy5@gmail.com> --- .ci-ignore | 2 + .github/workflows/build-docs.bak | 32 + .github/workflows/cpp-linter.yml | 9 +- .github/workflows/run-pylint.yml | 29 + .gitignore | 185 ++++- .pylintrc | 433 +++++++++++ Dockerfile | 10 +- README.md | 141 +++- action.yml | 66 +- demo/compile_commands.json | 12 - demo/compile_flags.txt | 2 + demo/demo.cpp | 8 +- demo/demo.hpp | 9 +- .../python_action.clang_format_xml.md | 6 + .../API Reference/python_action.clang_tidy.md | 3 + .../python_action.clang_tidy_yml.md | 6 + docs/API Reference/python_action.md | 3 + docs/API Reference/python_action.run.md | 3 + .../python_action.thread_comments.md | 3 + docs/README.md | 20 + docs/images/demo_annotations.png | Bin 0 -> 24468 bytes docs/images/demo_comment.png | Bin 0 -> 25979 bytes {demo/image => docs/images}/icon.png | Bin docs/images/icon_large.png | Bin 0 -> 33322 bytes docs/images/icon_large.xcf | Bin 0 -> 110626 bytes {demo/image => docs/images}/logo.png | Bin docs/images/logo_nobg.png | Bin 0 -> 14183 bytes {demo/image => docs/images}/result.png | Bin docs/index.md | 26 + docs/requirements.txt | 5 + docs/stylesheets/extra.css | 59 ++ mkdocs.yml | 69 ++ python_action/__init__.py | 103 +++ python_action/clang_format_xml.py | 162 ++++ python_action/clang_tidy.py | 111 +++ python_action/clang_tidy_yml.py | 143 ++++ python_action/run.py | 725 ++++++++++++++++++ python_action/thread_comments.py | 260 +++++++ requirements.txt | 2 + runchecks.sh | 19 +- setup.py | 39 + 41 files changed, 2631 insertions(+), 74 deletions(-) create mode 100644 .ci-ignore create mode 100644 .github/workflows/build-docs.bak create mode 100644 .github/workflows/run-pylint.yml create mode 100644 .pylintrc delete mode 100644 demo/compile_commands.json create mode 100644 demo/compile_flags.txt create mode 100644 docs/API Reference/python_action.clang_format_xml.md create mode 100644 docs/API Reference/python_action.clang_tidy.md create mode 100644 docs/API Reference/python_action.clang_tidy_yml.md create mode 100644 docs/API Reference/python_action.md create mode 100644 docs/API Reference/python_action.run.md create mode 100644 docs/API Reference/python_action.thread_comments.md create mode 100644 docs/README.md create mode 100644 docs/images/demo_annotations.png create mode 100644 docs/images/demo_comment.png rename {demo/image => docs/images}/icon.png (100%) create mode 100644 docs/images/icon_large.png create mode 100644 docs/images/icon_large.xcf rename {demo/image => docs/images}/logo.png (100%) create mode 100644 docs/images/logo_nobg.png rename {demo/image => docs/images}/result.png (100%) create mode 100644 docs/index.md create mode 100644 docs/requirements.txt create mode 100644 docs/stylesheets/extra.css create mode 100644 mkdocs.yml create mode 100644 python_action/__init__.py create mode 100644 python_action/clang_format_xml.py create mode 100644 python_action/clang_tidy.py create mode 100644 python_action/clang_tidy_yml.py create mode 100644 python_action/run.py create mode 100644 python_action/thread_comments.py create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/.ci-ignore b/.ci-ignore new file mode 100644 index 00000000..afba914c --- /dev/null +++ b/.ci-ignore @@ -0,0 +1,2 @@ +python_action +mkdocs.yml diff --git a/.github/workflows/build-docs.bak b/.github/workflows/build-docs.bak new file mode 100644 index 00000000..721744b3 --- /dev/null +++ b/.github/workflows/build-docs.bak @@ -0,0 +1,32 @@ +name: dev-docs + +on: + push: + pull_request: + types: [opened] + +jobs: + using-mkdocs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Install doc's deps + run: | + python3 -m pip install -r docs/requirements.txt + python3 -m pip install . + - name: Build docs + run: mkdocs build + - name: Save artifact + uses: actions/upload-artifact@v2 + with: + name: "cpp linter action dev docs" + path: site/** + - name: upload to github pages + if: ${{ github.event_name == 'release'}} + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: site diff --git a/.github/workflows/cpp-linter.yml b/.github/workflows/cpp-linter.yml index 19f21fb9..4eba50bd 100644 --- a/.github/workflows/cpp-linter.yml +++ b/.github/workflows/cpp-linter.yml @@ -2,21 +2,24 @@ name: cpp-linter on: push: + paths-ignore: "docs/**" pull_request: - types: [opened] + paths-ignore: "docs/**" jobs: cpp-linter: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: C/C++ Lint Action - uses: shenxianpeng/cpp-linter-action@master + - uses: shenxianpeng/cpp-linter-action@master id: linter env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: style: file + files-changed-only: false + # to ignore all demo folder contents except for demo.cpp + # ignore: demo|!demo/demo.cpp - name: Fail fast?! if: steps.linter.outputs.checks-failed > 0 diff --git a/.github/workflows/run-pylint.yml b/.github/workflows/run-pylint.yml new file mode 100644 index 00000000..3ef7c8bd --- /dev/null +++ b/.github/workflows/run-pylint.yml @@ -0,0 +1,29 @@ +name: "Check python code" + +on: + push: + paths: + - "**.py" + - .pylintrc + pull_request: + types: opened + paths: + - "**.py" + - .pylintrc + +jobs: + using-pylint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Install pylint and action deps + run: | + python3 -m pip install --upgrade pylint + python3 -m pip install -r requirements.txt + - name: run pylint + run: | + pylint python_action/** + pylint setup.py diff --git a/.gitignore b/.gitignore index cb8d0354..7806c15a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,182 @@ -.cpp_linter_action_changed_files.json -clang_format_report.txt -clang_tidy_report.txt +# local demo specific +.changed_files.json +clang_format*.txt +clang_tidy*.txt +act.exe +clang_tidy_output.yml +clang_format_output.xml +event_payload.json +comments.json + +#### ignores for Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# exclude local VSCode's settings folder +.vscode/ + +#### ignores for C++ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Cmake build-in-source generated stuff +CMakeUserPresets.json +CMakeCache.txt +CPackConfig.cmake +CPackSourceConfig.cmake +CMakeFiles +cmake_install.cmake diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..52eae424 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,433 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. +# jobs=1 +jobs=2 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +# disable=print-statement,parameter-unpacking,unpacking-in-except,backtick,long-suffix,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,bad-whitespace +disable=invalid-sequence-index,anomalous-backslash-in-string,print-statement,too-few-public-methods,consider-using-f-string,subprocess-run-check + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio).You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +# notes=FIXME,XXX,TODO +notes=FIXME,XXX + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb,_callback + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +# expected-line-ending-format= +expected-line-ending-format=LF + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=88 + +# Maximum number of lines in a module +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[BASIC] + +# Naming hint for argument names +argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct argument names +argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for attribute names +attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct attribute names +attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class names +# class-name-hint=[A-Z_][a-zA-Z0-9]+$ +class-name-hint=[A-Z_][a-zA-Z0-9_]+$ + +# Regular expression matching correct class names +# class-rgx=[A-Z_][a-zA-Z0-9]+$ +class-rgx=[A-Z_][a-zA-Z0-9_]+$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming hint for function names +function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct function names +function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Good variable names which should always be accepted, separated by a comma +# good-names=i,j,k,ex,Run,_ +good-names=r,g,b,w,i,j,k,n,x,y,z,ex,ok,Run,_ + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for method names +method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct method names +method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Naming hint for variable names +variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Maximum number of attributes for a class (see R0902). +# max-attributes=7 +max-attributes=11 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of statements in function / method body +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=1 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/Dockerfile b/Dockerfile index 48cab231..9c0a20f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,12 +13,14 @@ LABEL repository="https://github.com/shenxianpeng/cpp-linter-action" LABEL maintainer="shenxianpeng <20297606+shenxianpeng@users.noreply.github.com>" RUN apt-get update -RUN apt-get -y install curl jq +RUN apt-get -y install python3-pip +# RUN python3 -m pip install --upgrade pip -COPY runchecks.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh +COPY python_action/ pkg/python_action/ +COPY setup.py pkg/setup.py +RUN python3 -m pip install pkg/ # github action args use the CMD option # See https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsargs # also https://docs.docker.com/engine/reference/builder/#cmd -ENTRYPOINT [ "/entrypoint.sh" ] +ENTRYPOINT [ "python3", "-m", "python_action.run" ] diff --git a/README.md b/README.md index f3732568..cc112d95 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,125 @@

- icon +icon

+ # C/C++ Lint Action | clang-format & clang-tidy ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/shenxianpeng/cpp-linter-action) [![cpp-linter](https://github.com/shenxianpeng/cpp-linter-action/actions/workflows/cpp-linter.yml/badge.svg)](https://github.com/shenxianpeng/cpp-linter-action/actions/workflows/cpp-linter.yml) ![GitHub](https://img.shields.io/github/license/shenxianpeng/cpp-linter-action?label=license) +[![Dev Docs Status](https://readthedocs.org/projects/cpp-linter-action/badge/?version=latest)](https://cpp-linter-action.readthedocs.io/en/latest/?badge=latest) +[![open repo in gitpod](https://img.shields.io/badge/Gitpod-Use%20Online%20IDE-B16C04?logo=gitpod)](https://gitpod.io/#https://github.com/shenxianpeng/cpp-linter-action) -Github Actions for linting C/C++ code. Integrated clang-tidy, clang-format check. +A Github Action for linting C/C++ code integrating clang-tidy and clang-format to collect feedback provided in the form of thread comments and/or annotations. ## Usage -Create a new GitHub Actions workflow in your project, e.g. at `.github/workflows/cpp-linter.yml` +Create a new GitHub Actions workflow in your project, e.g. at [.github/workflows/cpp-linter.yml](https://github.com/shenxianpeng/cpp-linter-action/blob/master/.github/workflows/cpp-linter.yml) -The conetent of the file should be in the following format. +The content of the file should be in the following format. ```yaml name: cpp-linter -# Triggers the workflow on push or pull request events +# Workflow syntax: +# https://help.github.com/en/articles/workflow-syntax-for-github-actions + +name: cpp-linter + on: push: + paths-ignore: "docs/**" pull_request: - types: [opened] + paths-ignore: "docs/**" + jobs: cpp-linter: - name: cpp-linter runs-on: ubuntu-latest steps: - - name: C/C++ Lint Action + - uses: actions/checkout@v2 + - uses: shenxianpeng/cpp-linter-action@master + id: linter env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: shenxianpeng/cpp-linter-action@master + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - style: 'file' + style: file + files-changed-only: false + # to ignore all demo folder contents except for demo.cpp + # ignore: demo|!demo/demo.cpp + + - name: Fail fast?! + if: steps.linter.outputs.checks-failed > 0 + run: | + echo "Some files failed the linting checks!" + # for actual deployment + # run: exit 1 ``` -`GITHUB_TOKEN` - Provided by Github (see [Authenticating with the GITHUB_TOKEN](https://docs.github.com/en/actions/reference/authentication-in-a-workflow)) - ### Optional Inputs -| Name | Default | Description | -|------------|---------------|-------------| -| `style` | 'llvm' | The style rules to use. Set this to 'file' to have clang-format use the closest relative .clang-format file. | -| `extensions` | 'c,h,C,H,cpp,hpp,cc,hh,c++,h++,cxx,hxx' | The file extensions to run the action against. This is a comma-separated string. | -| `tidy-checks` | 'boost-\*,bugprone-\*,performance-\*,
readability-\*,portability-\*,
modernize-\*,clang-analyzer-\*,
cppcoreguidelines-\*' | A string of regex-like patterns specifying what checks clang-tidy will use.| -| `repo-root` | '.' | The relative path to the repository root directory. This path is relative to path designated by the runner's GITHUB_WORKSPACE environment variable. | -| `version` | '10' | The desired version of the clang tools to use. Accepted options are strings which can be 6.0, 7, 8, 9, 10, 11, 12. | +#### `style` + +- **Description**: The style rules to use. Set this to 'file' to have clang-format use the closest relative .clang-format file. +- Default: 'llvm' + +#### `extensions` + +- **Description**: The file extensions to run the action against. This is a comma-separated string. +- Default: 'c,h,C,H,cpp,hpp,cc,hh,c++,h++,cxx,hxx' + +#### `tidy-checks` + +- **Description**: Comma-separated list of globs with optional '-' prefix. Globs are processed in order of appearance in the list. Globs without '-' prefix add checks with matching names to the set, globs with the '-' prefix remove checks with matching names from the set of enabled checks. This option's value is appended to the value of the 'Checks' option in a .clang-tidy file (if any). +- Default: 'boost-\*,bugprone-\*,performance-\*,readability-\*,portability-\*,modernize-\*,clang-analyzer-\*,cppcoreguidelines-\*' + +#### `repo-root` + +- **Description**: The relative path to the repository root directory. This path is relative to the path designated as the runner's GITHUB_WORKSPACE environment variable. +- Default: '.' + +#### `version` + +- **Description**: The desired version of the clang tools to use. Accepted options are strings which can be 6.0, 7, 8, 9, 10, 11, or 12. +- Default: '10' + +#### `verbosity` + +- **Description**: This controls the action's verbosity in the workflow's logs. Supported options are defined by the python logging library's log levels. This option does not affect the verbosity of resulting comments or annotations. +- Default: '10' + +#### `lines-changed-only` + +- **Description**: Set this option to true to only analyse changes in the event's diff. +- Default: false + +#### `files-changed-only` + +- **Description**: Set this option to false to analyse any source files in the repo. +- Default: true + +#### `ignore` + +- **Description**: Set this option with string of path(s) to ignore. + - In the case of multiple paths, you can use a pipe character ('|') + to separate the multiple paths. Multiple lines are forbidden as an input to this option; it must be a single string. + - This can also have files, but the file's relative path has to be specified + as well. + - There is no need to use './' for each entry; a blank string ('') represents + the repo-root path (specified by the `repo-root` input option). + - Submodules are automatically ignored. Hidden directories (beginning with a '.') are also ignored automatically. + - Prefix a path with a bang ('!') to make it explicitly _not_ ignored - order of + multiple paths does _not_ take precedence. The '!' prefix can be applied to + a submodule's path (if desired) but not hidden directories. + - Glob patterns are not supported here. All asterick characters ('\*') are literal. +- Default: '.github' + +#### `thread-comments` + +- **Description**: Set this option to false to disable the use of thread comments as feedback. + - To use thread comments, the `GITHUB_TOKEN` (provided by Github to each repository) must be declared as an environment + variable. See [Authenticating with the GITHUB_TOKEN](https://docs.github.com/en/actions/reference/authentication-in-a-workflow) +- Default: true ### Outputs @@ -55,15 +127,36 @@ This action creates 1 output variable named `checks-failed`. Even if the linting ## Example -![github-actions bot](./demo/image/result.png) + + +### Thread Comment + +![github-actions bot](./docs/images/demo_comment.png) + +### Annotations + +![workflow annotations](./docs/images/demo_annotations.png) + + + +## Add C/C++ Lint Action badge in README -Example comment is [here](https://github.com/shenxianpeng/cpp-linter-action/pull/5#commitcomment-55252014). +You can show C/C++ Lint Action status with a badge in your repository README + +Example + +``` +[![cpp-linter](https://github.com/shenxianpeng/cpp-linter-action/actions/workflows/cpp-linter.yml/badge.svg)](https://github.com/shenxianpeng/cpp-linter-action/actions/workflows/cpp-linter.yml) +``` + +[![cpp-linter](https://github.com/shenxianpeng/cpp-linter-action/actions/workflows/cpp-linter.yml/badge.svg)](https://github.com/shenxianpeng/cpp-linter-action/actions/workflows/cpp-linter.yml) ## Have question or feedback? To provide feedback (requesting a feature or reporting a bug) please post to [issues](https://github.com/shenxianpeng/cpp-linter-action/issues). - ## License -The scripts and documentation in this project are released under the [MIT License](LICENSE) +The scripts and documentation in this project are released under the [MIT License](https://github.com/shenxianpeng/cpp-linter-action/blob/master/LICENSE) + + diff --git a/action.yml b/action.yml index e4527c8f..73151069 100644 --- a/action.yml +++ b/action.yml @@ -2,15 +2,19 @@ name: C/C++ Lint Action description: Automatically checks push & pull request changes with clang-format & clang-tidy, then posts a comment with faulty results. author: shenxianpeng branding: - icon: 'check-circle' - color: 'green' + icon: "check-circle" + color: "green" inputs: + thread-comments: + description: Set this option to false to disable the use of thread comments as feedback. Defaults to true. + required: false + default: true style: description: > The style rules to use (defaults to 'llvm'). Set this to 'file' to have clang-format use the closest relative .clang-format file. required: false - default: 'llvm' + default: "llvm" extensions: description: > The file extensions to run the action against. @@ -22,25 +26,61 @@ inputs: A string of regex-like patterns specifying what checks clang-tidy will use. This defaults to 'boost-*,bugprone-*,performance-*,readability-*,portability-*,modernize-*,clang-analyzer-*,cppcoreguidelines-*'. See also clang-tidy docs for more info. required: false - default: 'boost-*,bugprone-*,performance-*,readability-*,portability-*,modernize-*,clang-analyzer-*,cppcoreguidelines-*' + default: "boost-*,bugprone-*,performance-*,readability-*,portability-*,modernize-*,clang-analyzer-*,cppcoreguidelines-*" repo-root: description: > The relative path to the repository root directory. The default value '.' is relative to the runner's GITHUB_WORKSPACE environment variable. required: false - default: '.' + default: "." version: description: "The desired version of the clang tools to use. Accepted options are strings which can be 6.0, 7, 8, 9, 10, 11, 12. Defaults to 10." required: false - default: '10' + default: "10" + verbosity: + descruption: A hidden option to control the action's log verbosity. This is the `logging` level (degaults to DEBUG) + required: false + default: "10" + lines-changed-only: + description: Set this option to 'true' to only analyse changes in the event's diff. Defaults to 'false'. + required: false + default: false + files-changed-only: + description: Set this option to 'false' to analyse any source files in the repo. Defaults to 'true'. + required: false + default: true + ignore: + description: > + Set this option with string of path(s) to ignore. + + - In the case of multiple paths, you can use a pipe character ('|') + to separate the multiple paths. Multiple lines are forbidden as input to this option. + - This can also have files, but the file's relative path has to be specified + as well. + - There is no need to use './' for each entry; a blank string ('') represents + the repo-root path (specified by the `repo-root` input option). + - Path(s) containing a space should be inside single quotes. + - Submodules are automatically ignored. + - Prefix a path with a bang (`!`) to make it explicitly not ignored - order of + multiple paths does take precedence. The `!` prefix can be applied to + submodules if desired. + - Glob patterns are not supported here. All asterick characters ('*') are literal. + required: false + default: ".github" outputs: checks-failed: description: An integer that can be used as a boolean value to indicate if all checks failed. runs: - using: 'docker' - image: 'Dockerfile' + using: "docker" + image: "Dockerfile" args: - - ${{ inputs.style }} - - ${{ inputs.extensions }} - - ${{ inputs.tidy-checks }} - - ${{ inputs.repo-root }} - - ${{ inputs.version }} + - --style=${{ inputs.style }} + - --extensions=${{ inputs.extensions }} + - --tidy-checks=${{ inputs.tidy-checks }} + - --repo-root=${{ inputs.repo-root }} + - --version=${{ inputs.version }} + - --verbosity=${{ inputs.verbosity }} + - --lines-changed-only=${{ inputs.lines-changed-only }} + - --files-changed-only=${{ inputs.files-changed-only }} + - --thread-comments=${{ inputs.thread-comments }} + - --ignore + - ${{ inputs.ignore }} diff --git a/demo/compile_commands.json b/demo/compile_commands.json deleted file mode 100644 index d782c455..00000000 --- a/demo/compile_commands.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "directory": ".", - "command": "/usr/bin/g++ -Wall -Werror demo.cpp", - "file": "/demo.cpp" - }, - { - "directory": ".", - "command": "/usr/bin/g++ -Wall -Werror demo.cpp", - "file": "/demo.hpp" - } -] diff --git a/demo/compile_flags.txt b/demo/compile_flags.txt new file mode 100644 index 00000000..03e4446c --- /dev/null +++ b/demo/compile_flags.txt @@ -0,0 +1,2 @@ +-Wall +-Werror diff --git a/demo/demo.cpp b/demo/demo.cpp index 46cfb299..8bfaf9a7 100644 --- a/demo/demo.cpp +++ b/demo/demo.cpp @@ -1,16 +1,18 @@ /** This is a very ugly test code (doomed to fail linting) */ #include "demo.hpp" -#include +#include /// int main(){ - for (;;) break; + for (;;) break; /// + + + printf("Hello world!\n"); /// - printf("Hello world!\n"); return 0;} diff --git a/demo/demo.hpp b/demo/demo.hpp index 0bf1104a..4bdc1019 100644 --- a/demo/demo.hpp +++ b/demo/demo.hpp @@ -8,7 +8,7 @@ class Dummy { Dummy() :numb(0), useless("\0"){} public: - void *not_usefull(char *str){useless = str;} + void *not_usefull(char *str){useless = str;} /// }; @@ -26,14 +26,11 @@ class Dummy { - - - - struct LongDiff { - long diff; + + long diff; /// }; diff --git a/docs/API Reference/python_action.clang_format_xml.md b/docs/API Reference/python_action.clang_format_xml.md new file mode 100644 index 00000000..47dc0fef --- /dev/null +++ b/docs/API Reference/python_action.clang_format_xml.md @@ -0,0 +1,6 @@ +# clang_format_xml module + +!!! info + This API is experimental and not actually used in production. + +::: python_action.clang_format_xml diff --git a/docs/API Reference/python_action.clang_tidy.md b/docs/API Reference/python_action.clang_tidy.md new file mode 100644 index 00000000..754f8f57 --- /dev/null +++ b/docs/API Reference/python_action.clang_tidy.md @@ -0,0 +1,3 @@ +# clang_tidy module + +::: python_action.clang_tidy diff --git a/docs/API Reference/python_action.clang_tidy_yml.md b/docs/API Reference/python_action.clang_tidy_yml.md new file mode 100644 index 00000000..4b01f8d9 --- /dev/null +++ b/docs/API Reference/python_action.clang_tidy_yml.md @@ -0,0 +1,6 @@ +# clang_tidy_yml module + +!!! info + This API is experimental and not actually used in production. + +::: python_action.clang_tidy_yml diff --git a/docs/API Reference/python_action.md b/docs/API Reference/python_action.md new file mode 100644 index 00000000..ae028699 --- /dev/null +++ b/docs/API Reference/python_action.md @@ -0,0 +1,3 @@ +# Base module + +::: python_action.__init__ diff --git a/docs/API Reference/python_action.run.md b/docs/API Reference/python_action.run.md new file mode 100644 index 00000000..281b0fd6 --- /dev/null +++ b/docs/API Reference/python_action.run.md @@ -0,0 +1,3 @@ +# Run module + +::: python_action.run diff --git a/docs/API Reference/python_action.thread_comments.md b/docs/API Reference/python_action.thread_comments.md new file mode 100644 index 00000000..e83bebae --- /dev/null +++ b/docs/API Reference/python_action.thread_comments.md @@ -0,0 +1,3 @@ +# thread_comments module + +::: python_action.thread_comments diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..241cff0d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,20 @@ +## How to build the docs +From the root directory of the repository, do the following to steps + +1. Install docs' dependencies + + ``` + pip install -r docs/requirements.txt + ``` + On Linux, you may need to use `pip3` instead. + +2. Build the docs + + ``` + mkdocs build + ``` + + or use the following command to see changes rendered in realtime. + ``` + mkdocs serve + ``` diff --git a/docs/images/demo_annotations.png b/docs/images/demo_annotations.png new file mode 100644 index 0000000000000000000000000000000000000000..d5b773b5d03dfe70e8dacc37057cd0aad8cc3789 GIT binary patch literal 24468 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 R>7lDTNCu)HQ7UHW|3CXTLE8WT literal 0 HcmV?d00001 diff --git a/docs/images/demo_comment.png b/docs/images/demo_comment.png new file mode 100644 index 0000000000000000000000000000000000000000..aa80d1f7b8112f49a1626374bf6e685b0492b40d GIT binary patch literal 25979 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%0DWVq@d3e~sbu^4t`+(fjQ5GNe!kGH--d6SKz~ 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 R{Rx;eK{O5SV$>hL_+N?D53K+I literal 0 HcmV?d00001 diff --git a/demo/image/icon.png b/docs/images/icon.png similarity index 100% rename from demo/image/icon.png rename to docs/images/icon.png diff --git a/docs/images/icon_large.png b/docs/images/icon_large.png new file mode 100644 index 0000000000000000000000000000000000000000..c4edfff8bad1bb84c2d4fc1825d4db54ec6ef668 GIT binary patch literal 33322 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 literal 0 HcmV?d00001 diff --git a/docs/images/icon_large.xcf b/docs/images/icon_large.xcf new file mode 100644 index 0000000000000000000000000000000000000000..6e3d429db71d3384c51a1e4f70dc196e3982f871 GIT binary patch literal 110626 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%0Aw7k^qrHwkP+Iof-@cOP+kE4Pn zU9l`M)I$2cwwfx^yPH<@H_IDV)#lf*kI>RR7;NHWyW5KDP!nC&C%(a@)y_I*IStDP zq;>|nn+ut(@Ur|j_p{n*ZLa@L#QR`576t0P84(>1KfdZUla_=oOp3#HDo6AWKe~ix)NyW!6 ztxiaC@4x9C@X(c?JG7i<9{QWL^WdQ~uDR=>(WTMkkxyRTSp*L?^9x~{A5JUF_LJjU+1ZXHbIUUtcShwzUb8t!TI z%G~+1>+cyGZ$-TDi7Q%);i3g=cYOIs3q|ZqPCe6+I`M!Nj&gLTpLSN2^DtDJuIy`v)O7`yMv^>~}t z@BQ)^@(zlzu?c_4lFRQnjQ_~~kE|+2fSx~(g~`V2K7U9gArFn5*DMkhO!vOyV_y`B z$d|8J#M(8v=Ayqj0uv;^^zrj5J&L869>7o{GRSdsv82w6tTNo{*BmK|z)ZVJ?cWv5 zM@l2hNmGjWBOR7?JW#m3h)={}?X8S3`3_e{kWt+k8L5uIO?O(mYa-5(4 zzjN>ITKv<>ABKt$(gj%0>+!AyI0kC4&yu5A}|Ft;XH1PffdP0E3=<)XFa z2EqX+VZO-8?N@J}&&C%e@KyACc|zWKx9k)}v$ z*VX4X!9|T1+(4}%hvYT8KK;EDOi@>{7_K_& z?teizv&u-$KktF(UdhLtSkklT{4X9W6qon?hklzbv{o3&jt@NiEQ+zq8D$$j5dCQh zkiKchl5G<|KT(42Y_g#9gJ1nonYd~`5_=iVXW6h{v+ePpusM!k#w>2V{;6M9Sc_yn z(Yf=9Ur$$9uePpR-&f~$Igocun}yxiee3xu>y>vMJapGkX`$OC(gA1Pj_6sEz7)QNBD`~Ot)aG=0^uETkUn0QO}(piv4sUTkgL*e$C=M0xGV1;$<|+ zrjx&kjreOyjO5ab$A6q`m!0wd_0fT17fV`CuLgFml#*U!_P2n2rjz7q{@~S`h?VqQ+(I@-mf%kPRyWqf!)4gZ{eD~7#edBpF z)=vB)_QX$KX~F#3iCVsKft7jrxdAJkp1iLgy@!*J1-ZTS#0^aWL?F-Z>i2M;I((Yf z(I*m&=w&U2f$PN2Z?2*n=(Aou{;AXQ!PC5$>_)HQ#fi5xIhlz39M3C%e@i3WpSkmg zT>$9+b>I4Ymyx;bJ9u9B`^^Q|R?EEasnv+izJA?eCZ?$~A3vGlqfh+e_7!CwBh`BE z^GUq_Hg;YS7I=~+7d`lICzg}?m2X|i$eMK2oc+yT;!i$*-+WI#jiO-D`49Z`)umSQ z#jl+2brdrGRa`LsFDEkXNRy6T)$Udk(WVVw{LwTa$GO#;iWc~8{NZn#DafmTckyEQ-;uZW zV?V&3eC{)Ch46pl<^#_so2=CDj%?kd&a&#C)Oho&Z z*K`_%Bpc|cFH|n3%XyAYqkzaY^UGHm`9$`y;k62}j3o29R*Q>OM3K4LaI?Zl_q11Z z<05uRp;Ng6D|{}q+i*(Q>c-MF;$ruz#Kk@>F8|_M)H58RRG_oY>@%iA!NHYHB`g<1 zgKHKFa*$Qb!fMm>8!3NBRenE9>?SwM$5t=KqAYZ3tSWYHTM$a;*RixTWDkUCxwv}m zt@t$i-!2~_4K|hYeXCho%3Y*SD-Z;&v?VLZlYEv}1Yf3iu~Fk|GwQhJA^WORVz4Dj zG-i55KxTUDm8i^WT0}~bM77Ct8P#E+SbCR2r% zt1u?ql1$`j5^Z^x^V_LPCo(44GJ0V9yL_TE!>sJfx4*L+T1JzH?)+dYP`YAvZtlDG zG;fDdDAT6>k|x-gyvH z_Kq&;Du$S3a^#*H*F!=wIe5#e`fkHT3BI`d!iGYRVdXKak@?eI=dFRFOw2xYeb2lS z!zyiO$18dGZ$J8uDySMweR1De%q}ggO-x6}9=_w^HF)xu;(6%4Yg)WeckFZ5o>}C9 zx?k9H#uBKz_u^)#d)1H^T>NRaV4yH;{ylkwjgf+0upn)9nJ^0cZ2dzkKqod2Ql){^*7hvklFy>D0>) zU)1bkZ^lag`k$|^W6N2rWd4sE*5rXL0t|HGho9=fhzG7;-G5F2eTwL;p=Jh@5{0(( z^i#u&JH++s&-Sh?qw*P)o_+AFLLj_KsqU+%)0%O;^v#=Fil}}ve0ERBvr=4%hfk|; ztTgiI_g)&kbUyV@H~ZZ6m7dkuKbubdQilj+tl{p!E?_viEVAIwsPv6!~ACt6x@z?9~ zk-a?^J-5Lbz{=dVg^*xpTQR=~`yoUvL?{S*L__S>ITsjDC7jiQ*$TKk(}cX0S$&gc zanmcDoHo4lDV4TNwi=<92-Su0D`xH@>($vvCsCpLlf|l|kiMi^^(DGbsioJcBTt{| zeb~ZlvhdTBdD}=QYDf$4FQV5%I*%HS&)1535vn>);{bj`rf z&_GvX34!j?ro}5dTB=;t3s&{^tz1~*>@;31scZ6i^Gr`wOZ(#a)y}Yoy>^jH`fflb z&B)=x*)7fj^{p$}A803qTixf$o<$R0lUi5Kg>>8wYHBe%638&QwA)3Jm8ev?$t7%F z#BV<#^#5{`?HJJs>ta_#+#Q5DP_kM^1}6GilA(k*sg?j`y^Ms!HTyD6EtHqWC5m4) zK6E&-4K8j`6)RRHHNlV5E~I_PwZb^bn{q?K#{U*_R4u#lZx4B+>Carh0dnM(zjON+ z%2=K6J7Y2A=zY(}9~^T<)1!y^O&09%=ZrtgOq^~@cint`Qz?z5;oPft5*WTo{a*c63Rl*1&ehu^ z^voaJa%~u)fBL5L2Pj9^yFPUD_P@IJoF(U9yM5=*?N_a9Bd}`mhIe0b@foYiSH11Z zZ6CSx9c`r)Zc*Q+^-JrV6)WGe<=vaROT(Ujq-E1uXybpU+B`0Y$0!6(*}C-+=4I&< zcdzua7Asx8>)GGYZ@=*LRbDr1v9}+3uFgsy|AYuI7QN@|FQaOk%$#`QEChwg{P}l0 za{_pd^ve`(_KauE%>S}yuFE16{Kr@Q)4B}lEf>mSkMO}_HXSw$io ztX}`w-%g_t{Db@YL}M!HD!uFHU0NJJB|Iu zS6(U(Q~Ljcl$+S3ClY1v-`JBJkJI~`g_-} zrm7ZR`K2Eu{(0iUy6Ya9`qgj#^-DuF1m@QUKX}K;nXR7IbMO25cRqPuwVV2@TE2d0 zUZI)S^7gGC+}!347yZzX?Y<7xvFWK9N^o>?Wego`2*Oa|S@k;{P z`C4YXWmYCTV)|DelzvwF(Fgc7L}BMJ)`WD|+cDE1@6Eaj9?@~93}|5&1Kj`3 zWlR|1oXaqb4{%+M`H`2!^E#^o3$^^?5 zmmNyea>PZxlX-eFAH(n|D}FpK4lx9nZ=uK>FR_2&ZNfxa<4B~fxN?~Z%mvE%?Q|v% z+a+;cgf`B&0H#e0_UOAAuL4(XPDx7Jm8b=CZBEr&Erxjv5k%ZnE=lQ7ZYOq9opBgD zj%rNYno7i53`ysZbcD$hm}b}C7E|9A>oqbDJjTaq2VWP{U-uV#UV;9=%<)OKpN~%l z3|BmWwMahmY1xy7UMwv*h`>^|9_dG@6}%A1kh_pbe$gpccOB{!H>8nY6+dOiPgw~z z?Weeo)sijbIR-g+b^!#feZ+=>`X3d8wtQ%%Ahm&V|l?4UH3 zrG4&6#x99vQyywQ`*XR7^$~d z@yTVZxlUr>(=dYUmYc^jlj;i~BKrKV31HuFN)>I$83a@_l}N?e1=Kn-E zLN>W#5GXc7F58zwE)2IifP6b+sdy}hT-pn{VnQwwDRRX~3%Oz^emF!5KIFkvNRhVWdi;hl!RuoHx2Ip z0fYMxA#B7a(V9^t2oGm#Tso^s1u8iu#2lHSfkv2QX|QY8e+s;W-IXm_d`YK~42D8~ zEWo6k#rMV;c!@WZ2@axBE#MMCY=qXW8$^Tvw#3VtJ+xs%7Fps&D9rG^7VttagvyKt zl}KnN#IB(LF-q~I2Zz?n^8(U9^Cn=UNlH;&k*6yOM8b<&juqsgAc`*igrl!x5ihV2 z$x#f#XS&dmKuplb9ATp*CmDd5%;124MR;zl4HV&f)@})-Yb`ht@;R3Wz=(jU33gEu z$|YHvJSOmvD)Q{>)6fudlZvPzgN>U76jB{B*GcZ>eEQ(vIz%ubbATaFBGz)>qFovS zQbVo^{!vTe78(uwwi>k|^n@HZTa4;ZX8i^?&Sr7GJ*=JbO#>S|A!~Cp=nrSm3g8}W z!7XKUQ|Jw?xe$G^dZP-UnP15cYlg7;1^X9YY&eJ z^rAAtSq?NSfER*UL9IbPKyVl3WVq{63b#x0i`=${2vR65hdUswyd0lhJY74x!bE_z zkgUuI4^S^Wu-l3V$fAlO?AfCc7E*K4I;`E*DpaE@YDk+!R)DbCP4kF2hN&F-4YG9b zEiFbrNVmda&F=8ce~Kg~lq7yT(!wqM-_;(O?Sj z9X3=!%zJ4x1*tStK_LDbO+k>xD)692QxKxD3K-i|XbQGU6jFhejKWgjS3m`kXbO}< zOk@j8f%FPVfnT8%)JG{G1y3lL0tyWpM?n$=Q;?!*6a`5XP=R7-5Cz5v1gIeOryvwO zqL2zaVk0Qz5y2{ut42_eL}3*qiAGQq&wv%M=B6U4V6ohER0%RB_r(Au+52LE zk`V6XUI+wjVFT>@Mmv51PRO(E%ny(#VP1+E<~(JmN1h6h;FuI4j$73jhbm zj0zqz6Ir)d_co_yeU(;`kzA*-8e}A~laei3)^7EvRaA!89V*Wr`oedtb&1zfXTWek z)RwhOs8m3Oh0!WzmR5*wL@-fdRT(LixW${O6pvqds!5j#`W+Gu4bjl?2PgwsBZwkI z{Ol~v`pp3IPF1lL2o0<)j(|BRvC227eUcM#{jPvleKKzUscwgqd5V}miE&WCY$`Ls zr(;CZO}MHC+&Zt{m#==iq_@kV@~oaIgL18r_n61T4!Sxfr^^+frkGC^>5^74J9RbA(G30Lh6f3Yuwa+KQOpi6m=tn#B z;)WL|k?eP2OBPXR7r1HYimBpn)%9G+972iZ4hfJ_V+s`u4#p1Qp4&nMs{HDq)#eygUXIfTp2B zV~RbL8_LHNd)U6pm^>({R1B=4pHN&8MQ_>}g)p3k#4F@mvLWA7A@Ih*DAQMx$bmVu zdGV1>SR=+xVnF#4mAF%Wfa6}HT2jFok_r;x@vB5aCjx{q9(#nytltczpL|RbL+c6z zG>SFGRFf2+um(QKD%+gpbEW+#*2r*fnX5N@-E zq|mG}leSu1v#gVTCv7aa>=p=n(^XR%)uAz%VCdLIy7}67(5BgG$cT&GR&D1o^y? z-`NJObI3O6Hkvc-U5Z9GA5g8BYR~QBjhrrCC?c%q2nd$&jRj&$*&sG>W(s^JK(hyr zSr&>tz6-wc>yz98n?3P7+Z>@gkMXYmHW~v;q{R+F z&|ei00?4Jp3Md6{F#Van9@Kh+TNIWJ_4a;$DWTQ|w1I6{8(RB~CU-G43N8|H6sZPFjWmea#R$aM zC@9s?x5XiigA>?W2avZ_A&qv+-dC%5!Lv1ydAVdYB)&${RVts2Y$KWgrwwXrKsHzx zat=1BBowVi-Wntm%0@;4X7hzk1VbA)QbeOyl5I?df&p<3IYMbRKYNi*4uwKP4x>LL zFgJ|3$W%|TZz&vWT}C&BFv-nrSV0OLRFhE;o|R;8hBCp8;^`zNImi=3c|Zc!0(c7! z;mbG74P7BCAa`eQi7uER{Q{|h!67OG8$X8Bqa~3Wu|E^genhFEU>iYZs0e;F*YSCa zNnNZw3qvP^q4lSQLL0@O@*8W{wvu=1G@4WiA!)y1EF^&Z!pe^kt5)ibOiCFCO=jZF zM9XKD;YaCc-P*NO+aMwRWn@6lV3-opZ!!&b2i%59wPu2wvne=AfXAqzP-du7xrJncSj>b5D~11% zWBq<4+bMi#quS5vuf@%2Hk^Z<%Zwe&G9uZWf}yjSasMokF3XPl zXW(=e)E7WqJMzL(BNIN-$=9x;o(qg-CZifbhifzomurez2ea;Zq^lO$Viq01UyVi|w;54ja^`sIaSJO+HX}MFnpjkrW7rC^W|&cxp7-1Ahz+Ls62uqV42j6T zX(q&G$T1zFd9az{vvd*Ov?id1n%LXC_~;`Dq{ovJN2AdrHZMLpF;S0*oe-N7<(UJT z3x%*KeIJRAYfdyp9DO7+KB4$97xmVi#ldO#+gvUbDBN5Q1Qj=z0|Cg*a3Jn12c`kY zl}0oV3S`daK?(Logb9=THFWM2KFsU~NH>=Y)4?#Xx;ad!5q2{yI0t8EGvFy;JDd4t z0e47+v#j?aFmzOj2Ht6|OYOJOcQY)fF?hmnh+HF}#JKI=%?ENg(lTbzjHLk4H1tm2 zO~71^ky*r@Pv+X;IeTG(qlyk2R5tB~<9QX0GVnT9QDDUU(iWzaY%11}6eKsqO5a1X|v%4|FiNNVxYL_6( zhfYG=BQ_I~MU$PSX2KRIM25FzN5tlcd7DY26`qr{nUOoPo_QpdC5C1|d?U%*?x4an z_em-1sR2SX_X!e{y#O`t$*#uq$Z(i~+5;h})xJVFGcqhJ_$G|*_rvIF0NA0@atf5L z&ayb)=@d8}fxkc$pPU1!tFtoCMG?|fORTJ)?y4P7u9YER~L>)-l96TLJ+P{1>UBeqjiMtwHpG@0j3h{ zXM*b&Di2E!7rwwE)}hXzi^OfXP>=wbCkhlrxbs3w)ZJX~By6d;huooM+%v%;7e-SG zn4-HvR#;rJa3fh)2W~6qBuU>2g#T%lNd#~1H1b1cA2auLD-C&DX6-tHSq5jC(crX0 zh+dm#q?;|Pogjx=o6&*FNsQJMDxFTkoFP_GlP)_1bFhTuA!G8eS#kV$t6FIzb*EGg zSHCVthv%<2rG#Px6-$Ad(DU?k{K_IzB zGLo}fd898tkYuqsIjJ&OgRj>xjvp^WRr5H}3-bfhtlBAu>}U}{^-Kp1xi0M-WaU!rQm01+f>;^m{G-KMCTJJOlE=)9b>&UVKZ3_CL_vX%ccn!sVw7QF!p}Lr+ACqA0KKj^0g>n z!(Nh8%-ToW8s+;A3x`!9Sck_BqM@UBECy$h7d)n2R-Q302A4$-z-2s7bB-!5iAvFFgCwQCbRs8u5Av#J@#0fvv}w7n;g!oGl1wvVaQnKn{!xB z0`|?}x){2;Ib6pZZLZ^UvRub&*iJ(o26YzHXV75Tjlv45FQrVFGm+f{?}h6KmIQ4C zXKu5^$>63^+$NsI%46|?T>aVgx#q~9si z`xQWhGq>&}n=stRGKg7vWF)fE@bDm^d*G+$CE$Y{qK&f$Uc%?nJ~5qQpEeBt>9Ap& z|BNZ%K`gY$L-C)e;S2wv^h7?I{pb~q1iX=DKZ%q@lVUz1Sv)D`b5qbgHsc}ImtfJw z2V2Gl3%Tu9%*W0WqZ=WVUvVHCOn~DfmS#W6i*6Nwnk0~-A7|f&u0O@HWVf*2YIbNW zF~7$noM;{ii_$*Le?+kyFU+!>F~f3}1q8qz+Dz=k%?@o{*@2Bb8nUdW-I~#OPBYp{ z*bEej9?%Tg`;nJ2prtMG+>cE{!p zUHBQF7&NnkmoaDXT46I0O;rXi`PeG(**1Jzj@i)gZE*57d>fp+4c}JyYz5y|_-qB= z7JRl1-)43v@Ic_(K;>=tHc)vod=~h&f@&-Hw#H`*d|TnO6?|Lp**1Ke{SzC$4N%yI zZ!4^}f^P$r*YIti@|HqxgOJzQZ5$f84OCum+d$<(ZL?hiW_y6iz>r}Tb2-8V;WR$G}k4mEw7WP45~a+3mzv8HNCjDZQIi?e`-1x3Q0 zsf$b)EvwZqr)0t?yQ%_Knv!4u5G~9sWfK`tV%$fB6`x0hIf?(6piUU8t!I2{&f~Mp zXHd$R6bKBx0du)Vvd=y!5Bqp!gMKZu&4pB1o%#}iOUW12Fo~DTv{DI$%EE>fAQGlY z9mJNYUjcwgvGag=nNKGHhlzWYk$(F4q!TS7Cv||iNa|WU=|QHfg#Hb?zM%;v8}Mvy zL8uv*A{0JFoTCur6!#3FVmX9j^vER?-W)t(}a>FnozMBLXj9k ziE>HICY0P6@np_n?XwAmJClehLWxrmiYLvOs&677l*(NZ3KxWmWeF9t2}Q6?C}vRE z2Yo#36R(YU%f_K93;?h*qFBZ7%YP_xvLCdV2|l{0Yh75!0Jblq_>npSpE10%Ge{6t#S{!R0r6! zrt|gtD*|~&4eQH3{>i3yE!(Ha9lCH9l5i!<&A^&s;O?xHmj%+j26#QpXY%L*iRz&Y zXs)FO`UCzZwpJYsI^h4-^w;A#$zt(1J)uLUHod&8Tpn49ajD~z(vQ(t8rjnJlUYe1 z6^3XIHOR3Y~wcBBo&au*i7rzFpI-8?OXW|9LTnC*23xllWZ%O6H8oV;gA3N&N((t#B5t9?&U`wJ$h8fjs0ab_SkD0JDO;@jXn0-#?G?x zbsD?iDd#lyeF+C^bW_4#)7bYV3K)}|3@oa#$Lz))1LlcBqja{?*inA_=1gN}G@BE! zFlIM)2BO78W*d7m&T5Yo;3D9-x0*K?frHQXHk1q**0(9Ucqivw#93=-Ji3$C>xB} z%*^)coJ-=sr+akS6lICa;Z(Ber`vfRn_9!H^~5r0P6L-cK$A08B;qfiaa+9@+|TL9 zYiFBv)2aP;)9W>87Flx}G`%EDD9$=4_-B#!Pc& zBEVQS8_=bDZpM2iFEfqV(2aR+sI20V8N?a)q%q@T+yseFV>W2a%p$sYKsRO_%-J}r zIk)G=lhboc6c=Ya#zj;*({r<=XBxtz$byYO`p{=K8{XJ9V=xGW@MJ)y87QU4k3oA8 z_=@h5zz;0^g-kod-NqN#augFIh&*_7e zOeE?V1W$@59{;fxv*(F`T2`A(N*mA^Iq7F*3(Q_(MgAfh2D~sgNakQ04TvqPL!XgE z+4DGCCp9eSQZly@0~gG{cpU}@;;a-ciXnj(ZWe=+awv%84$5%mE&?bH`M{6Kh2smA z%d|L&ix3A(Kt%WIUd41rzMK+SwlkCXvmQkpo~1I2usKww)1RR-?inh_a;QuXKbOiR zG>^`qGLqY)xpa=rr85uA&{?^&bVmAX(^o)G7v2tGo3W#AZm2#c)(H-izcnB}N#uQ&XGQARQt`tTbp>I`-qIIh9pqB3bA9 zx>=b@E|bTO_0Dx77h?q#kIBt?ChX2qyUNI=btOEaDJ^SFP3KiMCyP;$xfCV|+XAy( z%$z05!^p#OIGBW*vQR(EzpE8Zt5v2pMfEAmxfC@;KYfOxxMwM;+`?Y?n)f$V4Fi$ zpK2J8d)oLh7WydSDQ*c%8Bj#Bu^l!wj9F^Ps#N4@bFUEofB$uwq5ok<=o0(lW{9CE zx#TZv7fI|qm1Ye^HE+H8nvY6s6PH+Kl32MYkNSHfS&fo1Ng4CF#JW=>*CN}^%~&XI zi$CtKcW!Q*B%1j%@BQkGd$x8YUuxjqpXfWviON4(qwe}ATa%=_??xT}IbF-<{r719 z1NQR|yrTVIvf~f16U(3Z&|&-jguXxgLw!GNKY!%!wExKW?fY}~{kQr)nxzkxm7c^- z8Eq8XkQ|qb7)3jn-Owh*B(In=l)KnC%SX;E7AECr+?1S~TY_1%w7QFpw`D4^+$BtG z!0A(t#9}4VA~LG->0_+wF}fy3+Dz=Xsq?D1|K205Fpnf{yyykYq=j9DhUv0H^4~$G zOxY;1zW?EaciFmw=Xq=P+}#?CKguQLz+Mg4GRkOqu`cApMx@v1^p&ZzcRR&fVsvxH zmDm#2r|uSFwh?)Z?XUOc<7d}E9k)wz%m}HB=N`5@6EWws-7=NB1><+fow%~+?3OD3 za*ijKuVmZHhag7X0~Dg4D{SWL+9^CDwPXI0+PMlB{iQ4Hm^A+Q^Kqr>U2K(LLh%s{ z1cx~mO_75`O;5fFPk$4h-ekTBPydg@(=i%Cm-B~@>l0(+VoForF`G#rASVEdu}u0C zu}{pcLmAgB^>t!SlMbzE37xN;yeCF9bw@0@$YJK%xTd6Ern)PAr*9O)=VA{zYY<%< zKbQQM>tpb^{i#pDlcNpg`ga;@u2Y>4vnE&A^KO2bce%p(ciFCYs^j$je}TZMZn*f@ zbjl&=m>o~6x_MI@`41iH@uoKNi_>pvBmY^?_CS>X1~DmJjoq=kF@%iFu_Y*T@&;5h z{)?TyA;TErSPW7BJB?CvVdwblF&6o0BkaNc7jSAu7xOFG zaVGb02UbwFJ#pio-_+(YFsfk><8OpR6dHZ4Sn-xa739)~D#%F)a%@bUDyJL*qc8gq z1+Um0$GW(j@FEt=vxg|;`4ElqsND4-3e2@o!cgZZ;0(JFv#2%Fjj{mCJwU;|S;x48 zOxR?JEekd@9ehk$e5kQu8oM0P@51A z%dsFn9CAj8ECy0?h=e#-B1z~{2P`>6V~{8r4v-0}XXpBmJ`!sAkYmo=r4fhSPiT@l Szmp|t(W^RyKJm{fh5SE-?{+)@ literal 0 HcmV?d00001 diff --git a/demo/image/logo.png b/docs/images/logo.png similarity index 100% rename from demo/image/logo.png rename to docs/images/logo.png diff --git a/docs/images/logo_nobg.png b/docs/images/logo_nobg.png new file mode 100644 index 0000000000000000000000000000000000000000..f76ee3cc05b456aadc608941dc485bd234abe30b GIT binary patch literal 14183 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

9zaTUZ0)LK&Q`VV?ey9S zM&LK5*Y@NTOI6Xd?AH-Nag4jn=e85Hxn*u@44n<3)DUXD7=q&OviDL^%ojIlkZ_;r zWXb%JQ5m+G_T|Ef7+gn_8`}2g_fo!_;sl$>`)x4Zejdrzv0%S)w7`sdjfeKrE-F6d z-Qt38G;aWVsVL%?{rKT+6KMatKw=GBtl zhjZQHiUl#46~Q4@5B)p1m0f<+)Y1eqSWL9vz?0z22S`27!Pu+Eq98y?UR|zS#{AX)14bSV1poj5 literal 0 HcmV?d00001 diff --git a/demo/image/result.png b/docs/images/result.png similarity index 100% rename from demo/image/result.png rename to docs/images/result.png diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..92c415cd --- /dev/null +++ b/docs/index.md @@ -0,0 +1,26 @@ +

+icon +

+ +{% + include-markdown "../README.md" + start="" + end="" +%} + +### Thread Comment + +![sample comment](images/demo_comment.png) + +### Annotations + +![workflow annotations](images/demo_annotations.png) + +{% + include-markdown "../README.md" + start="" + end="" +%} +{% + include-markdown "README.md" +%} diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..ac6db64f --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,5 @@ +mkdocs +mkdocstrings +mkdocs-material +mkdocs-autorefs +mkdocs-include-markdown-plugin diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 00000000..0d65599d --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,59 @@ +:root>* { + --md-code-hl-number-color: #7e9b1e; + --md-code-hl-special-color: #499DC7; + --md-code-hl-constant-color: #48C999; + --md-code-hl-keyword-color: #499CD6; + --md-code-hl-string-color: #B88451; + --md-code-hl-comment-color: #5E9955; + /* --md-typeset-table-color: #ff000085; */ +} + +.md-typeset table:not([class]) tbody tr:hover, +tr:hover { + background-color: var(--md-accent-fg-color--transparent); +} + +th { + background-color: var(--md-primary-fg-color--light); +} + +.md-typeset table:not([class]) { + /* border-radius: .1rem; */ + border-radius: 0 1rem 1rem 0; + border-left: .2rem solid; + border-left-color: var(--md-default-fg-color); +} + +.md-typeset details:not([open])>summary { + /* border-radius: .1rem; */ + border-radius: 0 1rem 1rem 0; +} + +.md-typeset summary { + border-top-right-radius: 1rem; +} + +.md-typeset .admonition, +.md-typeset details { + /* border-top-right-radius: .1rem; */ + border-top-right-radius: 1rem; + border-bottom-right-radius: 1rem; +} + +.md-typeset .cite>.admonition-title, +.md-typeset .cite>summary, +.md-typeset .quote>.admonition-title, +.md-typeset .quote>summary { + /* background-color: hsla(0,0%,62%,.1); */ + /* border-color: #9e9e9e; */ + background-color: hsla(27.9, 68.9%, 41.6%, 0.33); + border-color: hsl(27.9, 68.9%, 41.6%); +} + +.md-typeset .admonition.cite, +.md-typeset .admonition.quote, +.md-typeset details.cite, +.md-typeset details.quote { + /* border-color: #9e9e9e; */ + border-color: hsla(27.9, 68.9%, 41.6%); +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..609a75d5 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,69 @@ +site_name: C/C++ Linter Action's Docs +site_description: "Developer documentation from sources." +site_url: "https://shenxianpeng.github.io/cpp-linter-action" +repo_url: "https://github.com/shenxianpeng/cpp-linter-action" +edit_uri: "blob/master/docs/" +repo_name: "shenxianpeng/cpp-linter-action" +nav: + - index.md + - "Dev Docs": + - API Reference/python_action.md + - API Reference/python_action.run.md + - API Reference/python_action.clang_tidy.md + - API Reference/python_action.clang_tidy_yml.md + - API Reference/python_action.clang_format_xml.md + - API Reference/python_action.thread_comments.md + +theme: + name: material + logo: images/icon_large.png + features: + - navigation.top + favicon: images/icon_large.png + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: light blue + toggle: + icon: material/lightbulb-outline + name: Switch to dark mode + - media: "(prefers-color-scheme: light)" + scheme: slate + primary: indigo + accent: cyan + toggle: + icon: material/lightbulb + name: Switch to light mode + +extra_css: + - stylesheets/extra.css + +plugins: + - search + - include-markdown + - autorefs + - mkdocstrings: + default_handler: python + handlers: + python: + rendering: + # show_if_no_docstring: true + show_source: true + heading_level: 2 + watch: + - python_action + +markdown_extensions: + - admonition + - attr_list + - pymdownx.details + - pymdownx.emoji + - pymdownx.superfences + - pymdownx.tabbed + - pymdownx.tasklist + - toc: + permalink: true + - pymdownx.highlight: + linenums_style: pymdownx-inline + - pymdownx.inlinehilite diff --git a/python_action/__init__.py b/python_action/__init__.py new file mode 100644 index 00000000..613a5a2f --- /dev/null +++ b/python_action/__init__.py @@ -0,0 +1,103 @@ +"""The Base module of the `python_action` package. This holds the objects shared by +multiple modules.""" +import io +import os +import logging + +FOUND_RICH_LIB = False +try: + from rich.logging import RichHandler + + FOUND_RICH_LIB = True + + logging.basicConfig( + format="%(name)s: %(message)s", + handlers=[RichHandler(show_time=False)], + ) + +except ImportError: + logging.basicConfig() + +#: The logging.Logger object used for outputing data. +logger = logging.getLogger("CPP Linter") +if not FOUND_RICH_LIB: + logger.debug("rich module not found") + +# global constant variables +GITHUB_SHA = os.getenv("GITHUB_SHA", "95915a282b3efcad67b9ad3f95fba1501e43ab22") +GITHUB_TOKEN = os.getenv("GITHUB_TOKEN", os.getenv("GIT_REST_API", "")) +API_HEADERS = { + "Authorization": f"token {GITHUB_TOKEN}", + "Accept": "application/vnd.github.v3.text+json", +} + + +class Globals: + """Global variables for re-use (non-constant).""" + + PAYLOAD_TIDY = "" + """The accumulated output of clang-tidy (gets appended to OUTPUT)""" + OUTPUT = "" + """The accumulated body of the resulting comment that gets posted.""" + FILES = [] + """The reponding payload containing info about changed files.""" + EVENT_PAYLOAD = {} + """The parsed JSON of the event payload.""" + response_buffer = None + """A shared response object for `requests` module.""" + + +class GlobalParser: + """Global variables specific to output parsers. Each element in each of the + following attributes represents a clang-tool's output for 1 source file. + """ + + tidy_notes = [] + """This can only be a `list` of type + [`TidyNotification`][python_action.clang_tidy.TidyNotification]""" + tidy_advice = [] + """This can only be a `list` of type + [`YMLFixit`][python_action.clang_tidy_yml.YMLFixit]""" + format_advice = [] + """This can only be a `list` of type + [`XMLFixit`][python_action.clang_format_xml.XMLFixit]""" + + +def get_line_cnt_from_cols(file_path: str, offset: int) -> tuple: + """Gets a line count and columns offset from a file's absolute offset. + + Args: + file_path: Path to file. + offset: The byte offset to translate + + Returns: + A `tuple` of 2 `int` numbers: + + - Index 0 is the line number for the given offset. + - Index 1 is the column number for the given offset on the line. + """ + line_cnt = 1 + last_lf_pos = 0 + cols = 1 + file_path = file_path.replace("/", os.sep) + with io.open(file_path, "r", encoding="utf-8", newline="\n") as src_file: + src_file.seek(0, io.SEEK_END) + max_len = src_file.tell() + src_file.seek(0, io.SEEK_SET) + while src_file.tell() != offset and src_file.tell() < max_len: + char = src_file.read(1) + if char == "\n": + line_cnt += 1 + last_lf_pos = src_file.tell() - 1 # -1 because LF is part of offset + if last_lf_pos + 1 > max_len: + src_file.newlines = "\r\n" + src_file.seek(0, io.SEEK_SET) + line_cnt = 1 + cols = src_file.tell() - last_lf_pos + return (line_cnt, cols) + + +def log_response_msg(): + """Output the response buffer's message on failed request""" + if Globals.response_buffer.status_code >= 400: + logger.error("response returned message: %s", Globals.response_buffer.text) diff --git a/python_action/clang_format_xml.py b/python_action/clang_format_xml.py new file mode 100644 index 00000000..ea98f0f2 --- /dev/null +++ b/python_action/clang_format_xml.py @@ -0,0 +1,162 @@ +"""Parse output from clang-format's XML suggestions.""" +import os +import xml.etree.ElementTree as ET +from . import GlobalParser, get_line_cnt_from_cols + + +class FormatReplacement: + """An object representing a single replacement. + + Attributes: + cols (int): The columns number of where the suggestion starts on the line + null_len (int): The number of bytes removed by suggestion + text (str): The `bytearray` of the suggestion + """ + + def __init__(self, cols: int, null_len: int, text: str) -> None: + """ + Args: + cols: The columns number of where the suggestion starts on the line + null_len: The number of bytes removed by suggestion + text: The `bytearray` of the suggestion + """ + self.cols = cols + self.null_len = null_len + self.text = text + + def __repr__(self) -> str: + return ( + f"" + ) + + +class FormatReplacementLine: + """An object that represents a replacement(s) for a single line. + + Attributes: + line (int): The line number of where the suggestion starts + replacements (list): A list of + [`FormatReplacement`][python_action.clang_format_xml.FormatReplacement] + object(s) representing suggestions. + """ + + def __init__(self, line_numb: int): + """ + Args: + line_numb: The line number of about the replacements + """ + self.line = line_numb + self.replacements = [] + + def __repr__(self): + return ( + f"" + ) + + +class XMLFixit: + """A single object to represent each suggestion. + + Attributes: + filename (str): The source file that the suggestion concerns. + replaced_lines (list): A list of + [`FormatReplacementLine`][ + python_action.clang_format_xml.FormatReplacementLine] + representing replacement(s) on a single line. + """ + + def __init__(self, filename: str): + """ + Args: + filename: The source file's name for which the contents of the xml + file exported by clang-tidy. + """ + self.filename = filename.replace(os.sep, "/") + self.replaced_lines = [] + + def __repr__(self) -> str: + return ( + f"" + ) + + def log_command(self, style: str) -> str: + """Output a notification as a github log command. + + !!! info See Also + - [An error message](https://docs.github.com/en/actions/learn-github- + actions/workflow-commands-for-github-actions#setting-an-error-message) + - [A warning message](https://docs.github.com/en/actions/learn-github- + actions/workflow-commands-for-github-actions#setting-a-warning-message) + - [A notice message](https://docs.github.com/en/actions/learn-github- + actions/workflow-commands-for-github-actions#setting-a-notice-message) + + Args: + style: The chosen code style guidelines. + """ + if style not in ("llvm", "google", "webkit", "mozilla", "gnu"): + # potentially the style parameter could be a str of JSON/YML syntax + style = "Custom" + else: + if style.startswith("llvm") or style.startswith("gnu"): + style = style.upper() + else: + style = style.title() + + return ( + "::notice file={name},title=Run clang-format on {name}::" + "File {name} (lines {lines}): Code does not conform to {style_guide} " + "style guidelines.".format( + name=self.filename, + lines=", ".join(str(f.line) for f in self.replaced_lines), + style_guide=style, + ) + ) + + +def parse_format_replacements_xml(src_filename: str): + """Parse XML output of replacements from clang-format. Output is saved to + [`format_advice`][python_action.__init__.GlobalParser.format_advice]. + + Args: + src_filename: The source file's name for which the contents of the xml + file exported by clang-tidy. + """ + tree = ET.parse("clang_format_output.xml") + fixit = XMLFixit(src_filename) + for child in tree.getroot(): + if child.tag == "replacement": + offset = int(child.attrib["offset"]) + line, cols = get_line_cnt_from_cols(src_filename, offset) + null_len = int(child.attrib["length"]) + text = "" if child.text is None else child.text + fix = FormatReplacement(cols, null_len, text) + if not fixit.replaced_lines or ( + fixit.replaced_lines and line != fixit.replaced_lines[-1].line + ): + line_fix = FormatReplacementLine(line) + line_fix.replacements.append(fix) + fixit.replaced_lines.append(line_fix) + elif fixit.replaced_lines and line == fixit.replaced_lines[-1].line: + fixit.replaced_lines[-1].replacements.append(fix) + GlobalParser.format_advice.append(fixit) + + +def print_fixits(): + """Print all [`XMLFixit`][python_action.clang_format_xml.XMLFixit] objects in + [`format_advice`][python_action.__init__.GlobalParser.format_advice].""" + for fixit in GlobalParser.format_advice: + print(repr(fixit)) + for line_fix in fixit.replaced_lines: + print(" " + repr(line_fix)) + for fix in line_fix.replacements: + print("\t" + repr(fix)) + + +if __name__ == "__main__": + import sys + + parse_format_replacements_xml(sys.argv[1]) + print_fixits() diff --git a/python_action/clang_tidy.py b/python_action/clang_tidy.py new file mode 100644 index 00000000..f9a77bbb --- /dev/null +++ b/python_action/clang_tidy.py @@ -0,0 +1,111 @@ +"""Parse output from clang-tidy's stdout""" +import os +import sys +import re +from . import GlobalParser + + +class TidyNotification: + """Create a object that decodes info from the clang-tidy output's initial line that + details a specific notification. + + Attributes: + diagnostic (str): The clang-tidy check that enabled the notification. + filename (str): The source filename concerning the notification. + line (int): The line number of the source file. + cols (int): The columns of the line that triggered the notification. + note_type (str): The priority level of notification (warning/error). + note_info (str): The rationale of the notification. + fixit_lines (list): A `list` of lines (`str`) for the code-block in the + notification. + """ + + def __init__(self, notification_line: str): + """ + Args: + notification_line: The first line in the notification. + """ + sliced_line = notification_line.split(":") + if sys.platform.startswith("win32") and len(sliced_line) > 5: + # sliced_list items 0 & 1 are the path seperated at the ":". + # we need to re-assemble the path for correct list expansion (see below) + sliced_line = [sliced_line[0] + ":" + sliced_line[1]] + sliced_line[2:] + ( + self.filename, + self.line, + self.cols, + self.note_type, + self.note_info, + ) = sliced_line + + self.diagnostic = re.search("\[.*\]", self.note_info).group(0) + self.note_info = self.note_info.replace(self.diagnostic, "").strip() + self.diagnostic = self.diagnostic[1:-1] + self.note_type = self.note_type.strip() + self.line = int(self.line) + self.cols = int(self.cols) + self.filename = self.filename.replace(os.getcwd() + os.sep, "") + self.fixit_lines = [] + + def __repr__(self) -> str: + return ( + "
\n{}:{}:{}: {}: [{}]" + "\n\n> {}\n

\n\n```{}\n{}```\n

\n
\n\n".format( + self.filename, + self.line, + self.cols, + self.note_type, + self.diagnostic, + self.note_info, + os.path.splitext(self.filename)[1], + "".join(self.fixit_lines), + ) + ) + + def log_command(self) -> str: + """Output the notification as a github log command. + + !!! info See Also + - [An error message](https://docs.github.com/en/actions/learn-github- + actions/workflow-commands-for-github-actions#setting-an-error-message) + - [A warning message](https://docs.github.com/en/actions/learn-github- + actions/workflow-commands-for-github-actions#setting-a-warning-message) + - [A notice message](https://docs.github.com/en/actions/learn-github- + actions/workflow-commands-for-github-actions#setting-a-notice-message) + """ + return "::{} file={},line={},title={}:{}:{} [{}]::{}".format( + "notice" if self.note_type.startswith("note") else self.note_type, + self.filename, + self.line, + self.filename, + self.line, + self.cols, + self.diagnostic, + self.note_info, + ) + + +def parse_tidy_output() -> None: + """Parse clang-tidy output in a file created from stdout.""" + notification = None + with open("clang_tidy_report.txt", "r", encoding="utf-8") as tidy_out: + for line in tidy_out.readlines(): + if re.search("^.*:\d+:\d+:\s\w+:.*\[.*\]$", line) is not None: + notification = TidyNotification(line) + GlobalParser.tidy_notes.append(notification) + elif notification is not None: + notification.fixit_lines.append(line) + + +def print_fixits(): + """Print out all clang-tidy notifications from stdout (which are saved to + clang_tidy_report.txt and allocated to + [`tidy_notes`][python_action.__init__.GlobalParser.tidy_notes].""" + for notification in GlobalParser.tidy_notes: + print("found", len(GlobalParser.tidy_notes), "tidy_notes") + print(repr(notification)) + + +if __name__ == "__main__": + parse_tidy_output() + print_fixits() diff --git a/python_action/clang_tidy_yml.py b/python_action/clang_tidy_yml.py new file mode 100644 index 00000000..89476f5e --- /dev/null +++ b/python_action/clang_tidy_yml.py @@ -0,0 +1,143 @@ +"""Parse output from clang-tidy's YML format""" +import os +import yaml +from . import GlobalParser, get_line_cnt_from_cols + + +CWD_HEADER_GAURD = bytes( + os.getcwd().upper().replace(os.sep, "_").replace("-", "_"), encoding="utf-8" +) #: The constant used to trim absolute paths from header gaurd suggestions. + + +class TidyDiagnostic: + """Create an object that represents a diagnostic output found in the + YAML exported from clang-tidy. + + Attributes: + name (str): The diagnostic name + message (str): The diagnostic message + line (int): The line number that triggered the diagnostic + cols (int): The columns of the `line` that triggered the diagnostic + null_len (int): The number of bytes replaced by suggestions + replacements (list): The `list` of + [`TidyReplacement`][python_action.clang_tidy_yml.TidyReplacement] objects. + + """ + + def __init__(self, diagnostic_name: str): + """ + Args: + diagnostic_name: The name of the check that got triggered. + """ + self.name = diagnostic_name + self.message = "" + self.line = 0 + self.cols = 0 + self.null_len = 0 + self.replacements = [] + + def __repr__(self): + """a str representation of all attributes.""" + return ( + f"" + ) + + +class TidyReplacement: + """Create an object representing a clang-tidy suggested replacement. + + Attributes: + line (int): The replacement content's starting line + cols (int): The replacement content's starting columns + null_len (int): The number of bytes discarded from `cols` + text (list): The replacement content's text (each `str` item is a line) + """ + + def __init__(self, line_cnt: int, cols: int, length: int): + """ + Args: + line_cnt: The replacement content's starting line + cols: The replacement content's starting columns + length: The number of bytes discarded from `cols` + """ + self.line = line_cnt + self.cols = cols + self.null_len = length + self.text = [] + + def __repr__(self) -> str: + return ( + f"" + ) + + +class YMLFixit: + """A single object to represent each suggestion. + + Attributes: + filename (str): The source file's name concerning the suggestion. + diagnostics (list): The `list` of + [`TidyDiagnostic`][python_action.clang_tidy_yml.TidyDiagnostic] objects. + """ + + def __init__(self, filename: str) -> None: + """ + Args: + filename: The source file's name (with path) concerning the suggestion. + """ + self.filename = filename.replace(os.getcwd() + os.sep, "").replace(os.sep, "/") + self.diagnostics = [] + + def __repr__(self) -> str: + return ( + f"" + ) + + +def parse_tidy_suggestions_yml(): + """Read a YAML file from clang-tidy and create a list of suggestions from it. + Output is saved to [`tidy_advice`][python_action.__init__.GlobalParser.tidy_advice]. + """ + yml = {} + with open("clang_tidy_output.yml", "r", encoding="utf-8") as yml_file: + yml = yaml.safe_load(yml_file) + fixit = YMLFixit(yml["MainSourceFile"]) + for diag_results in yml["Diagnostics"]: + diag = TidyDiagnostic(diag_results["DiagnosticName"]) + diag.message = diag_results["DiagnosticMessage"]["Message"] + diag.line, diag.cols = get_line_cnt_from_cols( + yml["MainSourceFile"], diag_results["DiagnosticMessage"]["FileOffset"] + ) + for replacement in diag_results["DiagnosticMessage"]["Replacements"]: + line_cnt, cols = get_line_cnt_from_cols( + yml["MainSourceFile"], replacement["Offset"] + ) + fix = TidyReplacement(line_cnt, cols, replacement["Length"]) + fix.text = bytes(replacement["ReplacementText"], encoding="utf-8") + if fix.text.startswith(b"header is missing header guard"): + print( + "filtering header guard suggestion (making relative to repo root)" + ) + fix.text = fix.text.replace(CWD_HEADER_GAURD, b"") + diag.replacements.append(fix) + fixit.diagnostics.append(diag) + # filter out absolute header gaurds + GlobalParser.tidy_advice.append(fixit) + + +def print_fixits(): + """Print all [`YMLFixit`][python_action.clang_tidy_yml.YMLFixit] objects in + [`tidy_advice`][python_action.__init__.GlobalParser.tidy_advice].""" + for fix in GlobalParser.tidy_advice: + for diag in fix.diagnostics: + print(repr(diag)) + for replac in diag.replacements: + print(" " + repr(replac), f"\n\treplace text:\n{replac.text}") + + +if __name__ == "__main__": + parse_tidy_suggestions_yml() + print_fixits() diff --git a/python_action/run.py b/python_action/run.py new file mode 100644 index 00000000..e97d11bd --- /dev/null +++ b/python_action/run.py @@ -0,0 +1,725 @@ +"""Run clang-tidy and clang-format on a list of changed files provided by GitHub's +REST API. If executed from command-line, then [`main()`][python_action.run.main] is +the entrypoint. + +!!! info "See Also" + - [github rest API reference for pulls]( + https://docs.github.com/en/rest/reference/pulls) + - [github rest API reference for repos]( + https://docs.github.com/en/rest/reference/repos) + - [github rest API reference for issues]( + https://docs.github.com/en/rest/reference/issues) +""" +import subprocess +import os +import sys +import argparse +import configparser +import json +import requests +from . import ( + Globals, + GlobalParser, + logging, + logger, + GITHUB_TOKEN, + GITHUB_SHA, + API_HEADERS, + log_response_msg, +) + +from .clang_tidy_yml import parse_tidy_suggestions_yml +from .clang_format_xml import parse_format_replacements_xml +from .clang_tidy import parse_tidy_output +from .thread_comments import remove_bot_comments, list_diff_comments # , get_review_id + + +# global constant variables +GITHUB_EVEN_PATH = os.getenv("GITHUB_EVENT_PATH", "event_payload.json") +GITHUB_API_URL = os.getenv("GITHUB_API_URL", "https://api.github.com") +GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY", "2bndy5/cpp-linter-action") +GITHUB_EVENT_NAME = os.getenv("GITHUB_EVENT_NAME", "pull_request") + +# setup CLI args +cli_arg_parser = argparse.ArgumentParser( + description=__doc__[: __doc__.find("If executed from")] +) +cli_arg_parser.add_argument( + "-v", + "--verbosity", + default="10", + help="The logging level. Defaults to level 20 (aka 'logging.INFO').", +) +cli_arg_parser.add_argument( + "-s", + "--style", + default="llvm", + help="The style rules to use (defaults to 'llvm'). Set this to 'file' to have " + "clang-format use the closest relative .clang-format file.", +) +cli_arg_parser.add_argument( + "-c", + "--tidy-checks", + default="boost-*,bugprone-*,performance-*,readability-*,portability-*,modernize-*," + "clang-analyzer-*,cppcoreguidelines-*", + help="A string of regex-like patterns specifying what checks clang-tidy will use. " + "This defaults to %(default)s. See also clang-tidy docs for more info.", +) +cli_arg_parser.add_argument( + "-V", + "--version", + default="10", + help="The desired version of the clang tools to use. Accepted options are strings " + "which can be 6.0, 7, 8, 9, 10, 11, 12. Defaults to %(default)s.", +) +cli_arg_parser.add_argument( + "-e", + "--extensions", + default="c,h,C,H,cpp,hpp,cc,hh,c++,h++,cxx,hxx", + help="The file extensions to run the action against. This comma-separated string " + "defaults to %(default)s.", +) +cli_arg_parser.add_argument( + "-r", + "--repo-root", + default=".", + help="The relative path to the repository root directory. The default value " + "'%(default)s' is relative to the runner's GITHUB_WORKSPACE environment variable.", +) +cli_arg_parser.add_argument( + "-i", + "--ignore", + nargs="?", + help="Set this option with paths to ignore. In the case of multiple " + "paths, you can set this option (multiple times) for each path. This can " + "also have files, but the file's relative path has to be specified as well " + "with the filename.", +) +cli_arg_parser.add_argument( + "--lines-changed-only", + default="false", + type=lambda input: input.lower() == "true", + help="Set this option to 'true' to only analyse changes in the event's diff. " + "Defaults to %(default)s.", +) +cli_arg_parser.add_argument( + "--files-changed-only", + default="true", + type=lambda input: input.lower() == "true", + help="Set this option to 'false' to analyse any source files in the repo. " + "Defaults to %(default)s.", +) +cli_arg_parser.add_argument( + "--thread-comments", + default="true", + type=lambda input: input.lower() == "true", + help="Set this option to false to disable the use of thread comments as feedback." + "Defaults to %(default)s.", +) + + +def set_exit_code(override: int = None) -> int: + """Set the action's exit code. + + Args: + override: The number to use when overriding the action's logic. + + Returns: + The exit code that was used. If the `override` parameter was not passed, + then this value will describe (like a bool value) if any checks failed. + """ + exit_code = override if override is not None else bool(Globals.OUTPUT) + print(f"::set-output name=checks-failed::{exit_code}") + return exit_code + + +# setup a separate logger for using github log commands +log_commander = logger.getChild("LOG COMMANDER") # create a child of our logger obj +log_commander.setLevel(logging.DEBUG) # be sure that log commands are output +console_handler = logging.StreamHandler() # Create special stdout stream handler +console_handler.setFormatter(logging.Formatter("%(message)s")) # no formatted log cmds +log_commander.addHandler(console_handler) # Use special handler for log_commander +log_commander.propagate = False # prevent duplicate messages in the parent logger obj + + +def start_log_group(name: str) -> None: + """Begin a callapsable group of log statements. + + Argrs: + name: The name of the callapsable group + """ + log_commander.fatal("::group::%s", name) + + +def end_log_group() -> None: + """End a callapsable group of log statements.""" + log_commander.fatal("::endgroup::") + + +def is_file_in_list(paths: list, file_name: str, prompt: str) -> bool: + """Detirmine if a file is specified in a list of paths and/or filenames. + + Args: + paths: A list of specified paths to compare with. This list can contain a + specified file, but the file's path must be included as part of the + filename. + file_name: The file's path & name being sought in the `paths` list. + prompt: A debugging prompt to use when the path is found in the list. + Returns: + - True if `file_name` is in the `paths` list. + - False if `file_name` is not in the `paths` list. + """ + for path in paths: + result = os.path.commonpath([path, file_name]).replace(os.sep, "/") + if result == path: + logger.debug( + '"./%s" is %s as specified in the domain "./%s"', + file_name, + prompt, + path, + ) + return True + return False + + +def get_list_of_changed_files() -> None: + """Fetch the JSON payload of the event's changed files. Sets the + [`FILES`][python_action.__init__.Globals.FILES] attribute.""" + files_link = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/" + if GITHUB_EVENT_NAME == "pull_request": + files_link += f"pulls/{Globals.EVENT_PAYLOAD['number']}/files" + elif GITHUB_EVENT_NAME == "push": + files_link += f"commits/{GITHUB_SHA}" + else: + logger.warning("triggered on unsupported event.") + sys.exit(set_exit_code(0)) + logger.info("Fetching files list from url: %s", files_link) + Globals.FILES = requests.get(files_link).json() + + +def filter_out_non_source_files( + ext_list: list, ignored: list, not_ignored: list, lines_changed_only: bool +) -> bool: + """Exclude undesired files (specified by user input 'extensions'). This filter + applies to the event's [`FILES`][python_action.__init__.Globals.FILES] attribute. + + Args: + ext_list: A list of file extensions that are to be examined. + ignored: A list of paths to explicitly ignore. + not_ignored: A list of paths to explicitly not ignore. + lines_changed_only: A flag that forces focus on only changes in the event's + diff info. + + Returns: + True if there are files to check. False will invoke a early exit (in + [`main()`][python_action.run.main]) when no files to be checked. + """ + files = [] + for file in ( + Globals.FILES if GITHUB_EVENT_NAME == "pull_request" else Globals.FILES["files"] + ): + if ( + os.path.splitext(file["filename"])[1][1:] in ext_list + and not file["status"].endswith("removed") + and ( + not is_file_in_list(ignored, file["filename"], "ignored") + or is_file_in_list(not_ignored, file["filename"], "not ignored") + ) + ): + if lines_changed_only and "patch" in file.keys(): + # get diff details for the file's changes + line_filter = { + "name": file["filename"].replace("/", os.sep), + "lines": [], + } + file["diff_line_map"], line_numb_in_diff = ({}, 0) + # diff_line_map is a dict for which each + # - key is the line number in the file + # - value is the line's "position" in the diff + for i, line in enumerate(file["patch"].splitlines()): + if line.startswith("@@ -"): + changed_hunk = line[line.find(" +") + 2 : line.find(" @@")] + changed_hunk = changed_hunk.split(",") + start_line = int(changed_hunk[0]) + hunk_length = int(changed_hunk[1]) + line_filter["lines"].append( + [start_line, hunk_length + start_line] + ) + line_numb_in_diff = start_line + elif not line.startswith("-"): + file["diff_line_map"][line_numb_in_diff] = i + line_filter["lines"][-1][1] = line_numb_in_diff + line_numb_in_diff += 1 + file["line_filter"] = line_filter + elif lines_changed_only: + continue + files.append(file) + + if files: + logger.info( + "Giving attention to the following files:\n\t%s", + "\n\t".join([f["filename"] for f in files]), + ) + if GITHUB_EVENT_NAME == "pull_request": + Globals.FILES = files + else: + Globals.FILES["files"] = files + if not os.getenv("CI"): # if not executed on a github runner + with open(".changed_files.json", "w", encoding="utf-8") as temp: + # dump altered json of changed files + json.dump(Globals.FILES, temp, indent=2) + else: + logger.info("No source files need checking!") + return False + return True + + +def verify_files_are_present() -> None: + """Download the files if not present. + + !!! hint + This function assumes the working directory is the root of the invoking + repository. If files are not found, then they are downloaded to the working + directory. This is bad for files with the same name from different folders. + """ + for file in ( + Globals.FILES if GITHUB_EVENT_NAME == "pull_request" else Globals.FILES["files"] + ): + file_name = file["filename"].replace("/", os.sep) + if not os.path.exists(file_name): + logger.warning("Could not find %s! Did you checkout the repo?", file_name) + logger.info("Downloading file from url: %s", file["raw_url"]) + Globals.response_buffer = requests.get(file["raw_url"]) + with open(os.path.split(file_name)[1], "w", encoding="utf-8") as temp: + temp.write(Globals.response_buffer.text) + + +def list_source_files(ext_list: list, ignored_paths: list, not_ignored: list) -> bool: + """Make a list of source files to be checked. The resulting list is stored in + [`FILES`][python_action.__init__.Globals.FILES]. + + Args: + ext_list: A list of file extensions that should by attended. + ignored_paths: A list of paths to explicitly ignore. + not_ignored: A list of paths to explicitly not ignore. + + Returns: + True if there are files to check. False will invoke a early exit (in + [`main()`][python_action.run.main]) when no files to be checked. + """ + if os.path.exists(".gitmodules"): + submodules = configparser.ConfigParser() + submodules.read(".gitmodules") + for module in submodules.sections(): + logger.info( + "Apending submodule to ignored paths: %s", submodules[module]["path"] + ) + ignored_paths.append(submodules[module]["path"]) + + root_path = os.getcwd() + for dirpath, _, filenames in os.walk(root_path): + path = dirpath.replace(root_path, "").lstrip(os.sep) + if path.startswith("."): + # logger.debug("Skipping \"%s\"", path) + continue # skip sources in hidden directories + logger.debug('Crawling "./%s"', path) + for file in filenames: + if os.path.splitext(file)[1][1:] in ext_list: + file_path = os.path.join(path, file) + logger.debug('"./%s" is a source code file', file_path) + if not is_file_in_list( + ignored_paths, file_path, "ignored" + ) or is_file_in_list(not_ignored, file_path, "not ignored"): + Globals.FILES.append({"filename": file_path}) + + if Globals.FILES: + logger.info( + "Giving attention to the following files:\n\t%s", + "\n\t".join([f["filename"] for f in Globals.FILES]), + ) + else: + logger.info("No source files found.") # this might need to be warning + return False + return True + + +def run_clang_tidy( + filename: str, file_obj: dict, version: str, checks: str, lines_changed_only: bool +) -> None: + """Run clang-tidy on a certain file. + + Args: + filename: The name of the local file to run clang-tidy on. + file_obj: JSON info about the file. + version: The version of clang-tidy to run. + checks: The `str` of comma-separated regulate expressions that describe + the desired clang-tidy checks to be enabled/configured. + lines_changed_only: A flag that forces focus on only changes in the event's + diff info. + """ + cmds = [f"clang-tidy-{version}"] + if sys.platform.startswith("win32"): + cmds = ["clang-tidy"] + if checks: + cmds.append(f"-checks={checks}") + cmds.append("--export-fixes=clang_tidy_output.yml") + # cmds.append(f"--format-style={style}") + if lines_changed_only: + logger.info("line_filter = %s", json.dumps(file_obj["line_filter"]["lines"])) + cmds.append(f"--line-filter={json.dumps([file_obj['line_filter']])}") + cmds.append(filename.replace("/", os.sep)) + with open("clang_tidy_output.yml", "wb"): + pass # clear yml file's content before running clang-tidy + results = subprocess.run(cmds, capture_output=True) + with open("clang_tidy_report.txt", "wb") as f_out: + f_out.write(results.stdout) + logger.debug("Output from clang-tidy:\n%s", results.stdout.decode()) + if os.path.getsize("clang_tidy_output.yml"): + parse_tidy_suggestions_yml() # get clang-tidy fixes from yml + if results.returncode: + logger.warning( + "%s raised the following error(s):\n%s", cmds[0], results.stderr.decode() + ) + + +def run_clang_format( + filename: str, file_obj: dict, version: str, style: str, lines_changed_only: bool +) -> None: + """Run clang-format on a certain file + + Args: + filename: The name of the local file to run clang-format on. + file_obj: JSON info about the file. + version: The version of clang-format to run. + style: The clang-format style rules to adhere. Set this to 'file' to + use the relative-most .clang-format configuration file. + lines_changed_only: A flag that forces focus on only changes in the event's + diff info. + """ + cmds = [ + "clang-format" + ("" if sys.platform.startswith("win32") else f"-{version}"), + f"-style={style}", + "--output-replacements-xml", + ] + if lines_changed_only: + for line_range in file_obj["line_filter"]["lines"]: + cmds.append(f"--lines={line_range[0]}:{line_range[1]}") + cmds.append(filename.replace("/", os.sep)) + results = subprocess.run(cmds, capture_output=True) + with open("clang_format_output.xml", "wb") as f_out: + f_out.write(results.stdout) + if results.stdout: + logger.debug("clang-format has suggestions.") + if results.returncode: + logger.warning( + "%s raised the following error(s):\n%s", cmds[0], results.stderr.decode() + ) + + +def capture_clang_tools_output( + version: str, checks: str, style: str, lines_changed_only: bool +): + """Execute and capture all output from clang-tidy and clang-format. This aggregates + results in the [`OUTPUT`][python_action.__init__.Globals.OUTPUT]. + + Args: + version: The version of clang-tidy to run. + checks: The `str` of comma-separated regulate expressions that describe + the desired clang-tidy checks to be enabled/configured. + style: The clang-format style rules to adhere. Set this to 'file' to + use the relative-most .clang-format configuration file. + lines_changed_only: A flag that forces focus on only changes in the event's + diff info. + """ + tidy_notes = [] # temporary cache of parsed notifications for use in log commands + for file in ( + Globals.FILES + if GITHUB_EVENT_NAME == "pull_request" or isinstance(Globals.FILES, list) + else Globals.FILES["files"] + ): + filename = file["filename"] + if not os.path.exists(file["filename"]): + filename = os.path.split(file["raw_url"])[1] + start_log_group(f"Performing checkup on {filename}") + run_clang_tidy(filename, file, version, checks, lines_changed_only) + run_clang_format(filename, file, version, style, lines_changed_only) + end_log_group() + if os.path.getsize("clang_tidy_report.txt"): + parse_tidy_output() # get clang-tidy fixes from stdout + if Globals.PAYLOAD_TIDY: + Globals.PAYLOAD_TIDY += "
" + Globals.PAYLOAD_TIDY += f"
{filename}
\n" + for fix in GlobalParser.tidy_notes: + Globals.PAYLOAD_TIDY += repr(fix) + for note in GlobalParser.tidy_notes: + tidy_notes.append(note) + GlobalParser.tidy_notes.clear() # empty list to avoid duplicated output + + if os.path.getsize("clang_format_output.xml"): + parse_format_replacements_xml(filename.replace("/", os.sep)) + if not Globals.OUTPUT: + Globals.OUTPUT = "\n## :scroll: " + Globals.OUTPUT += "Run `clang-format` on the following files\n" + Globals.OUTPUT += f"- [ ] {file['filename']}\n" + + if Globals.PAYLOAD_TIDY: + if not Globals.OUTPUT: + Globals.OUTPUT = "\n" + else: + Globals.OUTPUT += "\n---\n" + Globals.OUTPUT += "## :speech_balloon: Output from `clang-tidy`\n" + Globals.OUTPUT += Globals.PAYLOAD_TIDY + GlobalParser.tidy_notes = tidy_notes[:] # restore cache of notifications + + +def post_push_comment(base_url: str, user_id: int) -> bool: + """POST action's results for a push event. + + Args: + base_url: The root of the url used to interact with the REST API via `requests`. + user_id: The user's account ID number. + + Returns: + A bool describing if the linter checks passed. This is used as the action's + output value (a soft exit code). + """ + comments_url = base_url + f"commits/{GITHUB_SHA}/comments" + remove_bot_comments(comments_url, user_id) + + if Globals.OUTPUT: # diff comments are not supported for push events (yet) + payload = json.dumps({"body": Globals.OUTPUT}) + logger.debug("payload body:\n%s", json.dumps({"body": Globals.OUTPUT})) + Globals.response_buffer = requests.post( + comments_url, headers=API_HEADERS, data=payload + ) + logger.info( + "Got %d response from POSTing comment", Globals.response_buffer.status_code + ) + log_response_msg() + return bool(Globals.OUTPUT) + + +def post_diff_comments(base_url: str, user_id: int) -> bool: + """Post comments inside a unified diff (only PRs are supported). + + Args: + base_url: The root of the url used to interact with the REST API via `requests`. + user_id: The user's account ID number. + + Returns: + A bool describing if the linter checks passed. This is used as the action's + output value (a soft exit code). + """ + comments_url = base_url + "pulls/comments/" # for use with comment_id + payload = list_diff_comments() + logger.info("Posting %d comments", len(payload)) + + # uncomment the next 3 lines for debug output without posting a comment + # for i, comment in enumerate(payload): + # logger.debug("comments %d: %s", i, json.dumps(comment, indent=2)) + # return + + # get existing review comments + reviews_url = base_url + f'pulls/{Globals.EVENT_PAYLOAD["number"]}/' + Globals.response_buffer = requests.get(reviews_url + "comments") + existing_comments = json.loads(Globals.response_buffer.text) + # filter out comments not made by our bot + for index, comment in enumerate(existing_comments): + if not comment["body"].startswith(""): + del existing_comments[index] + + # conditionally post comments in the diff + for i, body in enumerate(payload): + # check if comment is already there + already_posted = False + comment_id = None + for comment in existing_comments: + if ( + int(comment["user"]["id"]) == user_id + and comment["line"] == body["line"] + and comment["path"] == payload[i]["path"] + ): + already_posted = True + if comment["body"] != body["body"]: + comment_id = str(comment["id"]) # use this to update comment + else: + break + if already_posted and comment_id is None: + logger.info("comment %d already posted", i) + continue # don't bother reposting the same comment + + # update ot create a review comment (in the diff) + logger.debug("Payload %d body = %s", i, json.dumps(body)) + if comment_id is not None: + Globals.response_buffer = requests.patch( + comments_url + comment_id, + headers=API_HEADERS, + data=json.dumps({"body": body["body"]}), + ) + logger.info( + "Got %d from PATCHing comment %d (%d)", + Globals.response_buffer.status_code, + i, + comment_id, + ) + log_response_msg() + else: + Globals.response_buffer = requests.post( + reviews_url + "comments", headers=API_HEADERS, data=json.dumps(body) + ) + logger.info( + "Got %d from POSTing review comment %d", + Globals.response_buffer.status_code, + i, + ) + log_response_msg() + return bool(payload) + + +def post_pr_comment(base_url: str, user_id: int) -> bool: + """POST action's results for a push event. + + Args: + base_url: The root of the url used to interact with the REST API via `requests`. + user_id: The user's account ID number. + + Returns: + A bool describing if the linter checks passed. This is used as the action's + output value (a soft exit code). + """ + comments_url = base_url + f'issues/{Globals.EVENT_PAYLOAD["number"]}/comments' + remove_bot_comments(comments_url, user_id) + payload = "" + if Globals.OUTPUT: + payload = json.dumps({"body": Globals.OUTPUT}) + logger.debug( + "payload body:\n%s", json.dumps({"body": Globals.OUTPUT}, indent=2) + ) + Globals.response_buffer = requests.post( + comments_url, headers=API_HEADERS, data=payload + ) + logger.info("Got %d from POSTing comment", Globals.response_buffer.status_code) + log_response_msg() + return bool(payload) + + +def post_results(use_diff_comments: bool, user_id: int = 41898282): + """Post action's results using REST API. + + Args: + use_diff_comments: This flag enables making/updating comments in the PR's diff + info. + user_id: The user's account ID number. Defaults to the generic bot's ID. + """ + if not GITHUB_TOKEN: + logger.error("The GITHUB_TOKEN is required!") + sys.exit(set_exit_code(1)) + + base_url = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/" + checks_passed = True + if GITHUB_EVENT_NAME == "pull_request": + checks_passed = post_pr_comment(base_url, user_id) + if use_diff_comments: + checks_passed = post_diff_comments(base_url, user_id) + elif GITHUB_EVENT_NAME == "push": + checks_passed = post_push_comment(base_url, user_id) + set_exit_code(1 if checks_passed else 0) + + +def make_annotations(style: str) -> bool: + """Use github log commands to make annotations from clang-format and + clang-tidy output. + + Args: + style: The chosen code style guidelines. The value 'file' is replaced with + 'custom style'. + """ + # log_commander obj's verbosity is hard-coded to show debug statements + ret_val = False + for note in GlobalParser.tidy_notes: + ret_val = True + log_commander.info(note.log_command()) + for note in GlobalParser.format_advice: + ret_val = True + log_commander.info(note.log_command(style)) + return ret_val + + +def main(): + """The main script.""" + + # The parsed CLI args + args = cli_arg_parser.parse_args() + + # set logging verbosity + logger.setLevel(int(args.verbosity)) + + # prepare ignored paths list + ignored, not_ignored = ([], []) + if args.ignore is not None: + args.ignore = args.ignore.split("|") + for path in args.ignore: + path = path.lstrip("./") # relative dir is assumed + path = path.strip() # strip leading/trailing spaces + if path.startswith("!"): + not_ignored.append(path[1:]) + else: + ignored.append(path) + + # prepare extensions list + args.extensions = args.extensions.split(",") + + logger.info("processing %s event", GITHUB_EVENT_NAME) + + # load event's json info about the workflow run + with open(GITHUB_EVEN_PATH, "r", encoding="utf-8") as payload: + Globals.EVENT_PAYLOAD = json.load(payload) + if logger.getEffectiveLevel() <= logging.DEBUG: + start_log_group("Event json from the runner") + logger.debug(json.dumps(Globals.EVENT_PAYLOAD)) + end_log_group() + + # change working directory + os.chdir(args.repo_root) + + start_log_group("Get list of specified source files") + if ignored: + logger.info( + "Ignoring the following paths/files:\n\t%s", + "\n\t./".join(f for f in ignored), + ) + if not_ignored: + logger.info( + "Not ignoring the following paths/files:\n\t%s", + "\n\t./".join(f for f in not_ignored), + ) + exit_early = False + if args.files_changed_only: + get_list_of_changed_files() + exit_early = not filter_out_non_source_files( + args.extensions, + ignored, + not_ignored, + args.lines_changed_only if args.files_changed_only else False, + ) + if not exit_early: + verify_files_are_present() + else: + exit_early = not list_source_files(args.extensions, ignored, not_ignored) + end_log_group() + if exit_early: + sys.exit(set_exit_code(0)) + + capture_clang_tools_output( + args.version, args.tidy_checks, args.style, args.lines_changed_only + ) + + start_log_group("Posting comment(s)") + if args.thread_comments: + post_results(False) # False is hard-coded to disable diff comments. + set_exit_code(int(make_annotations(args.style))) + end_log_group() + + +if __name__ == "__main__": + main() diff --git a/python_action/thread_comments.py b/python_action/thread_comments.py new file mode 100644 index 00000000..415c586a --- /dev/null +++ b/python_action/thread_comments.py @@ -0,0 +1,260 @@ +"""A module to house the various functions for traversing/adjusting comments""" +import os +from typing import Union +import json +import requests +from . import Globals, GlobalParser, logger, API_HEADERS, GITHUB_SHA, log_response_msg + + +def remove_bot_comments(comments_url: str, user_id: int): + """Traverse the list of comments made by a specific user + and remove all. + + Args: + comments_url: The URL used to fetch the comments. + user_id: The user's account id number. + """ + logger.info("comments_url: %s", comments_url) + Globals.response_buffer = requests.get(comments_url) + comments = Globals.response_buffer.json() + for i, comment in enumerate(comments): + # only serach for comments from the user's ID and + # whose comment body begins with a specific html comment + if ( + int(comment["user"]["id"]) == user_id + # the specific html comment is our action's name + and comment["body"].startswith("") + ): + # remove other outdated comments but don't remove the last comment + Globals.response_buffer = requests.delete( + comment["url"], + headers=API_HEADERS, + ) + logger.info( + "Got %d from DELETE %s", + Globals.response_buffer.status_code, + comment["url"][comment["url"].find(".com") + 4 :], + ) + log_response_msg() + del comments[i] + logger.debug( + "comment id %d from user %s (%d)", + comment["id"], + comment["user"]["login"], + comment["user"]["id"], + ) + with open("comments.json", "w", encoding="utf-8") as json_comments: + json.dump(comments, json_comments, indent=4) + + +def aggregate_tidy_advice() -> list: + """Aggregate a list of json contents representing advice from clang-tidy + suggestions.""" + results = [] + for index, fixit in enumerate(GlobalParser.tidy_advice): + for diag in fixit.diagnostics: + # base body of comment + body = "\n## :speech_balloon: Clang-tidy\n**" + body += diag.name + "**\n>" + diag.message + + # get original code + filename = Globals.FILES[index]["filename"].replace("/", os.sep) + if not os.path.exists(filename): + # the file had to be downloaded (no git checkout). + # thus use only the filename (without the path to the file) + filename = os.path.split(filename)[1] + lines = [] # the list of lines in a file + with open(filename, encoding="utf-8") as temp: + lines = temp.readlines() + + # aggregate clang-tidy advice + suggestion = "\n```suggestion\n" + is_multiline_fix = False + fix_lines = [] # a list of line numbers for the suggested fixes + line = "" # the line that concerns the fix/comment + for i, tidy_fix in enumerate(diag.replacements): + line = lines[tidy_fix.line - 1] + if not fix_lines: + fix_lines.append(tidy_fix.line) + elif tidy_fix.line not in fix_lines: + is_multiline_fix = True + break + if i: # if this isn't the first tidy_fix for the same line + last_fix = diag.replacements[i - 1] + suggestion += ( + line[last_fix.cols + last_fix.null_len - 1 : tidy_fix.cols - 1] + + tidy_fix.text.decode() + ) + else: + suggestion += line[: tidy_fix.cols - 1] + tidy_fix.text.decode() + if not is_multiline_fix and diag.replacements: + # complete suggestion with original src code and closing md fence + last_fix = diag.replacements[len(diag.replacements) - 1] + suggestion += line[last_fix.cols + last_fix.null_len - 1 : -1] + "\n```" + body += suggestion + + results.append( + { + "body": body, + "commit_id": GITHUB_SHA, + "line": diag.line, + "path": fixit.filename, + "side": "RIGHT", + } + ) + return results + + +def aggregate_format_advice() -> list: + """Aggregate a list of json contents representing advice from clang-format + suggestions.""" + results = [] + for index, fmt_advice in enumerate(GlobalParser.format_advice): + + # get original code + filename = Globals.FILES[index]["filename"].replace("/", os.sep) + if not os.path.exists(filename): + # the file had to be downloaded (no git checkout). + # thus use only the filename (without the path to the file) + filename = os.path.split(filename)[1] + lines = [] # the list of lines from the src file + with open(filename, encoding="utf-8") as temp: + lines = temp.readlines() + + # aggregate clang-format suggestion + line = "" # the line that concerns the fix + for fixed_line in fmt_advice.replaced_lines: + # clang-format can include advice that starts/ends outside the diff's domain + in_range = False + ranges = Globals.FILES[index]["line_filter"]["lines"] + for scope in ranges: + if fixed_line.line in range(scope[0], scope[1] + 1): + in_range = True + if not in_range: + continue # line is out of scope for diff, so skip this fix + + # assemble the suggestion + body = "## :scroll: clang-format advice\n```suggestion\n" + line = lines[fixed_line.line - 1] + # logger.debug("%d >>> %s", fixed_line.line, line[:-1]) + for fix_index, line_fix in enumerate(fixed_line.replacements): + # logger.debug( + # "%s >>> %s", repr(line_fix), line_fix.text.encode("utf-8") + # ) + if fix_index: + last_fix = fixed_line.replacements[fix_index - 1] + body += line[ + last_fix.cols + last_fix.null_len - 1 : line_fix.cols - 1 + ] + body += line_fix.text + else: + body += line[: line_fix.cols - 1] + line_fix.text + # complete suggestion with original src code and closing md fence + last_fix = fixed_line.replacements[-1] + body += line[last_fix.cols + last_fix.null_len - 1 : -1] + "\n```" + # logger.debug("body <<< %s", body) + + # create a suggestion from clang-format advice + results.append( + { + "body": body, + "commit_id": GITHUB_SHA, + "line": fixed_line.line, + "path": fmt_advice.filename, + "side": "RIGHT", + } + ) + return results + + +def concatenate_comments(tidy_advice: list, format_advice: list) -> list: + """Concatenate comments made to the same line of the same file.""" + # traverse comments from clang-format + for index, comment_body in enumerate(format_advice): + # check for comments from clang-tidy on the same line + comment_index = None + for i, payload in enumerate(tidy_advice): + if ( + payload["line"] == comment_body["line"] + and payload["path"] == comment_body["path"] + ): + comment_index = i # mark this comment for concatenation + break + if comment_index is not None: + # append clang-format advice to clang-tidy output/suggestion + tidy_advice[comment_index]["body"] += "\n" + comment_body["body"] + del format_advice[index] # remove duplicate comment + return tidy_advice + format_advice + + +def list_diff_comments() -> list: + """Aggregate list of comments for use in the event's diff. This function assumes + that the CLI option `--diff-only` is set to True. + + Returns: + A list of comments (each element as json content). + """ + tidy_advice = aggregate_tidy_advice() + format_advice = aggregate_format_advice() + results = concatenate_comments(tidy_advice, format_advice) + return results + + +def get_review_id(reviews_url: str, user_id: int) -> int: + """Dismiss all stale reviews (only the ones made by our bot). + + Args: + reviews_url: The URL used to fetch the review comments. + user_id: The user's account id number. + Returns: + The ID number of the review created by the action's generic bot. + """ + logger.info(" review_url: %s", reviews_url) + Globals.response_buffer = requests.get(reviews_url) + review_id = find_review(json.loads(Globals.response_buffer.text), user_id) + if review_id is None: # create a PR review + Globals.response_buffer = requests.post( + reviews_url, + headers=API_HEADERS, + data=json.dumps( + { + "body": "\n" + "CPP Linter Action found no problems", + "event": "COMMENTED", + } + ), + ) + logger.info( + "Got %d from POSTing new(/temp) PR review", + Globals.response_buffer.status_code, + ) + Globals.response_buffer = requests.get(reviews_url) + if Globals.response_buffer.status_code != 200: + log_response_msg() + raise RuntimeError("could not create a review for commemts") + reviews = json.loads(Globals.response_buffer.text) + reviews.reverse() # traverse the list in reverse + review_id = find_review(reviews, user_id) + return review_id + + +def find_review(reviews: dict, user_id: int) -> Union[int, None]: + """Find a review created by a certain user ID. + + Args: + reviews: the JSON object fetched via GIT REST API. + user_id: The user account's ID number + + Returns: + An ID that corresponds to the specified `user_id`. + """ + review_id = None + for review in reviews: + if int(review["user"]["id"]) == user_id and review["body"].startswith( + "" + ): + review_id = int(review["id"]) + break # there will only be 1 review from this action, so break when found + + logger.info(" review_id: %d", review_id) + return review_id diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..ae1f79e0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests +pyyaml diff --git a/runchecks.sh b/runchecks.sh index 44eaf625..e3f75de8 100644 --- a/runchecks.sh +++ b/runchecks.sh @@ -8,6 +8,7 @@ OUTPUT="" URLS="" PATHNAMES="" declare -a JSON_INDEX +FILES_LINK="" # alias CLI args args=("$@") @@ -39,9 +40,9 @@ set_exit_code () { # Fetch JSON of event's changed files ################################################### get_list_of_changed_files() { - echo "GH_EVENT_PATH = $GITHUB_EVENT_PATH" + # echo "GH_EVENT_PATH = $GITHUB_EVENT_PATH" echo "processing $GITHUB_EVENT_NAME event" - # cat "$GITHUB_EVENT_PATH" | jq '.' + jq '.' "$GITHUB_EVENT_PATH" # Use git REST API payload if [[ "$GITHUB_EVENT_NAME" == "push" ]] @@ -207,7 +208,7 @@ capture_clang_tools_output() { then if [ "$OUTPUT" == "" ] then - OUTPUT=$'## Run `clang-format` on the following files\n' + OUTPUT=$'\n## Run `clang-format` on the following files\n' fi OUTPUT+="- [ ] ${PATHNAMES[index]}"$'\n' fi @@ -235,13 +236,19 @@ post_results() { fi COMMENTS_URL=$(jq -r .pull_request.comments_url "$GITHUB_EVENT_PATH") + COMMENT_COUNT=$(jq -r .comments "$GITHUB_EVENT_PATH") if [[ "$GITHUB_EVENT_NAME" == "push" ]] then COMMENTS_URL="$FILES_LINK/comments" + COMMENT_COUNT=$(jq -r .commit.comment_count .cpp_linter_action_changed_files.json) + fi + echo "COMMENTS_URL: $COMMENTS_URL" + echo "Number of Comments = $COMMENT_COUNT" + if [[ $COMMENT_COUNT -gt 0 ]] + then + # get the list of comments + curl "$COMMENTS_URL" > ".comments.json" fi - - echo "COMMENTS_URL = $COMMENTS_URL" - PAYLOAD=$(echo '{}' | jq --arg body "$OUTPUT" '.body = $body') # creating PR comments is the same API as creating issue. Creating commit comments have more optional parameters (but same required API) diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..24de2052 --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +"""Bootstrapper for docker's ENTRYPOINT executable.""" +import os +from setuptools import setup + + +ROOT_DIR = os.path.abspath(os.path.dirname(__file__)) +REPO = "https://github.com/" +repo = os.getenv("GITHUB_REPOSITORY", None) # in case this is published from a fork +REPO += "" if repo is None else repo +if repo is None: + REPO += "2bndy5/cpp-linter-action" + + +setup( + name="python_action", + # use_scm_version=True, + # setup_requires=["setuptools_scm"], + version="v1.2.1", + description=__doc__, + long_description=".. warning:: this is not meant for PyPi (yet)", + author="Brendan Doherty", + author_email="2bndy5@gmail.com", + install_requires=["requests"], #, "pyyaml"], # pyyaml is installed with clang-tidy + license="MIT", + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + "Development Status :: 1 - Production/Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + ], + keywords="clang clang-tidy clang-format", + packages=["python_action"], + + entry_points={"console_scripts": ["run-action=python_action.run:main"]}, + # Specifiy your homepage URL for your project here + url=REPO, + download_url=f"{REPO}/releases", +) From d5c898525c026638b0f23d952cb6179a15db7191 Mon Sep 17 00:00:00 2001 From: Peter Shen Date: Mon, 11 Oct 2021 07:39:19 -0600 Subject: [PATCH 06/43] Remove duplicate name from README.md --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index cc112d95..a94e39eb 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,8 @@ Create a new GitHub Actions workflow in your project, e.g. at [.github/workflows The content of the file should be in the following format. ```yaml -name: cpp-linter - # Workflow syntax: # https://help.github.com/en/articles/workflow-syntax-for-github-actions - name: cpp-linter on: From 02781e84f37ebddfc566e7a770e7bf373025208b Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Mon, 11 Oct 2021 07:24:58 -0700 Subject: [PATCH 07/43] remove dev artifacts (#28) * remove dev artifacts * Remove trailing whitespace * Update README.md --- .github/workflows/cpp-linter.yml | 3 --- README.md | 3 --- 2 files changed, 6 deletions(-) diff --git a/.github/workflows/cpp-linter.yml b/.github/workflows/cpp-linter.yml index 4eba50bd..1125f995 100644 --- a/.github/workflows/cpp-linter.yml +++ b/.github/workflows/cpp-linter.yml @@ -17,9 +17,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: style: file - files-changed-only: false - # to ignore all demo folder contents except for demo.cpp - # ignore: demo|!demo/demo.cpp - name: Fail fast?! if: steps.linter.outputs.checks-failed > 0 diff --git a/README.md b/README.md index a94e39eb..55f24f23 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: style: file - files-changed-only: false - # to ignore all demo folder contents except for demo.cpp - # ignore: demo|!demo/demo.cpp - name: Fail fast?! if: steps.linter.outputs.checks-failed > 0 From b4e9991cb7c37092c2344979d07273a2922d8453 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 20 Oct 2021 07:55:04 -0700 Subject: [PATCH 08/43] field testing revealed various bugs (#30) * upload new demo image * Revert "upload new demo image" This reverts commit 1aff6c70c69acc366cdddd69358a173a502fe31b. * update readme & upload new demo pic * avoids duplicated checks on commits to open PR * fix workflow from last commit * Revert "fix workflow from last commit" This reverts commit 778e10dc42714dfb46a2ef1065162ac4969ab549. * rename py pkg; allow no clang-tidy & no event.json * pleasing pylint * various bug fixes * pleasing pylint * update README about `tidy-checks=-*` * increase indent in last change * increase indent again (I don't like mkdocs) * update docs * avoid nesting log groups * switch pylint to my check-python-sources action * trigger pylint action * Revert "switch pylint to my check-python-sources action" This reverts commit 1733c4f9879b036c497ca6b0c3230ce580a525ec. --- .ci-ignore | 2 +- .github/workflows/build-docs.bak | 32 ------- .github/workflows/run-pylint.yml | 2 +- Dockerfile | 4 +- README.md | 1 + {python_action => cpp_linter}/__init__.py | 24 +++--- .../clang_format_xml.py | 10 +-- {python_action => cpp_linter}/clang_tidy.py | 30 +++---- .../clang_tidy_yml.py | 10 +-- {python_action => cpp_linter}/run.py | 81 +++++++++++------- .../thread_comments.py | 0 ..._xml.md => cpp_linter.clang_format_xml.md} | 2 +- docs/API Reference/cpp_linter.clang_tidy.md | 3 + ...dy_yml.md => cpp_linter.clang_tidy_yml.md} | 2 +- docs/API Reference/cpp_linter.md | 3 + docs/API Reference/cpp_linter.run.md | 3 + .../cpp_linter.thread_comments.md | 3 + .../API Reference/python_action.clang_tidy.md | 3 - docs/API Reference/python_action.md | 3 - docs/API Reference/python_action.run.md | 3 - .../python_action.thread_comments.md | 3 - img/demo_comment.png | Bin 0 -> 42823 bytes mkdocs.yml | 14 +-- setup.py | 11 ++- 24 files changed, 114 insertions(+), 135 deletions(-) delete mode 100644 .github/workflows/build-docs.bak rename {python_action => cpp_linter}/__init__.py (76%) rename {python_action => cpp_linter}/clang_format_xml.py (93%) rename {python_action => cpp_linter}/clang_tidy.py (79%) rename {python_action => cpp_linter}/clang_tidy_yml.py (92%) rename {python_action => cpp_linter}/run.py (91%) rename {python_action => cpp_linter}/thread_comments.py (100%) rename docs/API Reference/{python_action.clang_format_xml.md => cpp_linter.clang_format_xml.md} (74%) create mode 100644 docs/API Reference/cpp_linter.clang_tidy.md rename docs/API Reference/{python_action.clang_tidy_yml.md => cpp_linter.clang_tidy_yml.md} (75%) create mode 100644 docs/API Reference/cpp_linter.md create mode 100644 docs/API Reference/cpp_linter.run.md create mode 100644 docs/API Reference/cpp_linter.thread_comments.md delete mode 100644 docs/API Reference/python_action.clang_tidy.md delete mode 100644 docs/API Reference/python_action.md delete mode 100644 docs/API Reference/python_action.run.md delete mode 100644 docs/API Reference/python_action.thread_comments.md create mode 100644 img/demo_comment.png diff --git a/.ci-ignore b/.ci-ignore index afba914c..b9610532 100644 --- a/.ci-ignore +++ b/.ci-ignore @@ -1,2 +1,2 @@ -python_action +cpp_linter mkdocs.yml diff --git a/.github/workflows/build-docs.bak b/.github/workflows/build-docs.bak deleted file mode 100644 index 721744b3..00000000 --- a/.github/workflows/build-docs.bak +++ /dev/null @@ -1,32 +0,0 @@ -name: dev-docs - -on: - push: - pull_request: - types: [opened] - -jobs: - using-mkdocs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v1 - with: - python-version: 3.7 - - name: Install doc's deps - run: | - python3 -m pip install -r docs/requirements.txt - python3 -m pip install . - - name: Build docs - run: mkdocs build - - name: Save artifact - uses: actions/upload-artifact@v2 - with: - name: "cpp linter action dev docs" - path: site/** - - name: upload to github pages - if: ${{ github.event_name == 'release'}} - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: site diff --git a/.github/workflows/run-pylint.yml b/.github/workflows/run-pylint.yml index 3ef7c8bd..0d103b95 100644 --- a/.github/workflows/run-pylint.yml +++ b/.github/workflows/run-pylint.yml @@ -25,5 +25,5 @@ jobs: python3 -m pip install -r requirements.txt - name: run pylint run: | - pylint python_action/** + pylint cpp_linter/** pylint setup.py diff --git a/Dockerfile b/Dockerfile index 9c0a20f5..43fb1064 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,11 +16,11 @@ RUN apt-get update RUN apt-get -y install python3-pip # RUN python3 -m pip install --upgrade pip -COPY python_action/ pkg/python_action/ +COPY cpp_linter/ pkg/cpp_linter/ COPY setup.py pkg/setup.py RUN python3 -m pip install pkg/ # github action args use the CMD option # See https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsargs # also https://docs.docker.com/engine/reference/builder/#cmd -ENTRYPOINT [ "python3", "-m", "python_action.run" ] +ENTRYPOINT [ "python3", "-m", "cpp_linter.run" ] diff --git a/README.md b/README.md index 55f24f23..dab2bce8 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ jobs: #### `tidy-checks` - **Description**: Comma-separated list of globs with optional '-' prefix. Globs are processed in order of appearance in the list. Globs without '-' prefix add checks with matching names to the set, globs with the '-' prefix remove checks with matching names from the set of enabled checks. This option's value is appended to the value of the 'Checks' option in a .clang-tidy file (if any). + - It is possible to disable clang-tidy entirely by setting this option to '-\*'. This allows using only clang-format to lint your source files. - Default: 'boost-\*,bugprone-\*,performance-\*,readability-\*,portability-\*,modernize-\*,clang-analyzer-\*,cppcoreguidelines-\*' #### `repo-root` diff --git a/python_action/__init__.py b/cpp_linter/__init__.py similarity index 76% rename from python_action/__init__.py rename to cpp_linter/__init__.py index 613a5a2f..ae8d6e96 100644 --- a/python_action/__init__.py +++ b/cpp_linter/__init__.py @@ -1,4 +1,4 @@ -"""The Base module of the `python_action` package. This holds the objects shared by +"""The Base module of the `cpp_linter` package. This holds the objects shared by multiple modules.""" import io import os @@ -24,7 +24,7 @@ logger.debug("rich module not found") # global constant variables -GITHUB_SHA = os.getenv("GITHUB_SHA", "95915a282b3efcad67b9ad3f95fba1501e43ab22") +GITHUB_SHA = os.getenv("GITHUB_SHA", "") GITHUB_TOKEN = os.getenv("GITHUB_TOKEN", os.getenv("GIT_REST_API", "")) API_HEADERS = { "Authorization": f"token {GITHUB_TOKEN}", @@ -54,13 +54,13 @@ class GlobalParser: tidy_notes = [] """This can only be a `list` of type - [`TidyNotification`][python_action.clang_tidy.TidyNotification]""" + [`TidyNotification`][cpp_linter.clang_tidy.TidyNotification]""" tidy_advice = [] """This can only be a `list` of type - [`YMLFixit`][python_action.clang_tidy_yml.YMLFixit]""" + [`YMLFixit`][cpp_linter.clang_tidy_yml.YMLFixit]""" format_advice = [] """This can only be a `list` of type - [`XMLFixit`][python_action.clang_format_xml.XMLFixit]""" + [`XMLFixit`][cpp_linter.clang_format_xml.XMLFixit]""" def get_line_cnt_from_cols(file_path: str, offset: int) -> tuple: @@ -80,24 +80,20 @@ def get_line_cnt_from_cols(file_path: str, offset: int) -> tuple: last_lf_pos = 0 cols = 1 file_path = file_path.replace("/", os.sep) - with io.open(file_path, "r", encoding="utf-8", newline="\n") as src_file: - src_file.seek(0, io.SEEK_END) - max_len = src_file.tell() + # logger.debug("Getting line count from %s at offset %d", file_path, offset) + with io.open(file_path, "rb") as src_file: + max_len = src_file.seek(0, io.SEEK_END) src_file.seek(0, io.SEEK_SET) while src_file.tell() != offset and src_file.tell() < max_len: char = src_file.read(1) - if char == "\n": + if char == b"\n": line_cnt += 1 last_lf_pos = src_file.tell() - 1 # -1 because LF is part of offset - if last_lf_pos + 1 > max_len: - src_file.newlines = "\r\n" - src_file.seek(0, io.SEEK_SET) - line_cnt = 1 cols = src_file.tell() - last_lf_pos return (line_cnt, cols) def log_response_msg(): - """Output the response buffer's message on failed request""" + """Output the response buffer's message on a failed request.""" if Globals.response_buffer.status_code >= 400: logger.error("response returned message: %s", Globals.response_buffer.text) diff --git a/python_action/clang_format_xml.py b/cpp_linter/clang_format_xml.py similarity index 93% rename from python_action/clang_format_xml.py rename to cpp_linter/clang_format_xml.py index ea98f0f2..3657c5ce 100644 --- a/python_action/clang_format_xml.py +++ b/cpp_linter/clang_format_xml.py @@ -37,7 +37,7 @@ class FormatReplacementLine: Attributes: line (int): The line number of where the suggestion starts replacements (list): A list of - [`FormatReplacement`][python_action.clang_format_xml.FormatReplacement] + [`FormatReplacement`][cpp_linter.clang_format_xml.FormatReplacement] object(s) representing suggestions. """ @@ -63,7 +63,7 @@ class XMLFixit: filename (str): The source file that the suggestion concerns. replaced_lines (list): A list of [`FormatReplacementLine`][ - python_action.clang_format_xml.FormatReplacementLine] + cpp_linter.clang_format_xml.FormatReplacementLine] representing replacement(s) on a single line. """ @@ -118,7 +118,7 @@ def log_command(self, style: str) -> str: def parse_format_replacements_xml(src_filename: str): """Parse XML output of replacements from clang-format. Output is saved to - [`format_advice`][python_action.__init__.GlobalParser.format_advice]. + [`format_advice`][cpp_linter.__init__.GlobalParser.format_advice]. Args: src_filename: The source file's name for which the contents of the xml @@ -145,8 +145,8 @@ def parse_format_replacements_xml(src_filename: str): def print_fixits(): - """Print all [`XMLFixit`][python_action.clang_format_xml.XMLFixit] objects in - [`format_advice`][python_action.__init__.GlobalParser.format_advice].""" + """Print all [`XMLFixit`][cpp_linter.clang_format_xml.XMLFixit] objects in + [`format_advice`][cpp_linter.__init__.GlobalParser.format_advice].""" for fixit in GlobalParser.format_advice: print(repr(fixit)) for line_fix in fixit.replaced_lines: diff --git a/python_action/clang_tidy.py b/cpp_linter/clang_tidy.py similarity index 79% rename from python_action/clang_tidy.py rename to cpp_linter/clang_tidy.py index f9a77bbb..55770b73 100644 --- a/python_action/clang_tidy.py +++ b/cpp_linter/clang_tidy.py @@ -1,9 +1,9 @@ """Parse output from clang-tidy's stdout""" import os -import sys import re -from . import GlobalParser +from . import GlobalParser # , logger +NOTE_HEADER = re.compile("^(.*):(\d+):(\d+):\s(\w+):(.*)\[(.*)\]$") class TidyNotification: """Create a object that decodes info from the clang-tidy output's initial line that @@ -20,27 +20,24 @@ class TidyNotification: notification. """ - def __init__(self, notification_line: str): + def __init__(self, notification_line: tuple): """ Args: - notification_line: The first line in the notification. + notification_line: The first line in the notification parsed into a tuple of + string that represent the different components of the notification's + details. """ - sliced_line = notification_line.split(":") - if sys.platform.startswith("win32") and len(sliced_line) > 5: - # sliced_list items 0 & 1 are the path seperated at the ":". - # we need to re-assemble the path for correct list expansion (see below) - sliced_line = [sliced_line[0] + ":" + sliced_line[1]] + sliced_line[2:] + # logger.debug("Creating tidy note from line %s", notification_line) ( self.filename, self.line, self.cols, self.note_type, self.note_info, - ) = sliced_line + self.diagnostic, + ) = notification_line - self.diagnostic = re.search("\[.*\]", self.note_info).group(0) - self.note_info = self.note_info.replace(self.diagnostic, "").strip() - self.diagnostic = self.diagnostic[1:-1] + self.note_info = self.note_info.strip() self.note_type = self.note_type.strip() self.line = int(self.line) self.cols = int(self.cols) @@ -90,8 +87,9 @@ def parse_tidy_output() -> None: notification = None with open("clang_tidy_report.txt", "r", encoding="utf-8") as tidy_out: for line in tidy_out.readlines(): - if re.search("^.*:\d+:\d+:\s\w+:.*\[.*\]$", line) is not None: - notification = TidyNotification(line) + match = re.match(NOTE_HEADER, line) + if match is not None: + notification = TidyNotification(match.groups()) GlobalParser.tidy_notes.append(notification) elif notification is not None: notification.fixit_lines.append(line) @@ -100,7 +98,7 @@ def parse_tidy_output() -> None: def print_fixits(): """Print out all clang-tidy notifications from stdout (which are saved to clang_tidy_report.txt and allocated to - [`tidy_notes`][python_action.__init__.GlobalParser.tidy_notes].""" + [`tidy_notes`][cpp_linter.__init__.GlobalParser.tidy_notes].""" for notification in GlobalParser.tidy_notes: print("found", len(GlobalParser.tidy_notes), "tidy_notes") print(repr(notification)) diff --git a/python_action/clang_tidy_yml.py b/cpp_linter/clang_tidy_yml.py similarity index 92% rename from python_action/clang_tidy_yml.py rename to cpp_linter/clang_tidy_yml.py index 89476f5e..9cb377b4 100644 --- a/python_action/clang_tidy_yml.py +++ b/cpp_linter/clang_tidy_yml.py @@ -20,7 +20,7 @@ class TidyDiagnostic: cols (int): The columns of the `line` that triggered the diagnostic null_len (int): The number of bytes replaced by suggestions replacements (list): The `list` of - [`TidyReplacement`][python_action.clang_tidy_yml.TidyReplacement] objects. + [`TidyReplacement`][cpp_linter.clang_tidy_yml.TidyReplacement] objects. """ @@ -79,7 +79,7 @@ class YMLFixit: Attributes: filename (str): The source file's name concerning the suggestion. diagnostics (list): The `list` of - [`TidyDiagnostic`][python_action.clang_tidy_yml.TidyDiagnostic] objects. + [`TidyDiagnostic`][cpp_linter.clang_tidy_yml.TidyDiagnostic] objects. """ def __init__(self, filename: str) -> None: @@ -99,7 +99,7 @@ def __repr__(self) -> str: def parse_tidy_suggestions_yml(): """Read a YAML file from clang-tidy and create a list of suggestions from it. - Output is saved to [`tidy_advice`][python_action.__init__.GlobalParser.tidy_advice]. + Output is saved to [`tidy_advice`][cpp_linter.__init__.GlobalParser.tidy_advice]. """ yml = {} with open("clang_tidy_output.yml", "r", encoding="utf-8") as yml_file: @@ -129,8 +129,8 @@ def parse_tidy_suggestions_yml(): def print_fixits(): - """Print all [`YMLFixit`][python_action.clang_tidy_yml.YMLFixit] objects in - [`tidy_advice`][python_action.__init__.GlobalParser.tidy_advice].""" + """Print all [`YMLFixit`][cpp_linter.clang_tidy_yml.YMLFixit] objects in + [`tidy_advice`][cpp_linter.__init__.GlobalParser.tidy_advice].""" for fix in GlobalParser.tidy_advice: for diag in fix.diagnostics: print(repr(diag)) diff --git a/python_action/run.py b/cpp_linter/run.py similarity index 91% rename from python_action/run.py rename to cpp_linter/run.py index e97d11bd..ea416cbf 100644 --- a/python_action/run.py +++ b/cpp_linter/run.py @@ -1,5 +1,5 @@ """Run clang-tidy and clang-format on a list of changed files provided by GitHub's -REST API. If executed from command-line, then [`main()`][python_action.run.main] is +REST API. If executed from command-line, then [`main()`][cpp_linter.run.main] is the entrypoint. !!! info "See Also" @@ -35,10 +35,10 @@ # global constant variables -GITHUB_EVEN_PATH = os.getenv("GITHUB_EVENT_PATH", "event_payload.json") +GITHUB_EVEN_PATH = os.getenv("GITHUB_EVENT_PATH", "") GITHUB_API_URL = os.getenv("GITHUB_API_URL", "https://api.github.com") -GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY", "2bndy5/cpp-linter-action") -GITHUB_EVENT_NAME = os.getenv("GITHUB_EVENT_NAME", "pull_request") +GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY", "") +GITHUB_EVENT_NAME = os.getenv("GITHUB_EVENT_NAME", "unknown") # setup CLI args cli_arg_parser = argparse.ArgumentParser( @@ -104,14 +104,14 @@ ) cli_arg_parser.add_argument( "--files-changed-only", - default="true", + default="false", type=lambda input: input.lower() == "true", help="Set this option to 'false' to analyse any source files in the repo. " "Defaults to %(default)s.", ) cli_arg_parser.add_argument( "--thread-comments", - default="true", + default="false", type=lambda input: input.lower() == "true", help="Set this option to false to disable the use of thread comments as feedback." "Defaults to %(default)s.", @@ -173,9 +173,11 @@ def is_file_in_list(paths: list, file_name: str, prompt: str) -> bool: result = os.path.commonpath([path, file_name]).replace(os.sep, "/") if result == path: logger.debug( - '"./%s" is %s as specified in the domain "./%s"', + '".%s%s" is %s as specified in the domain ".%s%s"', + os.sep, file_name, prompt, + os.sep, path, ) return True @@ -184,7 +186,8 @@ def is_file_in_list(paths: list, file_name: str, prompt: str) -> bool: def get_list_of_changed_files() -> None: """Fetch the JSON payload of the event's changed files. Sets the - [`FILES`][python_action.__init__.Globals.FILES] attribute.""" + [`FILES`][cpp_linter.__init__.Globals.FILES] attribute.""" + start_log_group("Get list of specified source files") files_link = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/" if GITHUB_EVENT_NAME == "pull_request": files_link += f"pulls/{Globals.EVENT_PAYLOAD['number']}/files" @@ -201,7 +204,7 @@ def filter_out_non_source_files( ext_list: list, ignored: list, not_ignored: list, lines_changed_only: bool ) -> bool: """Exclude undesired files (specified by user input 'extensions'). This filter - applies to the event's [`FILES`][python_action.__init__.Globals.FILES] attribute. + applies to the event's [`FILES`][cpp_linter.__init__.Globals.FILES] attribute. Args: ext_list: A list of file extensions that are to be examined. @@ -212,7 +215,7 @@ def filter_out_non_source_files( Returns: True if there are files to check. False will invoke a early exit (in - [`main()`][python_action.run.main]) when no files to be checked. + [`main()`][cpp_linter.run.main]) when no files to be checked. """ files = [] for file in ( @@ -296,7 +299,7 @@ def verify_files_are_present() -> None: def list_source_files(ext_list: list, ignored_paths: list, not_ignored: list) -> bool: """Make a list of source files to be checked. The resulting list is stored in - [`FILES`][python_action.__init__.Globals.FILES]. + [`FILES`][cpp_linter.__init__.Globals.FILES]. Args: ext_list: A list of file extensions that should by attended. @@ -305,8 +308,9 @@ def list_source_files(ext_list: list, ignored_paths: list, not_ignored: list) -> Returns: True if there are files to check. False will invoke a early exit (in - [`main()`][python_action.run.main]) when no files to be checked. + [`main()`][cpp_linter.run.main]) when no files to be checked. """ + start_log_group("Get list of specified source files") if os.path.exists(".gitmodules"): submodules = configparser.ConfigParser() submodules.read(".gitmodules") @@ -319,14 +323,20 @@ def list_source_files(ext_list: list, ignored_paths: list, not_ignored: list) -> root_path = os.getcwd() for dirpath, _, filenames in os.walk(root_path): path = dirpath.replace(root_path, "").lstrip(os.sep) - if path.startswith("."): - # logger.debug("Skipping \"%s\"", path) + path_parts = path.split(os.sep) + is_hidden = False + for part in path_parts: + if part.startswith("."): + # logger.debug("Skipping \".%s%s\"", os.sep, path) + is_hidden = True + break + if is_hidden: continue # skip sources in hidden directories - logger.debug('Crawling "./%s"', path) + logger.debug('Crawling ".%s%s"', os.sep, path) for file in filenames: if os.path.splitext(file)[1][1:] in ext_list: file_path = os.path.join(path, file) - logger.debug('"./%s" is a source code file', file_path) + logger.debug('".%s%s" is a source code file', os.sep, file_path) if not is_file_in_list( ignored_paths, file_path, "ignored" ) or is_file_in_list(not_ignored, file_path, "not ignored"): @@ -357,6 +367,10 @@ def run_clang_tidy( lines_changed_only: A flag that forces focus on only changes in the event's diff info. """ + if checks == "-*": # if all checks are disabled, then clang-tidy is skipped + # clear the clang-tidy output file and exit function + with open("clang_tidy_report.txt", "wb") as f_out: + return cmds = [f"clang-tidy-{version}"] if sys.platform.startswith("win32"): cmds = ["clang-tidy"] @@ -370,6 +384,7 @@ def run_clang_tidy( cmds.append(filename.replace("/", os.sep)) with open("clang_tidy_output.yml", "wb"): pass # clear yml file's content before running clang-tidy + logger.info('Running "%s"', " ".join(cmds)) results = subprocess.run(cmds, capture_output=True) with open("clang_tidy_report.txt", "wb") as f_out: f_out.write(results.stdout) @@ -405,11 +420,10 @@ def run_clang_format( for line_range in file_obj["line_filter"]["lines"]: cmds.append(f"--lines={line_range[0]}:{line_range[1]}") cmds.append(filename.replace("/", os.sep)) + logger.info('Running "%s"', " ".join(cmds)) results = subprocess.run(cmds, capture_output=True) with open("clang_format_output.xml", "wb") as f_out: f_out.write(results.stdout) - if results.stdout: - logger.debug("clang-format has suggestions.") if results.returncode: logger.warning( "%s raised the following error(s):\n%s", cmds[0], results.stderr.decode() @@ -420,7 +434,7 @@ def capture_clang_tools_output( version: str, checks: str, style: str, lines_changed_only: bool ): """Execute and capture all output from clang-tidy and clang-format. This aggregates - results in the [`OUTPUT`][python_action.__init__.Globals.OUTPUT]. + results in the [`OUTPUT`][cpp_linter.__init__.Globals.OUTPUT]. Args: version: The version of clang-tidy to run. @@ -455,8 +469,8 @@ def capture_clang_tools_output( tidy_notes.append(note) GlobalParser.tidy_notes.clear() # empty list to avoid duplicated output - if os.path.getsize("clang_format_output.xml"): - parse_format_replacements_xml(filename.replace("/", os.sep)) + parse_format_replacements_xml(filename.replace("/", os.sep)) + if GlobalParser.format_advice[-1].replaced_lines: if not Globals.OUTPUT: Globals.OUTPUT = "\n## :scroll: " Globals.OUTPUT += "Run `clang-format` on the following files\n" @@ -636,12 +650,17 @@ def make_annotations(style: str) -> bool: """ # log_commander obj's verbosity is hard-coded to show debug statements ret_val = False + count = 0 for note in GlobalParser.tidy_notes: ret_val = True log_commander.info(note.log_command()) + count += 1 for note in GlobalParser.format_advice: - ret_val = True - log_commander.info(note.log_command(style)) + if note.replaced_lines: + ret_val = True + log_commander.info(note.log_command(style)) + count += 1 + logger.info("Created %d annotations", count) return ret_val @@ -671,18 +690,9 @@ def main(): logger.info("processing %s event", GITHUB_EVENT_NAME) - # load event's json info about the workflow run - with open(GITHUB_EVEN_PATH, "r", encoding="utf-8") as payload: - Globals.EVENT_PAYLOAD = json.load(payload) - if logger.getEffectiveLevel() <= logging.DEBUG: - start_log_group("Event json from the runner") - logger.debug(json.dumps(Globals.EVENT_PAYLOAD)) - end_log_group() - # change working directory os.chdir(args.repo_root) - start_log_group("Get list of specified source files") if ignored: logger.info( "Ignoring the following paths/files:\n\t%s", @@ -695,6 +705,13 @@ def main(): ) exit_early = False if args.files_changed_only: + # load event's json info about the workflow run + with open(GITHUB_EVEN_PATH, "r", encoding="utf-8") as payload: + Globals.EVENT_PAYLOAD = json.load(payload) + if logger.getEffectiveLevel() <= logging.DEBUG: + start_log_group("Event json from the runner") + logger.debug(json.dumps(Globals.EVENT_PAYLOAD)) + end_log_group() get_list_of_changed_files() exit_early = not filter_out_non_source_files( args.extensions, diff --git a/python_action/thread_comments.py b/cpp_linter/thread_comments.py similarity index 100% rename from python_action/thread_comments.py rename to cpp_linter/thread_comments.py diff --git a/docs/API Reference/python_action.clang_format_xml.md b/docs/API Reference/cpp_linter.clang_format_xml.md similarity index 74% rename from docs/API Reference/python_action.clang_format_xml.md rename to docs/API Reference/cpp_linter.clang_format_xml.md index 47dc0fef..8f7a76c8 100644 --- a/docs/API Reference/python_action.clang_format_xml.md +++ b/docs/API Reference/cpp_linter.clang_format_xml.md @@ -3,4 +3,4 @@ !!! info This API is experimental and not actually used in production. -::: python_action.clang_format_xml +::: cpp_linter.clang_format_xml diff --git a/docs/API Reference/cpp_linter.clang_tidy.md b/docs/API Reference/cpp_linter.clang_tidy.md new file mode 100644 index 00000000..689b12e0 --- /dev/null +++ b/docs/API Reference/cpp_linter.clang_tidy.md @@ -0,0 +1,3 @@ +# clang_tidy module + +::: cpp_linter.clang_tidy diff --git a/docs/API Reference/python_action.clang_tidy_yml.md b/docs/API Reference/cpp_linter.clang_tidy_yml.md similarity index 75% rename from docs/API Reference/python_action.clang_tidy_yml.md rename to docs/API Reference/cpp_linter.clang_tidy_yml.md index 4b01f8d9..7db0ae26 100644 --- a/docs/API Reference/python_action.clang_tidy_yml.md +++ b/docs/API Reference/cpp_linter.clang_tidy_yml.md @@ -3,4 +3,4 @@ !!! info This API is experimental and not actually used in production. -::: python_action.clang_tidy_yml +::: cpp_linter.clang_tidy_yml diff --git a/docs/API Reference/cpp_linter.md b/docs/API Reference/cpp_linter.md new file mode 100644 index 00000000..31b45342 --- /dev/null +++ b/docs/API Reference/cpp_linter.md @@ -0,0 +1,3 @@ +# Base module + +::: cpp_linter.__init__ diff --git a/docs/API Reference/cpp_linter.run.md b/docs/API Reference/cpp_linter.run.md new file mode 100644 index 00000000..4563d550 --- /dev/null +++ b/docs/API Reference/cpp_linter.run.md @@ -0,0 +1,3 @@ +# Run module + +::: cpp_linter.run diff --git a/docs/API Reference/cpp_linter.thread_comments.md b/docs/API Reference/cpp_linter.thread_comments.md new file mode 100644 index 00000000..17a801bc --- /dev/null +++ b/docs/API Reference/cpp_linter.thread_comments.md @@ -0,0 +1,3 @@ +# thread_comments module + +::: cpp_linter.thread_comments diff --git a/docs/API Reference/python_action.clang_tidy.md b/docs/API Reference/python_action.clang_tidy.md deleted file mode 100644 index 754f8f57..00000000 --- a/docs/API Reference/python_action.clang_tidy.md +++ /dev/null @@ -1,3 +0,0 @@ -# clang_tidy module - -::: python_action.clang_tidy diff --git a/docs/API Reference/python_action.md b/docs/API Reference/python_action.md deleted file mode 100644 index ae028699..00000000 --- a/docs/API Reference/python_action.md +++ /dev/null @@ -1,3 +0,0 @@ -# Base module - -::: python_action.__init__ diff --git a/docs/API Reference/python_action.run.md b/docs/API Reference/python_action.run.md deleted file mode 100644 index 281b0fd6..00000000 --- a/docs/API Reference/python_action.run.md +++ /dev/null @@ -1,3 +0,0 @@ -# Run module - -::: python_action.run diff --git a/docs/API Reference/python_action.thread_comments.md b/docs/API Reference/python_action.thread_comments.md deleted file mode 100644 index e83bebae..00000000 --- a/docs/API Reference/python_action.thread_comments.md +++ /dev/null @@ -1,3 +0,0 @@ -# thread_comments module - -::: python_action.thread_comments diff --git a/img/demo_comment.png b/img/demo_comment.png new file mode 100644 index 0000000000000000000000000000000000000000..1040442127ad8cbe7bd31a83cb3c9e73f84faaf3 GIT binary patch literal 42823 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 A#sB~S literal 0 HcmV?d00001 diff --git a/mkdocs.yml b/mkdocs.yml index 609a75d5..d12bc632 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,12 +7,12 @@ repo_name: "shenxianpeng/cpp-linter-action" nav: - index.md - "Dev Docs": - - API Reference/python_action.md - - API Reference/python_action.run.md - - API Reference/python_action.clang_tidy.md - - API Reference/python_action.clang_tidy_yml.md - - API Reference/python_action.clang_format_xml.md - - API Reference/python_action.thread_comments.md + - API Reference/cpp_linter.md + - API Reference/cpp_linter.run.md + - API Reference/cpp_linter.clang_tidy.md + - API Reference/cpp_linter.clang_tidy_yml.md + - API Reference/cpp_linter.clang_format_xml.md + - API Reference/cpp_linter.thread_comments.md theme: name: material @@ -52,7 +52,7 @@ plugins: show_source: true heading_level: 2 watch: - - python_action + - cpp_linter markdown_extensions: - admonition diff --git a/setup.py b/setup.py index 24de2052..9e422074 100644 --- a/setup.py +++ b/setup.py @@ -12,15 +12,15 @@ setup( - name="python_action", + name="cpp_linter", # use_scm_version=True, # setup_requires=["setuptools_scm"], - version="v1.2.1", + version="1.3.1", description=__doc__, long_description=".. warning:: this is not meant for PyPi (yet)", author="Brendan Doherty", author_email="2bndy5@gmail.com", - install_requires=["requests"], #, "pyyaml"], # pyyaml is installed with clang-tidy + install_requires=["requests", "pyyaml"], # pyyaml is installed with clang-tidy license="MIT", # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ @@ -30,9 +30,8 @@ "Programming Language :: Python :: 3", ], keywords="clang clang-tidy clang-format", - packages=["python_action"], - - entry_points={"console_scripts": ["run-action=python_action.run:main"]}, + packages=["cpp_linter"], + entry_points={"console_scripts": ["cpp-linter=cpp_linter.run:main"]}, # Specifiy your homepage URL for your project here url=REPO, download_url=f"{REPO}/releases", From 3ad10e1add96e38e9bb25c6d713d37ddf0b90b9b Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Thu, 21 Oct 2021 02:55:53 -0700 Subject: [PATCH 09/43] fix some reggression from #30 (#31) * upload new demo image * Revert "upload new demo image" This reverts commit 1aff6c70c69acc366cdddd69358a173a502fe31b. * update readme & upload new demo pic * avoids duplicated checks on commits to open PR * fix workflow from last commit * Revert "fix workflow from last commit" This reverts commit 778e10dc42714dfb46a2ef1065162ac4969ab549. * rename py pkg; allow no clang-tidy & no event.json * pleasing pylint * various bug fixes * pleasing pylint * update README about `tidy-checks=-*` * increase indent in last change * increase indent again (I don't like mkdocs) * update docs * avoid nesting log groups * switch pylint to my check-python-sources action * trigger pylint action * Revert "switch pylint to my check-python-sources action" This reverts commit 1733c4f9879b036c497ca6b0c3230ce580a525ec. * fix regressions from last update * break out parsing ignore option from main() --- cpp_linter/run.py | 82 ++++++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/cpp_linter/run.py b/cpp_linter/run.py index ea416cbf..c5782780 100644 --- a/cpp_linter/run.py +++ b/cpp_linter/run.py @@ -469,12 +469,16 @@ def capture_clang_tools_output( tidy_notes.append(note) GlobalParser.tidy_notes.clear() # empty list to avoid duplicated output - parse_format_replacements_xml(filename.replace("/", os.sep)) - if GlobalParser.format_advice[-1].replaced_lines: - if not Globals.OUTPUT: - Globals.OUTPUT = "\n## :scroll: " - Globals.OUTPUT += "Run `clang-format` on the following files\n" - Globals.OUTPUT += f"- [ ] {file['filename']}\n" + if os.path.getsize("clang_format_output.xml"): + parse_format_replacements_xml(filename.replace("/", os.sep)) + if ( + GlobalParser.format_advice + and GlobalParser.format_advice[-1].replaced_lines + ): + if not Globals.OUTPUT: + Globals.OUTPUT = "\n## :scroll: " + Globals.OUTPUT += "Run `clang-format` on the following files\n" + Globals.OUTPUT += f"- [ ] {file['filename']}\n" if Globals.PAYLOAD_TIDY: if not Globals.OUTPUT: @@ -651,19 +655,56 @@ def make_annotations(style: str) -> bool: # log_commander obj's verbosity is hard-coded to show debug statements ret_val = False count = 0 - for note in GlobalParser.tidy_notes: - ret_val = True - log_commander.info(note.log_command()) - count += 1 for note in GlobalParser.format_advice: if note.replaced_lines: ret_val = True log_commander.info(note.log_command(style)) count += 1 + for note in GlobalParser.tidy_notes: + ret_val = True + log_commander.info(note.log_command()) + count += 1 logger.info("Created %d annotations", count) return ret_val +def parse_ignore_option(paths: str): + """Parse a givven string of paths (separated by a '|') into `ignored` and + `not_ignored` lists of strings. + + Args: + paths: This argument conforms to the CLI arg `--ignore` (or `-i`). + + Returns: + A tuple of lists in which each list is a set of strings. + - index 0 is the `ignored` list + - index 1 is the `not_ignored` list + """ + ignored, not_ignored = ([], []) + paths = paths.split("|") + for path in paths: + is_included = path.startswith("!") + if path.startswith("!./" if is_included else "./"): + path = path.replace("./", "", 1) # relative dir is assumed + path = path.strip() # strip leading/trailing spaces + if is_included: + not_ignored.append(path[1:]) + else: + ignored.append(path) + + if ignored: + logger.info( + "Ignoring the following paths/files:\n\t./%s", + "\n\t./".join(f for f in ignored), + ) + if not_ignored: + logger.info( + "Not ignoring the following paths/files:\n\t./%s", + "\n\t./".join(f for f in not_ignored), + ) + return (ignored, not_ignored) + + def main(): """The main script.""" @@ -674,16 +715,7 @@ def main(): logger.setLevel(int(args.verbosity)) # prepare ignored paths list - ignored, not_ignored = ([], []) - if args.ignore is not None: - args.ignore = args.ignore.split("|") - for path in args.ignore: - path = path.lstrip("./") # relative dir is assumed - path = path.strip() # strip leading/trailing spaces - if path.startswith("!"): - not_ignored.append(path[1:]) - else: - ignored.append(path) + ignored, not_ignored = parse_ignore_option("" if not args.ignore else args.ignore) # prepare extensions list args.extensions = args.extensions.split(",") @@ -693,16 +725,6 @@ def main(): # change working directory os.chdir(args.repo_root) - if ignored: - logger.info( - "Ignoring the following paths/files:\n\t%s", - "\n\t./".join(f for f in ignored), - ) - if not_ignored: - logger.info( - "Not ignoring the following paths/files:\n\t%s", - "\n\t./".join(f for f in not_ignored), - ) exit_early = False if args.files_changed_only: # load event's json info about the workflow run From d94b6a40dc75a4cec3481164438de6ce088bc0f1 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Thu, 6 Jan 2022 14:33:56 +0000 Subject: [PATCH 10/43] #32 remove runchecks.sh --- runchecks.sh | 275 --------------------------------------------------- 1 file changed, 275 deletions(-) delete mode 100644 runchecks.sh diff --git a/runchecks.sh b/runchecks.sh deleted file mode 100644 index e3f75de8..00000000 --- a/runchecks.sh +++ /dev/null @@ -1,275 +0,0 @@ -#!/bin/bash - -# global varibales -EXIT_CODE="0" -PAYLOAD_TIDY="" -FENCES=$'\n```\n' -OUTPUT="" -URLS="" -PATHNAMES="" -declare -a JSON_INDEX -FILES_LINK="" - -# alias CLI args -args=("$@") -FMT_STYLE=${args[0]} -IFS=',' read -r -a FILE_EXT_LIST <<< "${args[1]}" -TIDY_CHECKS="${args[2]}" -cd "${args[3]}" || exit "1" -CLANG_VERSION="${args[4]}" - - -################################################### -# Set the exit code (for expected exit calls). -# Optional parameter overides action-specific logic -################################################### -set_exit_code () { - if [[ $# -gt 0 ]] - then - EXIT_CODE="$1" - else - if [[ "$PAYLOAD_FORMAT" != "" || "$PAYLOAD_TIDY" != "" ]] - then - EXIT_CODE="1" - fi - fi - echo "::set-output name=checks-failed::$EXIT_CODE" -} - -################################################### -# Fetch JSON of event's changed files -################################################### -get_list_of_changed_files() { - # echo "GH_EVENT_PATH = $GITHUB_EVENT_PATH" - echo "processing $GITHUB_EVENT_NAME event" - jq '.' "$GITHUB_EVENT_PATH" - - # Use git REST API payload - if [[ "$GITHUB_EVENT_NAME" == "push" ]] - then - FILES_LINK="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA" - elif [[ "$GITHUB_EVENT_NAME" == "pull_request" ]] - then - # FILES_LINK="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/pulls//files" - # Get PR ID number from the event's JSON located in the runner's GITHUB_EVENT_PATH - FILES_LINK="$(jq -r '.pull_request._links.self.href' "$GITHUB_EVENT_PATH")/files" - fi - - # Download files list (another JSON containing files' names, URLS, statuses, & diffs/patches) - echo "Fetching files list from $FILES_LINK" - curl "$FILES_LINK" > .cpp_linter_action_changed_files.json -} - -################################################### -# extract info from downloaded JSON file -################################################### -extract_changed_files_info() { - # pull_request events have a slightly different JSON format than push events - JSON_FILES=".[" - if [[ "$GITHUB_EVENT_NAME" == "push" ]] - then - JSON_FILES=".files[" - fi - FILES_URLS_STRING=$(jq -r "$JSON_FILES].raw_url" .cpp_linter_action_changed_files.json) - FILES_NAMES_STRING=$(jq -r "$JSON_FILES].filename" .cpp_linter_action_changed_files.json) - - # convert json info to arrays - readarray -t URLS <<<"$FILES_URLS_STRING" - readarray -t PATHNAMES <<<"$FILES_NAMES_STRING" - - # Initialize the `JSON_INDEX` array. This helps us keep track of the - # source files' index in the JSON after calling `filter_out_source_files()` function. - for index in "${!URLS[@]}" - do - # this will only be used when parsing diffs from the JSON - JSON_INDEX[$index]=$index - done -} - -################################################### -# exclude undesired files (specified by the user) -################################################### -filter_out_non_source_files() { - for index in "${!URLS[@]}" - do - is_supported=0 - for i in "${FILE_EXT_LIST[@]}" - do - if [[ ${URLS[index]} == *".$i" ]] - then - is_supported=1 - break - fi - done - - if [ $is_supported == 0 ] - then - unset -v "URLS[index]" - unset -v "PATHNAMES[index]" - unset -v "JSON_INDEX[index]" - fi - done - - # exit early if nothing to do - if [ ${#URLS[@]} == 0 ] - then - set_exit_code "0" - echo "No source files need checking!" - exit $EXIT_CODE - else - echo "File names: ${PATHNAMES[*]}" - fi -} - -################################################### -# Download the files if not present. -# This function assumes that the working directory is the root of the invoking repo. -# Note that all github actions are run in path specified by the environment variable GITHUB_WORKSPACE. -################################################### -verify_files_are_present() { - # URLS, PATHNAMES, & PATCHES are parallel arrays - for index in "${!PATHNAMES[@]}" - do - if [[ ! -f "${PATHNAMES[index]}" ]] - then - echo "Downloading ${URLS[index]}" - curl --location --insecure --remote-name "${URLS[index]}" - fi - done -} - -################################################### -# get the patch info from the JSON. -# required parameter is the index in the JSON_INDEX array -################################################### -get_patch_info() { - # patches are multiline strings. Thus, they need special attention because of the '\n' used within. - # - # a git diff (aka "patch" in the REST API) can have multiple "hunks" for a single file. - # hunks start with `@@ -, +, @@` - # A positive sign indicates the incoming changes, while a negative sign indicates existing code that was changed - # Any changed lines will also have a prefixed `-` or `+`. - - file_status=$(jq -r "$JSON_FILES${JSON_INDEX[$1]}].status" .cpp_linter_action_changed_files.json) - - # we only need the first line stating the line numbers changed (ie "@@ -1,5 +1,5 @@"") - patched_lines=$(jq -r -c "$JSON_FILES${JSON_INDEX[$1]}].patch" .cpp_linter_action_changed_files.json) - patches=$(echo "$patched_lines" | grep -o "@@ \\-[1-9]*,[1-9]* +[1-9]*,[1-9]* @@" | grep -o " +[1-9]*,[1-9]*" | tr -d "\\n" | sed 's; +;;; s;+;;g') - - # if there is no patch field, we need to handle 'renamed' as an edgde case - if [[ "$patches" == "" ]] - then - echo "${PATHNAMES[$1]} was $file_status" - # don't bother checking renamed files with no changes to file's content - patches="0,0" - fi - echo "$patches" -} - -################################################### -# execute clang-tidy/format & assemble a unified OUTPUT -################################################### -capture_clang_tools_output() { - clang-tidy --version - - for index in "${!URLS[@]}" - do - filename=$(basename ${URLS[index]}) - if [[ -f "${PATHNAMES[index]}" ]] - then - filename="${PATHNAMES[index]}" - fi - - true > clang_format_report.txt - true > clang_tidy_report.txt - - echo "Performing checkup on $filename" - # echo "incoming changed lines: $(get_patch_info $index)" - - if [ "$TIDY_CHECKS" == "" ] - then - clang-tidy-"$CLANG_VERSION" "$filename" >> clang_tidy_report.txt - else - clang-tidy-"$CLANG_VERSION" -checks="$TIDY_CHECKS" "$filename" >> clang_tidy_report.txt - fi - clang-format-"$CLANG_VERSION" -style="$FMT_STYLE" --dry-run "$filename" 2> clang_format_report.txt - - if [[ $(wc -l < clang_tidy_report.txt) -gt 0 ]] - then - PAYLOAD_TIDY+=$"### ${PATHNAMES[index]}" - PAYLOAD_TIDY+="$FENCES" - sed -i "s|$GITHUB_WORKSPACE/||g" clang_tidy_report.txt - # cat clang_tidy_report.txt - PAYLOAD_TIDY+=$(cat clang_tidy_report.txt) - PAYLOAD_TIDY+="$FENCES" - fi - - if [[ $(wc -l < clang_format_report.txt) -gt 0 ]] - then - if [ "$OUTPUT" == "" ] - then - OUTPUT=$'\n## Run `clang-format` on the following files\n' - fi - OUTPUT+="- [ ] ${PATHNAMES[index]}"$'\n' - fi - done - - if [ "$PAYLOAD_TIDY" != "" ]; then - OUTPUT+=$'\n---\n## Output from `clang-tidy`\n' - OUTPUT+="$PAYLOAD_TIDY" - fi - - echo "OUTPUT is:" - echo "$OUTPUT" -} - -################################################### -# POST action's results using REST API -################################################### -post_results() { - # check for access token (ENV VAR needed for git API calls) - if [[ -z "$GITHUB_TOKEN" ]] - then - set_exit_code "1" - echo "The GITHUB_TOKEN is required." - exit "$EXIT_CODE" - fi - - COMMENTS_URL=$(jq -r .pull_request.comments_url "$GITHUB_EVENT_PATH") - COMMENT_COUNT=$(jq -r .comments "$GITHUB_EVENT_PATH") - if [[ "$GITHUB_EVENT_NAME" == "push" ]] - then - COMMENTS_URL="$FILES_LINK/comments" - COMMENT_COUNT=$(jq -r .commit.comment_count .cpp_linter_action_changed_files.json) - fi - echo "COMMENTS_URL: $COMMENTS_URL" - echo "Number of Comments = $COMMENT_COUNT" - if [[ $COMMENT_COUNT -gt 0 ]] - then - # get the list of comments - curl "$COMMENTS_URL" > ".comments.json" - fi - PAYLOAD=$(echo '{}' | jq --arg body "$OUTPUT" '.body = $body') - - # creating PR comments is the same API as creating issue. Creating commit comments have more optional parameters (but same required API) - curl -s -S -H "Authorization: token $GITHUB_TOKEN" --header "Content-Type: application/vnd.github.VERSION.text+json" "$COMMENTS_URL" --data "$PAYLOAD" -} - -################################################### -# The main body of this script (all function calls) -################################################### -# for local testing (without docker): -# 1. Set the env var GITHUB_EVENT_NAME to "push" or "pull_request" -# 2. Download and save the event's payload (in JSON) to a file named ".cpp_linter_action_changed_files.json". -# See the FILES_LINK variable in the get_list_of_changed_files() function for the event's payload. -# 3. Comment out the following calls to `get_list_of_changed_files` & `post_results` functions -# 4. Run this script using `./run_checks.sh